Anagram and Palindrome Problems Explained — Java DSA Guide for Beginners
Every tech interview on the planet has at least one string manipulation problem, and anagrams and palindromes are the two most popular flavours. Google uses palindrome-style logic in DNA sequence analysis. Spell checkers use anagram detection to suggest corrections. Even cybersecurity tools use these ideas to detect scrambled spam keywords. The reason they're so popular isn't to torture you — it's because solving them correctly requires you to understand how characters are stored, how to count efficiently, and how to think about equality in a smarter way than just ==.
The problem they solve is deceptively simple: given one or two strings, can you determine a relationship between their characters without brute-forcing every possible combination? A naive approach to checking if two strings are anagrams might compare every permutation — that explodes to factorial time complexity for long strings. The clever approaches you'll learn here run in linear time, which is why interviewers love them.
By the end of this article you'll be able to write clean, efficient Java solutions for both anagram and palindrome problems from scratch. You'll understand exactly why each approach works, spot the subtle bugs that trip up 80% of beginners, and confidently walk through your thought process in an interview setting.
What Is an Anagram — And How Do You Detect One in Code?
Two strings are anagrams if they contain exactly the same characters in exactly the same frequencies — just arranged differently. 'Astronomer' and 'Moon starer' are anagrams (ignoring spaces). 'Cat' and 'Car' are NOT — because 't' and 'r' are different characters.
The core insight is this: if you count how many times each letter appears in both strings, an anagram will produce identical frequency counts. That's your algorithm.
The simplest way to implement this in Java is with a frequency array of size 26 — one slot for each letter of the alphabet. You increment the count for each character in the first string, then decrement for each character in the second string. If every slot ends up at zero, they're anagrams. This runs in O(n) time and O(1) space — because the array is always size 26, no matter how long the strings are.
Why not just sort both strings and compare? You can — and it works — but sorting costs O(n log n). The frequency array approach is strictly faster. For small strings it barely matters, but in a system processing millions of queries per second, that difference is huge. Good engineers think about this from the start.
public class AnagramChecker { /** * Checks if two strings are anagrams of each other. * Ignores case, ignores spaces — just like real word puzzles. * * Time Complexity: O(n) — we scan each string once * Space Complexity: O(1) — fixed-size array of 26, regardless of input size */ public static boolean areAnagrams(String firstWord, String secondWord) { // Step 1: Normalise both strings — lowercase, no spaces // This means "Listen" and "Silent" will still match String cleanFirst = firstWord.toLowerCase().replaceAll("\\s", ""); String cleanSecond = secondWord.toLowerCase().replaceAll("\\s", ""); // Step 2: Quick sanity check — if lengths differ after cleaning, // they can't possibly be anagrams (different total character counts) if (cleanFirst.length() != cleanSecond.length()) { return false; } // Step 3: Create a frequency array — 26 slots, one per letter a-z // All slots start at 0 by default in Java int[] letterFrequency = new int[26]; // Step 4: Walk through the first word, incrementing the count // for each character we encounter. // 'a' has ASCII value 97. Subtracting 'a' maps 'a'->0, 'b'->1, etc. for (char letter : cleanFirst.toCharArray()) { letterFrequency[letter - 'a']++; } // Step 5: Walk through the second word, DECREMENTING the count. // If the second word has a letter not in the first, its slot goes negative. for (char letter : cleanSecond.toCharArray()) { letterFrequency[letter - 'a']--; } // Step 6: If every slot is exactly zero, the frequencies matched perfectly. // Any non-zero slot means one word had more of some letter than the other. for (int frequency : letterFrequency) { if (frequency != 0) { return false; // Mismatch found — not an anagram } } return true; // All frequencies balanced out — they ARE anagrams! } public static void main(String[] args) { // Test case 1: Classic anagram pair System.out.println(areAnagrams("listen", "silent")); // true // Test case 2: Anagram with mixed case System.out.println(areAnagrams("Astronomer", "Moon starer")); // true // Test case 3: Not an anagram — 't' vs 'r' System.out.println(areAnagrams("cat", "car")); // false // Test case 4: Different lengths System.out.println(areAnagrams("hello", "helloo")); // false // Test case 5: Single characters that match System.out.println(areAnagrams("a", "a")); // true } }
true
false
false
true
What Is a Palindrome — And the Two-Pointer Trick That Makes It Easy
A palindrome reads the same forwards and backwards. 'Racecar'. 'Madam'. 'A man a plan a canal Panama' (if you strip spaces and ignore case). The concept is beautifully simple, but there's a subtle trap: do you handle even-length and odd-length palindromes differently?
The answer with the two-pointer technique is no — and that's what makes it elegant. You place one pointer at the very start of the string and one at the very end. You compare the characters those pointers are pointing to. If they match, you move both pointers inward — left pointer goes right, right pointer goes left. If they ever don't match, it's not a palindrome. You stop when the pointers meet or cross.
For 'racecar' (7 characters, odd length): r==r ✓, a==a ✓, c==c ✓, middle 'e' is never compared against anything. For 'abba' (4 characters, even length): a==a ✓, b==b ✓, pointers cross, done. Both cases handled by the exact same loop.
This is O(n) time and O(1) space — you're not creating any new data structures, just using two integer indices as pointers. This matters in memory-constrained environments.
public class PalindromeChecker { /** * Checks if a string is a palindrome using the two-pointer technique. * Ignores spaces and case so real phrases like "Race a car" work correctly. * * Time Complexity: O(n) — each character is visited at most once * Space Complexity: O(n) — only for the cleaned string; logic itself is O(1) */ public static boolean isPalindrome(String inputText) { // Step 1: Clean the input — remove spaces, make lowercase // We want "A man a plan a canal Panama" to return true String cleanedText = inputText.toLowerCase().replaceAll("[^a-z0-9]", ""); // Step 2: Edge case — an empty string or single character is always a palindrome if (cleanedText.length() <= 1) { return true; } // Step 3: Set up two pointers — one at each end of the string int leftIndex = 0; int rightIndex = cleanedText.length() - 1; // Step 4: March the pointers toward the centre // They stop when they meet (odd length) or cross (even length) while (leftIndex < rightIndex) { char leftChar = cleanedText.charAt(leftIndex); char rightChar = cleanedText.charAt(rightIndex); // Step 5: If the characters don't match — it's NOT a palindrome. // We can exit immediately, no need to check further. if (leftChar != rightChar) { return false; } // Step 6: Characters matched — move both pointers one step inward leftIndex++; rightIndex--; } // Step 7: If we made it through the whole loop without returning false, // every mirrored pair matched — it IS a palindrome! return true; } /** * BONUS: Check if a string is a palindrome by comparing it to its reverse. * Simpler to read, but uses O(n) extra space for the reversed string. * Great to mention in an interview as the naive approach before optimising. */ public static boolean isPalindromeNaive(String inputText) { String cleanedText = inputText.toLowerCase().replaceAll("[^a-z0-9]", ""); String reversedText = new StringBuilder(cleanedText).reverse().toString(); // Direct string comparison — true only if they're identical return cleanedText.equals(reversedText); } public static void main(String[] args) { // Test case 1: Classic odd-length palindrome System.out.println(isPalindrome("racecar")); // true // Test case 2: Classic even-length palindrome System.out.println(isPalindrome("abba")); // true // Test case 3: Famous phrase palindrome (spaces and mixed case ignored) System.out.println(isPalindrome("A man a plan a canal Panama")); // true // Test case 4: Not a palindrome System.out.println(isPalindrome("hello")); // false // Test case 5: Single character System.out.println(isPalindrome("z")); // true // Test case 6: Naive approach for comparison System.out.println(isPalindromeNaive("Madam")); // true } }
true
true
false
true
true
Levelling Up — Grouping Anagrams and Finding the Longest Palindromic Substring
Once you're comfortable with the basics, interviews throw harder variants at you. The two most common are: (1) Given a list of words, group all the anagrams together. (2) Find the longest substring inside a string that is itself a palindrome.
For grouping anagrams, the key insight is that if you sort the letters of any word, all its anagrams produce the same sorted string — that sorted string becomes a key in a HashMap. 'eat', 'tea', 'ate' all sort to 'aet'. You group words by their sorted key.
For the longest palindromic substring, the classic approach is 'expand from centre'. For every character in the string (and every gap between characters for even-length palindromes), you expand outward as long as the characters on both sides match. You track the longest expansion you found. This runs in O(n²) time — acceptable for interview settings. There's a fancier O(n) algorithm called Manacher's, but interviewers don't expect it unless you're at a top-tier FAANG role.
These two problems show up in the same interview as 'easy/medium' pairs. Nail them both.
import java.util.*; public class AdvancedStringProblems { // ───────────────────────────────────────────────────────────── // PROBLEM 1: Group Anagrams // Input: ["eat", "tea", "tan", "ate", "nat", "bat"] // Output: [["eat","tea","ate"], ["tan","nat"], ["bat"]] // ───────────────────────────────────────────────────────────── public static List<List<String>> groupAnagrams(String[] words) { // A HashMap where: // key = the sorted version of a word (e.g. "aet" for "eat","tea","ate") // value = the list of original words that sort to that key Map<String, List<String>> anagramGroups = new HashMap<>(); for (String word : words) { // Sort the characters of the word to create a canonical key // "eat" -> char array ['e','a','t'] -> sorted ['a','e','t'] -> "aet" char[] wordCharacters = word.toCharArray(); Arrays.sort(wordCharacters); String sortedKey = new String(wordCharacters); // If this key doesn't exist in the map yet, create a new list for it // computeIfAbsent is cleaner than a manual if-null check anagramGroups.computeIfAbsent(sortedKey, k -> new ArrayList<>()); // Add the original word to its anagram group anagramGroups.get(sortedKey).add(word); } // Return all the groups as a list of lists return new ArrayList<>(anagramGroups.values()); } // ───────────────────────────────────────────────────────────── // PROBLEM 2: Longest Palindromic Substring // Input: "babad" // Output: "bab" (or "aba" — both are valid) // // Input: "cbbd" // Output: "bb" // ───────────────────────────────────────────────────────────── public static String longestPalindromicSubstring(String inputText) { if (inputText == null || inputText.length() < 1) return ""; // Track the start and end indices of the longest palindrome found int longestStart = 0; int longestEnd = 0; for (int centreIndex = 0; centreIndex < inputText.length(); centreIndex++) { // Case 1: Odd-length palindromes (single character centre) // e.g. "racecar" — centre is 'e' int oddLength = expandFromCentre(inputText, centreIndex, centreIndex); // Case 2: Even-length palindromes (gap between two characters as centre) // e.g. "abba" — centre is the gap between the two b's int evenLength = expandFromCentre(inputText, centreIndex, centreIndex + 1); // Take whichever expansion was longer int maxLengthAtThisCentre = Math.max(oddLength, evenLength); // Check if this is longer than the best we've found so far if (maxLengthAtThisCentre > longestEnd - longestStart + 1) { // Back-calculate the start and end indices from the length // (integer division handles both odd and even lengths correctly) longestStart = centreIndex - (maxLengthAtThisCentre - 1) / 2; longestEnd = centreIndex + maxLengthAtThisCentre / 2; } } // Extract and return the actual substring return inputText.substring(longestStart, longestEnd + 1); } /** * Helper: Expands outward from a centre position as long as characters match. * Returns the LENGTH of the palindrome found. * * For odd palindromes: call with (index, index) * For even palindromes: call with (index, index + 1) */ private static int expandFromCentre(String text, int leftPointer, int rightPointer) { // Expand outward while within bounds AND characters still match while (leftPointer >= 0 && rightPointer < text.length() && text.charAt(leftPointer) == text.charAt(rightPointer)) { leftPointer--; // Move left pointer further left rightPointer++; // Move right pointer further right } // When the loop ends, pointers have gone ONE step too far. // The actual palindrome length is rightPointer - leftPointer - 1. return rightPointer - leftPointer - 1; } public static void main(String[] args) { // ── Group Anagrams Demo ── String[] wordList = {"eat", "tea", "tan", "ate", "nat", "bat"}; List<List<String>> groups = groupAnagrams(wordList); System.out.println("Anagram Groups:"); for (List<String> group : groups) { System.out.println(" " + group); } System.out.println(); // ── Longest Palindromic Substring Demo ── System.out.println("Longest palindrome in 'babad': " + longestPalindromicSubstring("babad")); System.out.println("Longest palindrome in 'cbbd': " + longestPalindromicSubstring("cbbd")); System.out.println("Longest palindrome in 'racecar': " + longestPalindromicSubstring("racecar")); System.out.println("Longest palindrome in 'abacaba': " + longestPalindromicSubstring("abacaba")); } }
[eat, tea, ate]
[tan, nat]
[bat]
Longest palindrome in 'babad': bab
Longest palindrome in 'cbbd': bb
Longest palindrome in 'racecar': racecar
Longest palindrome in 'abacaba': abacaba
| Aspect | Frequency Array Approach | Sort-and-Compare Approach |
|---|---|---|
| Time Complexity | O(n) — single pass through each string | O(n log n) — sorting dominates |
| Space Complexity | O(1) — fixed 26-slot array always | O(n) — sorted copy of string created |
| Code Simplicity | Moderate — requires index mapping trick | Very simple — Arrays.sort + equals() |
| Works for Unicode? | No — only handles 26 English letters as-is | Yes — Arrays.sort handles any char set |
| Best for | Performance-critical / interview optimal answer | Quick solution or non-ASCII characters |
| Handles duplicates? | Yes — frequency count tracks exact quantities | Yes — sorted duplicates stay adjacent |
🎯 Key Takeaways
- The frequency array trick (26 slots, letter - 'a' indexing) solves anagram detection in O(n) time and O(1) space — memorise this pattern cold.
- The two-pointer technique for palindrome checking is O(n) time O(1) space — left pointer starts at 0, right at length-1, they march inward until they meet or a mismatch is found.
- Always run BOTH the odd-centre (i, i) and even-centre (i, i+1) expansions in the expand-from-centre palindrome approach — missing the even case is the #1 silent bug.
- Normalise your strings first — lowercase and strip spaces/punctuation — BEFORE running any anagram or palindrome logic. Skipping this step causes subtle failures on real-world inputs.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Using == instead of .equals() to compare strings — Your isPalindrome check returns wrong results even when strings look identical — Always use .equals() for String content comparison in Java. == checks if they're the same object in memory, not if they have the same content.
cleanedText == reversedTextwill almost always be false even for 'racecar'. - ✕Mistake 2: Forgetting to normalise case and spaces before checking — 'Racecar' returns false, 'A man a plan a canal Panama' returns false — Always call .toLowerCase() and strip non-alphanumeric characters with .replaceAll('[^a-z0-9]', '') BEFORE running your palindrome or anagram logic. Normalise first, check second.
- ✕Mistake 3: Only checking odd-length palindromes in the expand-from-centre approach — 'abba', 'deed', 'noon' all incorrectly return a single character as the longest palindrome — Make sure you run expandFromCentre(i, i) AND expandFromCentre(i, i+1) for every index. The first handles odd-length, the second handles even-length. Both calls are required every single iteration.
Interview Questions on This Topic
- QGiven a string, determine if it is a valid palindrome considering only alphanumeric characters and ignoring case — what is your approach and what is its time and space complexity?
- QGiven an array of strings, group all anagrams together and return them as a list of lists — walk me through your solution and explain why you chose that data structure.
- QHow would you find ALL palindromic substrings in a string, not just the longest one? What changes in your approach — and what is the total time complexity now?
Frequently Asked Questions
What is the difference between an anagram and a palindrome?
An anagram is a relationship between TWO strings — one is a rearrangement of the other ('listen' and 'silent'). A palindrome is a property of ONE string — it reads the same forwards and backwards ('racecar'). They're completely different concepts that just happen to both involve rearranging characters.
Is every palindrome an anagram of itself?
Yes, technically — any string is an anagram of itself since it contains the same characters with the same frequencies. But the more interesting question is whether the reverse of a palindrome is an anagram of the original. It always is, because the reverse contains identical characters. This is a fun edge case worth mentioning in interviews to show deep thinking.
Why use a frequency array instead of a HashMap for anagram checking?
When the input is strictly lowercase English letters (a-z), the frequency array is faster and uses less memory. A HashMap has hashing overhead and stores boxed Integer objects. The array is a fixed 26 slots of primitive ints. If you're dealing with Unicode, full ASCII, or arbitrary character sets, switch to a HashMap — but always state your assumption about the character set to your interviewer.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.