Intermediate 5 min · March 05, 2026

String Manipulation Patterns — Loop Concatenation OOM

Autocomplete crashed OOM: 2GB/min temporary char[] from loop concatenation.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Sliding window: contiguous substring problems (longest/shortest/min window)
  • Two pointers: symmetry problems (palindrome, reverse, compare ends)
  • Frequency map: character count problems (anagram, permutation, unique chars)
  • String hashing: grouping/dedup problems (group anagrams, Rabin-Karp search)
  • Build-then-join: always use StringBuilder, never + in a loop
  • Search engines, autocomplete, log dedup, and content fingerprinting all rely on these patterns.
  • The missing Math.max guard in sliding window is the #1 silent bug.
  • String concatenation with + inside a loop. O(n^2) on 10K-char strings. Use StringBuilder.
Plain-English First

Imagine you're sorting through a long receipt from a grocery store, looking for every time you bought milk. You don't reread the entire receipt from scratch each time — you scan once, maybe use your finger as a pointer, and keep a tally. String manipulation patterns work exactly the same way: they're clever scanning strategies that let your code process text efficiently without doing unnecessary repetitive work. Once you learn the patterns, you stop reinventing the wheel every time a string problem shows up.

Strings are the most common data type in production systems. Search engines scan billions of words per second. Autocomplete predicts the next word from a prefix. Log deduplication identifies duplicate entries by content fingerprinting. Each of these relies on a small set of string manipulation patterns.

The core problem with naive string processing is O(n^2) concatenation and O(n^2) substring enumeration. A 10,000-character string processed naively can require 100 million comparisons. The patterns covered here — sliding window, two pointers, frequency maps, and hashing — reduce most string problems to O(n) or O(n log n).

The common misconception is that 'string problems are just array problems with characters.' While the algorithmic patterns overlap, strings introduce unique concerns: immutable concatenation cost, character encoding (ASCII vs Unicode), case sensitivity, and the choice between int[26] and HashMap for frequency counting. Understanding these distinctions is what separates a correct solution from a production-grade one.

Worked Example — Reverse Words in a String

Input: s = ' the sky is blue '

  1. Split by whitespace (ignore empty tokens): words = ['the','sky','is','blue'].
  2. Reverse the list: ['blue','is','sky','the'].
  3. Join with single space: 'blue is sky the'.

Now trace character-by-character palindrome check on 'racecar': 1. left=0 (r), right=6 (r). Match. 2. left=1 (a), right=5 (a). Match. 3. left=2 (c), right=4 (c). Match. 4. left=3 (e) = right=3 (e). Left >= right, stop. Result: palindrome.

Anagram check 'listen' vs 'silent': sort both → 'eilnst' == 'eilnst'. True. Or use frequency count: count each char in both strings; if all counts match, they are anagrams. O(n) time with hash map vs O(n log n) with sorting.

io/thecodeforge/algo/StringWorkedExamples.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package io.thecodeforge.algo;

import java.util.Arrays;

public class StringWorkedExamples {

    /**
     * Reverses the order of words in a string.
     * Handles multiple spaces between words.
     */
    public static String reverseWords(String s) {
        // Split on whitespace, trim leading/trailing spaces first
        String[] words = s.trim().split("\\s+");

        // Reverse in-place using two pointers
        int left = 0, right = words.length - 1;
        while (left < right) {
            String temp = words[left];
            words[left] = words[right];
            words[right] = temp;
            left++;
            right--;
        }

        // Join with single space — never concatenate in a loop
        return String.join(" ", words);
    }

    /**
     * Checks if a string is a palindrome (alphanumeric only, case-insensitive).
     */
    public static boolean isPalindrome(String s) {
        int left = 0, right = s.length() - 1;
        while (left < right) {
            while (left < right && !Character.isLetterOrDigit(s.charAt(left))) left++;
            while (left < right && !Character.isLetterOrDigit(s.charAt(right))) right--;
            if (Character.toLowerCase(s.charAt(left)) != Character.toLowerCase(s.charAt(right))) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }

    /**
     * Checks if two strings are anagrams using int[26] frequency map. O(n) time, O(1) space.
     */
    public static boolean areAnagrams(String s1, String s2) {
        if (s1.length() != s2.length()) return false;

        int[] freqint i = 0; i < s1.length(); = new int[26];
        for ( i++) {
            freq[Character.toLowerCase(s1.charAt(i)) - 'a']++;
            freq[Character.toLowerCase(s2.charAt(i)) - 'a']--;
        }
        for (int count : freq) {
            if (count != 0) return false;
        }
        return true;
    }

    public static void main(String[] args) {
        System.out.println(reverseWords("  the sky  is  blue  ")); // "blue is sky the"
        System.out.println(isPalindrome("A man, a plan, a canal: Panama")); // true
        System.out.println(areAnagrams("listen", "silent")); // true
        System.out.println(areAnagrams("hello", "world")); // false
    }
}
Output
blue is sky the
true
true
false
The Three Building Blocks
  • Compare characters: two pointers moving inward. Palindrome, reverse, symmetry.
  • Count characters: frequency map (int[26] or HashMap). Anagram, permutation, unique.
  • Track region: sliding window with two boundary pointers. Longest/shortest substring.
  • Combine patterns: minimum window substring = sliding window + frequency map.
  • Build output: always StringBuilder or String.join, never + in a loop.
Production Insight
A log deduplication service used anagram detection to identify re-ordered log entries as duplicates. Two log lines with the same words in different order were treated as duplicates. The service processed 10 million log lines per hour. Using sorted-key anagram detection: O(k log k) per line with k=50 average word count. Using frequency-count key: O(k) per line. The frequency-count approach reduced per-line processing from 12μs to 3μs — a 4x speedup that saved $2,400/month in compute costs.
Key Takeaway
Every string problem reduces to compare, count, or track. Reverse words and palindrome use two pointers. Anagram uses frequency maps. Most interview problems combine two patterns. Always build output with StringBuilder or String.join, never + in a loop.

Key String Manipulation Patterns — Plain English

String problems cluster into a handful of recurring patterns. Recognising the pattern determines the approach.

Pattern 1 — Frequency map (anagram, permutation check): Build a character count dict. Two strings are anagrams when their counts are equal.

Pattern 2 — Two pointers (palindrome, reverse): left=0, right=n-1. While left<right: compare or swap. O(n), O(1) space.

Pattern 3 — Sliding window (minimum window substring, longest without repeating): Expand right; shrink left when constraint violated.

Pattern 4 — Build output in a list, join once: Appending to a list is O(1); string concatenation with '+' is O(n). Always join at the end.

Step-by-step — is 'listen' an anagram of 'silent'? 1. Both length 6: ok. 2. Count 'listen': {l:1,i:1,s:1,t:1,e:1,n:1}. 3. Count 'silent': {s:1,i:1,l:1,e:1,n:1,t:1}. 4. Maps equal. Answer: True.

Step-by-step — reverse 'hello' in-place: left=0,right=4: swap h↔o → 'oellh'. left=1,right=3: swap e↔l → 'olleh'. Done.

io/thecodeforge/algo/StringPatterns.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package io.thecodeforge.algo;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class StringPatterns {

    /**
     * Pattern 1: Frequency map anagram check. O(n) time, O(1) space.
     */
    public static boolean areAnagrams(String s1, String s2) {
        if (s1.length() != s2.length()) return false;
        int[] freq = new int[26];
        for (int i = 0; i < s1.length(); i++) {
            freq[s1.charAt(i) - 'a']++;
            freq[s2.charAt(i) - 'a']--;
        }
        for (int count : freq) {
            if (count != 0) return false;
        }
        return true;
    }

    /**
     * Pattern 2: Two-pointer reverse. O(n) time, O(n) space (char array).
     */
    public static String reverseString(String s) {
        char[] chars = s.toCharArray();
        int left = 0, right = chars.length - 1;
        while (left < right) {
            char temp = chars[left];
            chars[left] = chars[right];
            chars[right] = temp;
            left++;
            right--;
        }
        return new String(chars);
    }

    /**
     * Pattern 3: Sliding window — longest substring without repeating chars.
     */
    public static int longestUniqueSubstring(String s) {
        Map<Character, Integer> lastSeen = new HashMap<>();
        int maxLen = 0, windowStart = 0;
        for (int windowEnd = 0; windowEnd < s.length(); windowEnd++) {
            char ch = s.charAt(windowEnd);
            if (lastSeen.containsKey(ch)) {
                windowStart = Math.max(windowStart, lastSeen.get(ch));
            }
            lastSeen.put(ch, windowEnd + 1);
            maxLen = Math.max(maxLen, windowEnd - windowStart + 1);
        }
        return maxLen;
    }

    /**
     * Pattern 4: Build output with StringBuilder, join at end.
     */
    public static String reverseWords(String s) {
        String[] words = s.trim().split("\\s+");
        StringBuilder sb = new StringBuilder();
        for (int i = words.length - 1; i >= 0; i--) {
            sb.append(words[i]);
            if (i > 0) sb.append(' ');
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        System.out.println(areAnagrams("listen", "silent")); // true
        System.out.println(reverseString("hello")); // olleh
        System.out.println(longestUniqueSubstring("abcabcbb")); // 3
        System.out.println(reverseWords("  the sky  is  blue  ")); // "blue is sky the"
    }
}
Output
true
olleh
3
blue is sky the
Pattern Recognition Decision Tree
  • Contiguous substring → sliding window (fixed or variable size).
  • Symmetry/comparison from ends → two pointers.
  • Character counting/frequency → frequency map (int[26] or HashMap).
  • Grouping by content → string hashing (sorted key or frequency key).
  • Building output → StringBuilder or String.join, never + in a loop.
Production Insight
A search engine's query expansion service used pattern recognition to route queries to the correct handler. Queries with repeated characters (e.g., 'aa. Queries requesting 'similar words' routed to anagram handlers. This routing reduced average query processing time from 8ms to 2ms by avoiding pattern-mismatch algorithms.
Key Takeaway
Pattern recognition is the skill. Implementation is mechanical. Ask three questions: contiguous substring? symmetry? frequency counting? The answer determines the pattern. Most problems combine two patterns.

The Sliding Window Pattern — Scan Once, Answer Fast

io/thecodeforge/algo/LongestUniqueSubstring.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package io.thecodeforge.algo;

import java.util.HashMap;
import java.util.Map;

public class LongestUniqueSubstring {

    /**
     * Finds the length of the longest substring without repeating characters.
     * Classic variable-size sliding window problem.
     *
     * Time:  O(n) — each character is visited at most twice (once by right, once by left)
     * Space: O(min(n, alphabet)) — the map holds at most one entry per unique character
     */
    public static int findLongestUniqueSubstring(String input) {
        // Maps each character to the index AFTER its last known position.
        // Storing index+1 lets us jump the left pointer forward in one step.
        Map<Character, Integer> lastSeenAt = new HashMap<>();

        int maxLength = 0;
        int windowStart = 0; // left edge of our sliding window

        for (int windowEnd = 0; windowEnd < input.length(); windowEnd++) {
            char currentChar = input.charAt(windowEnd);

            // If this character already exists inside our current window,
            // move the left edge just past its previous position so the
            // window no longer contains the duplicate.
            if (lastSeenAt.containsKey(currentChar)) {
                // Math.max prevents the window from moving BACKWARD
                // if the duplicate was outside the current window.
                windowStart = Math.max(windowStart, lastSeenAt.get(currentChar));
            }

            // Record this character's "next safe position" for the left pointer
            lastSeenAt.put(currentChar, windowEnd + 1);

            // Current window length = windowEnd - windowStart + 1
            maxLength = Math.max(maxLength, windowEnd - windowStart + 1);
        }

        return maxLength;
    }

    public static void main(String[] args) {
        System.out.println(findLongestUniqueSubstring("abcabcbb"));  // 3  → "abc"
        System.out.println(findLongestUniqueSubstring("bbbbb"));     // 1  → "b"
        System.out.println(findLongestUniqueSubstring("pwwkew"));    // 3  → "wke"
        System.out.println(findLongestUniqueSubstring(""));          // 0  → empty string
        System.out.println(findLongestUniqueSubstring("abcdefg"));   // 7  → whole string
    }
}
Output
3
1
3
0
7
Watch Out: The Missing Math.max
  • Without Math.max: windowStart jumps backward on characters seen before the current window.
  • With Math.max: windowStart only moves forward. The window invariant is preserved.
  • Test cases that catch this bug: 'abba' (expected 2), 'tmmzuxt' (expected 5).
  • The guard ensures each character is visited at most twice — once by right, once by left.
  • This is the #1 bug in sliding window implementations in interviews.
Production Insight
A real-time log deduplication service used sliding window to find the longest sequence of unique log messages in a 10-second window. Without the Math.max guard, the service reported windows of 50,000 unique messages when the actual maximum was 12. The bug was invisible in testing (small inputs) but manifested in production with 100,000+ log messages per second. The fix: add Math.max(windowStart, lastSeen.get(messageHash) + 1). The service correctly reported the longest unique sequence after the fix.
Key Takeaway
Variable window expands right freely and shrinks left when the constraint is violated. The Math.max guard on the left boundary is mandatory — without it, the window silently grows incorrectly on repeated elements. Test with 'abba' and 'tmmzuxt' to catch this bug.

Frequency Maps — When You Care About Character Counts, Not Positions

A frequency map (also called a character count array or histogram) is a data structure that counts how many times each character appears. It transforms a string into a numerical fingerprint. Two strings with identical fingerprints are anagrams of each other. That's powerful.

For strings restricted to lowercase English letters, you can use an int[26] array instead of a HashMap. Array access is O(1) with no hashing overhead, and comparing two int[26] arrays takes exactly 26 comparisons — constant time regardless of string length. This is a significant practical speedup that interviewers love to hear you mention.

Frequency maps unlock the fixed-size sliding window approach for anagram problems. You precompute the frequency map of the pattern, then slide a same-length window across the text. Each slide, you add one character and remove one character from your running frequency map. When the running map matches the pattern map, you've found an anagram. One pass. O(n) time.

io/thecodeforge/algo/AnagramFinder.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package io.thecodeforge.algo;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class AnagramFinder {

    /**
     * Returns all starting indices where an anagram of 'pattern' begins in 'text'.
     * Uses a fixed-size sliding window with an int[26] frequency map.
     *
     * Time:  O(n) where n = text.length()
     * Space: O(1) — the two int[26] arrays are constant size regardless of input
     */
    public static List<Integer> findAnagramStartIndices(String text, String pattern) {
        List<Integer> resultIndices = new ArrayList<>();

        // Edge case: pattern can't fit inside text
        if (pattern.length() > text.length()) {
            return resultIndices;
        }

        int windowSize = pattern.length();

        // Build the frequency fingerprint for the target pattern
        int[] patternFrequency = new int[26];
        for (char ch : pattern.toCharArray()) {
            patternFrequency[ch - 'a']++; // 'a'=0, 'b'=1, ..., 'z'=25
        }

        // Build the frequency fingerprint for the first window in text
        int[] windowFrequency = new int[26];
        for (int i = 0; i < windowSize; i++) {
            windowFrequency[text.charAt(i) - 'a']++;
        }

        // Check the first window before we start sliding
        if (Arrays.equals(patternFrequency, windowFrequency)) {
            resultIndices.add(0);
        }

        // Slide the window one character at a time
        for (int windowEnd = windowSize; windowEnd < text.length(); windowEnd++) {
            // Add the new character entering the right side of the window
            windowFrequency[text.charAt(windowEnd) - 'a']++;

            // Remove the character leaving the left side of the window
            int windowStart = windowEnd - windowSize;
            windowFrequency[text.charAt(windowStart) - 'a']--;

            // Arrays.equals on two int[26] arrays is 26 comparisons — O(1)
            if (Arrays.equals(patternFrequency, windowFrequency)) {
                resultIndices.add(windowStart + 1); // +1 because new window starts one ahead
            }
        }

        return resultIndices;
    }

    public static void main(String[] args) {
        System.out.println(findAnagramStartIndices("cbaebabacd", "abc")); // [0, 6]
        System.out.println(findAnagramStartIndices("abab", "ab"));        // [0, 1, 2]
        System.out.println(findAnagramStartIndices("af", "be"));          // []
    }
}
Output
[0, 6]
[0, 1, 2]
[]
Pro Tip: int[26] vs HashMap
  • int[26]: O(1) access, O(1) comparison (26 comparisons), no autoboxing.
  • HashMap: O(1) amortized access, O(k) comparison, autoboxing overhead.
  • Use int[26] when input is lowercase English only.
  • Use HashMap when input includes uppercase, digits, or Unicode.
  • Benchmark: int[26] is 3-5x faster than HashMap for anagram detection on lowercase strings.
Production Insight
A plagiarism detection service compared document fingerprints using frequency maps. Each document was reduced to a 26-dimensional vector of character frequencies. Comparing two documents: 26 integer comparisons — O(1). With 10 million document pairs compared per day, the int[26] approach used 260 million integer comparisons. A HashMap approach would have added autoboxing overhead (10 million Integer object allocations per comparison batch) and hash computation costs. The int[26] approach was 4x faster and used 90% less memory.
Key Takeaway
Frequency maps transform strings into numerical fingerprints. int[26] is strictly better than HashMap for lowercase-only inputs: no autoboxing, no collision handling, O(1) comparison via Arrays.equals. Always state this trade-off in interviews.

Two Pointers on Strings — The Palindrome and Reverse Toolkit

Two pointers on a string means placing one pointer at the start and one at the end, then marching them toward each other. It's perfect for problems involving symmetry (palindromes), reversals, or comparisons from both ends simultaneously.

The reason this pattern exists is that many string properties are inherently symmetric. A palindrome reads the same forwards and backwards. The minimum number of deletions to make a string a palindrome depends on mismatches at mirrored positions. Both of these are naturally expressed as comparisons between a left and right pointer moving inward.

Two pointers also pairs beautifully with other patterns. You can use two pointers on the result of a frequency map to reconstruct strings. You can use them inside a sliding window as the window boundaries themselves. Once you're comfortable with each pattern in isolation, start noticing when problems require you to combine two of them — that's where intermediate-level solutions start to look elegant rather than brute-force.

io/thecodeforge/algo/PalindromeChecker.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package io.thecodeforge.algo;

public class PalindromeChecker {

    /**
     * Checks if a string is a valid palindrome, considering only
     * alphanumeric characters and ignoring case.
     *
     * Time:  O(n)
     * Space: O(1) — no extra data structures, just two integer pointers
     */
    public static boolean isValidPalindrome(String sentence) {
        int leftPointer = 0;
        int rightPointer = sentence.length() - 1;

        while (leftPointer < rightPointer) {
            // Skip non-alphanumeric characters from the left
            while (leftPointer < rightPointer
                    && !Character.isLetterOrDigit(sentence.charAt(leftPointer))) {
                leftPointer++;
            }

            // Skip non-alphanumeric characters from the right
            while (leftPointer < rightPointer
                    && !Character.isLetterOrDigit(sentence.charAt(rightPointer))) {
                rightPointer--;
            }

            // Compare characters at both pointers, case-insensitive
            char leftChar  = Character.toLowerCase(sentence.charAt(leftPointer));
            char rightChar = Character.toLowerCase(sentence.charAt(rightPointer));

            if (leftChar != rightChar) {
                return false; // Mismatch — definitely not a palindrome
            }

            // Both characters matched — move both pointers inward
            leftPointer++;
            rightPointer--;
        }

        // All mirrored pairs matched
        return true;
    }

    /**
     * Finds the length of the longest palindromic substring using
     * the "expand around center" technique.
     *
     * Time:  O(n²) — each of the 2n-1 centers expands up to n/2 times
     * Space: O(1)
     */
    public static String longestPalindromicSubstring(String word) {
        if (word == null || word.isEmpty()) return "";

        int bestStart  = 0;
        int bestLength = 1;

        for (int centerIndex = 0; centerIndex < word.length(); centerIndex++) {
            int oddLength = expandFromCenter(word, centerIndex, centerIndex);
            int evenLength = expandFromCenter(word, centerIndex, centerIndex + 1);
            int longerExpansion = Math.max(oddLength, evenLength);

            if (longerExpansion > bestLength) {
                bestLength = longerExpansion;
                bestStart = centerIndex - (longerExpansion - 1) / 2;
            }
        }

        return word.substring(bestStart, bestStart + bestLength);
    }

    private static int expandFromCenter(String word, int left, int right) {
        while (left >= 0 && right < word.length()
                && word.charAt(left) == word.charAt(right)) {
            left--;
            right++;
        }
        return right - left - 1;
    }

    public static void main(String[] args) {
        System.out.println(isValidPalindrome("A man, a plan, a canal: Panama")); // true
        System.out.println(isValidPalindrome("race a car"));                      // false
        System.out.println(isValidPalindrome(" "));                               // true

        System.out.println(longestPalindromicSubstring("babad"));   // "bab" or "aba"
        System.out.println(longestPalindromicSubstring("cbbd"));    // "bb"
        System.out.println(longestPalindromicSubstring("racecar")); // "racecar"
    }
}
Output
true
false
true
bab
bb
racecar
Interview Gold: Expand Around Center vs Dynamic Programming
  • Expand around center: simplest, O(1) space. Best for interviews.
  • DP table: O(n²) space. Useful when you need to answer many palindrome queries on the same string.
  • Manacher's: O(n) time. Complex to implement. Mention it exists, don't implement unless asked.
  • Two-pointer palindrome check: O(n) time, O(1) space. Different from longest palindromic substring.
  • For 'valid palindrome' check: two pointers. For 'longest palindromic substring': expand around center.
Production Insight
A content moderation service checked if user-generated usernames were palindromes to flag potential bot accounts (bots often use palindromic names like 'abba12321ba'). The two-pointer palindrome check was O(n) per username. With 50 million username registrations per day, the check added 0.02ms per registration — negligible. The service flagged 3% of registrations as potential bots, reducing downstream spam processing by 15%.
Key Takeaway
Two pointers on strings handle symmetry: palindrome check (O(n), O(1) space), longest palindromic substring (expand around center, O(n²), O(1) space). Expand-around-center beats DP for the same time complexity because it uses O(1) space instead of O(n²).

String Hashing — Catching Patterns You Can't See by Eye

Hashing a string means converting it into a number so you can compare strings in O(1) instead of O(n). The canonical application is the Rabin-Karp rolling hash algorithm for substring search, but the concept shows up any time you need to detect duplicate substrings, group anagrams, or find repeated patterns at scale.

The key idea is a rolling hash: when your window slides one character to the right, you don't recompute the entire hash from scratch. You mathematically remove the contribution of the outgoing character and add the incoming character. This keeps each slide at O(1), making the full scan O(n) regardless of pattern length.

Grouping anagrams is a softer but very common application. The trick: sort each word's characters to produce a canonical key. All anagrams of 'eat' sort to 'aet'. Store them in a HashMap<String, List<String>> keyed by the sorted form. This is O(n * k log k) where k is the average word length — entirely practical for real word lists and comes up frequently in interview problems involving dictionaries.

io/thecodeforge/algo/AnagramGrouper.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package io.thecodeforge.algo;

import java.util.*;

public class AnagramGrouper {

    /**
     * Groups a list of words so that anagrams appear together.
     * Uses a sorted-character string as a canonical hash key.
     *
     * Time:  O(n * k log k) where n = number of words, k = average word length
     * Space: O(n * k) to store all words in the result map
     */
    public static List<List<String>> groupAnagrams(String[] words) {
        Map<String, List<String>> anagramBuckets = new HashMap<>();

        for (String word : words) {
            char[] wordChars = word.toCharArray();
            Arrays.sort(wordChars);
            String canonicalKey = new String(wordChars);

            anagramBuckets.computeIfAbsent(canonicalKey, k -> new ArrayList<>()).add(word);
        }

        return new ArrayList<>(anagramBuckets.values());
    }

    /**
     * Alternative fingerprint approach using a frequency-count key.
     * Avoids sorting entirely — O(n * k) overall.
     */
    public static List<List<String>> groupAnagramsLinear(String[] words) {
        Map<String, List<String>> anagramBuckets = new HashMap<>();

        for (String word : words) {
            int[] charCounts = new int[26];
            for (char ch : word.toCharArray()) {
                charCounts[ch - 'a']++;
            }

            StringBuilder keyBuilder = new StringBuilder();
            for (int count : charCounts) {
                keyBuilder.append('#').append(count);
            }
            String frequencyKey = keyBuilder.toString();

            anagramBuckets.computeIfAbsent(frequencyKey, k -> new ArrayList<>()).add(word);
        }

        return new ArrayList<>(anagramBuckets.values());
    }

    public static void main(String[] args) {
        String[] wordList = {"eat", "tea", "tan", "ate", "nat", "bat"};

        List<List<String>> grouped = groupAnagrams(wordList);
        System.out.println("Sorted-key approach:");
        for (List<String> group : grouped) {
            System.out.println("  " + group);
        }

        System.out.println("\nFrequency-key approach:");
        List<List<String>> groupedLinear = groupAnagramsLinear(wordList);
        for (List<String> group : groupedLinear) {
            System.out.println("  " + group);
        }
    }
}
Output
Sorted-key approach:
[eat, tea, ate]
[tan, nat]
[bat]
Frequency-key approach:
[eat, tea, ate]
[tan, nat]
[bat]
Pro Tip: The Frequency-Key Trick Beats Sorting for Long Words
  • Sorted key: simple, O(k log k) per word. Best for short words (k < 50).
  • Frequency key: O(k) per word. Best for long words (k > 100).
  • Frequency key uses int[26] → string conversion: "#2#0#1#..." as the hash key.
  • Both produce the same grouping result. The difference is per-word key-building cost.
  • Decision rule: if k is unknown, use sorted key for simplicity. If k can be large, use frequency key.
Production Insight
A dictionary service grouped 500,000 words by anagram families. With average word length 6, the sorted-key approach cost 500,000 x 6 x log(6) = 7.7 million operations. The frequency-key approach cost 500,000 x 6 = 3 million operations. The frequency-key approach was 2.5x faster. For a genome processing pipeline with k=10,000, the difference was 133x — the frequency-key approach was the only viable option.
Key Takeaway
String hashing converts strings to numbers for O(1) comparison. For anagram grouping, sorted key is O(k log k) per word, frequency key is O(k) per word. Use frequency key when k is large. Rabin-Karp rolling hash enables O(1) per slide for substring search.

StringBuilder and String Concatenation — The Hidden O(n^2)

String concatenation with + inside a loop is the most common performance anti-pattern in string-heavy code. In Java, Strings are immutable. Each + creates a new String object, copying all previous characters. For a loop of n iterations building a string of final length L, total work is O(L^2) — not O(L).

StringBuilder solves this by maintaining a mutable char[] buffer. Appends are O(1) amortized (occasional resize is O(L) but amortized over n appends). The final toString() call allocates one String of length L. Total work: O(L).

The Java compiler can optimize simple string + into StringBuilder for cases like String s = a + b + c. But it fails for complex patterns like result += match + ", " inside a loop. Never rely on the compiler — use StringBuilder explicitly for any string assembly inside a loop.

io/thecodeforge/algo/StringBuilderBenchmark.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package io.thecodeforge.algo;

public class StringBuilderBenchmark {

    /** Demonstrates the O(n^2) cost of string concatenation vs O(n) StringBuilder. */
    public static void main(String[] args) {
        int n = 100_000;

        // BAD: O(n^2) — each + copies all previous characters
        long start = System.nanoTime();
        String bad = "";
        for (int i = 0; i < n; i++) {
            bad += "a"; // new String object each iteration
        }
        long badTime = System.nanoTime() - start;

        // GOOD: O(n) — StringBuilder appends in-place
        start = System.nanoTime();
        StringBuilder sb = new StringBuilder(n); // pre-sized
        for (int i = 0; i < n; i++) {
            sb.append('a');
        }
        String good = sb.toString();
        long goodTime = System.nanoTime() - start;

        System.out.println("String +000)
StringBuilder:   1 ms (length=100000)
Speedup: 84 loop:  " + badTime / 1_000_000 + " ms (length=" + bad.length() + ")");
        System.out.println("StringBuilder:   " + goodTime / 1_000_000 + " ms (length=" + good.length() + ")");
        System.out.println("Speedup: " + (badTime / Math.max(goodTime, 1)) + "x");
    }
}
Output
String + loop: 8423 ms (length=10023x
Watch Out: String + in a Loop Is O(n^2)
  • String + in loop: O(n^2) total work. Each iteration copies all previous characters.
  • StringBuilder: O(n) total work. Appends are O(1) amortized.
  • Pre-size StringBuilder: new StringBuilder(estimatedLength) avoids resize overhead.
  • The compiler optimizes simple + into StringBuilder. But it fails for complex loop patterns.
  • Rule: if you see += on a String inside a for/while loop, replace with StringBuilder.
Production Insight
A report generation service built CSV output by appending rows with String += in a loop. A report with 500,000 rows took 45 seconds to generate. The heap dump showed 500,000 temporary String objects, each up to 25MB. GC paused for 3 seconds between each generation. Replacing with StringBuilder (pre-sized to 500,000 x 50 = 25MB): report generation dropped to 0.8 seconds. The 56x speedup and elimination of GC pressure made the service viable for real-time report generation.
Key Takeaway
String + in a loop is O(n^2). StringBuilder is O(n). Pre-size with new StringBuilder(estimatedLength). The compiler cannot optimize complex loop patterns — always use StringBuilder explicitly. This is the single most impactful one-line fix in string-heavy code.
● Production incidentPOST-MORTEMseverity: high

Autocomplete Service OOM: String Concatenation in Loop Created 2GB of Temporary Objects Per Minute

Symptom
Autocomplete service crashed with OutOfMemoryError every 3 hours. Heap dump showed 2GB of char[] objects — all temporary String allocations from concatenation. GC pause times exceeded 2 seconds during the crash window. P99 latency spiked from 15ms to 8 seconds before the crash.
Assumption
A memory leak in the trie data structure was causing unbounded growth. The team spent 6 hours profiling the trie, checking for unclosed streams, and reviewing cache eviction policies.
Root cause
The suggestion builder used String result = ""; for (String match : matches) { result += match + ", "; } to build comma-separated suggestion strings. Each += creates a new String object, copying all previous characters. For a list of 1,000 matches with average length 10, this is 1,000 + 999 + 998 + ... + 1 = 500,000 character copies. With 50 requests per second, that is 25 million temporary char[] allocations per second. The GC could not keep up. StringBuilder allocates a single buffer and appends in-place — O(n) total instead of O(n^2).
Fix
1. Replaced String += with StringBuilder. Single buffer allocation, O(n) append. 2. Added a lint rule: StringConcatenationInLoop — flag any + or += on String inside a for/while loop. 3. Added a metric: string_concat_temporary_objects_total to track temporary String allocations. 4. Pre-sized the StringBuilder: new StringBuilder(matches.size() * 15) to avoid resize overhead. 5. Added a unit test that builds a 10,000-element string and asserts no GC pressure.
Key lesson
  • String + in a loop is O(n^2). StringBuilder is O(n). This is the single most impactful one-line fix in string-heavy code.
  • Always pre-size StringBuilder when you know the approximate final length. Default capacity is 16, causing log(n) resize operations.
  • Add lint rules to catch string concatenation in loops. This bug recurs every time a new developer joins the team.
  • Profile GC pressure, not just latency. The OOM was a symptom of 2GB/min temporary allocations, not a memory leak.
  • For Java, the compiler optimizes string + into StringBuilder for simple cases. But it fails for complex expressions like result += match + ", ". Never rely on the compiler — use StringBuilder explicitly.
Production debug guideSymptom-first investigation path for string pattern bugs.6 entries
Symptom · 01
Sliding window returns wrong answer on strings with distant duplicate characters (e.g., 'abba' returns 3 instead of 2).
Fix
Check if the left pointer update uses Math.max. Without it, a duplicate found before the current window drags the pointer backward. Add: windowStart = Math.max(windowStart, lastSeenAt.get(ch)).
Symptom · 02
String processing is O(n^2) despite using a linear algorithm. TLE on large inputs.
Fix
Check for string concatenation with + inside a loop. Each + creates a new String object. Replace with StringBuilder.
Symptom · 03
ArrayIndexOutOfBoundsException on frequency map with uppercase or Unicode characters.
Fix
The code uses int[26] but the input contains uppercase, digits, or non-ASCII characters. Use Character.toLowerCase() before indexing, or switch to HashMap<Character, Integer>.
Symptom · 04
Anagram check returns wrong result for strings with different lengths.
Fix
Add a length check before building frequency maps. If s1.length() != s2.length(), they cannot be anagrams. Return false immediately.
Symptom · 05
Palindrome check fails on strings with spaces and punctuation.
Fix
The code compares characters without skipping non-alphanumeric characters. Add inner while loops to skip non-alphanumeric from both ends.
Symptom · 06
Group anagrams produces wrong groups or misses anagrams.
Fix
Check if the canonical key is computed correctly. For sorted-key approach: sort the characters. For frequency-key approach: include all 26 counts in the key string, not just non-zero counts.
★ String Pattern TriageRapid checks to isolate string manipulation bugs.
Sliding window gives wrong answer on 'abba' or 'tmmzuxt'.
Immediate action
Check Math.max guard on left pointer.
Commands
Add trace: System.out.println("before max: " + windowStart + " lastSeen: " + lastSeenAt.get(ch))
Verify windowStart = Math.max(windowStart, lastSeenAt.get(ch) + 1)
Fix now
Add Math.max to prevent left pointer from jumping backward.
String processing is 100x slower than expected on 10K-char input.+
Immediate action
Check for + concatenation inside loops.
Commands
Profile with -XX:+PrintGCDetails — look for excessive GC from char[] allocations
Search code for: result += or result = result +
Fix now
Replace all += on String with StringBuilder.append().
Frequency map throws ArrayIndexOutOfBoundsException.+
Immediate action
Check character range assumptions.
Commands
Print the character that crashes: System.out.println("char=" + ch + " code=" + (int)ch)
If code > 127 or char is uppercase, the int[26] assumption is wrong
Fix now
Use Character.toLowerCase(ch) - 'a' or switch to HashMap<Character, Integer>.
Anagram detection is O(n log n) and TLE on large inputs.+
Immediate action
Check if the code sorts characters to build the key.
Commands
Count key-building operations: is it O(k) or O(k log k) per word?
If sorting: switch to frequency-count key (int[26] → string key)
Fix now
Replace sorted-key with frequency-key approach. O(k) per word instead of O(k log k).
String Manipulation Patterns Compared
PatternBest ForTime ComplexitySpace ComplexityKey Signal in Problem
Sliding Window (variable)Longest/shortest substring with constraintO(n)O(alphabet size)Problem says 'longest/shortest/minimum window'
Sliding Window (fixed)Anagram detection, fixed-length pattern matchO(n)O(1) with int[26]Problem gives a fixed pattern length
Two Pointers (inward)Palindrome check, symmetry comparisonO(n)O(1)Problem involves mirroring or reading both ends
Frequency MapCharacter count comparison, anagram groupingO(n)O(alphabet size)Problem asks 'same characters?' or 'rearrangement?'
String Hashing / Sorted KeyGrouping anagrams, duplicate substring detectionO(n * k log k)O(n * k)Problem asks to group or deduplicate by content
StringBuilderBuilding output strings from partsO(n)O(n)Problem requires assembling a string from components
Expand Around CenterLongest palindromic substringO(n^2)O(1)Problem asks for longest palindrome (not just check)
Rabin-Karp Rolling HashSubstring search, duplicate detection at scaleO(n + m) avgO(1)Problem requires finding pattern occurrences in large text

Key takeaways

1
Pattern recognition before coding
identify whether the problem needs a fixed or variable window, symmetric comparison, or frequency fingerprint before touching the keyboard — the right pattern choice eliminates 80% of implementation bugs.
2
The sliding window left pointer must only ever move forward
the missing Math.max guard is the single most common sliding window bug in interview settings and production code alike.
3
int[26] is strictly better than HashMap for lowercase-only string problems
no autoboxing, no collision handling, constant-time comparison via Arrays.equals — always state this trade-off in an interview.
4
Sorting characters is the easiest anagram key but costs O(k log k) per word
the frequency-count key approach cuts it to O(k) and matters significantly when processing long strings at scale.
5
String + in a loop is O(n^2). StringBuilder is O(n). This is the single most impactful one-line fix in string-heavy code. Pre-size with new StringBuilder(estimatedLength).
6
Expand-around-center beats DP for longest palindromic substring
same O(n^2) time but O(1) space instead of O(n^2) space.
7
Always test sliding window with 'abba' and 'tmmzuxt' to catch the Math.max bug. Always test anagram with different-length strings.
8
The five patterns
sliding window, two pointers, frequency map, string hashing, StringBuilder — cover 95% of string manipulation problems in interviews and production.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

FAQ · 8 QUESTIONS

Frequently Asked Questions

01
What is the sliding window technique in string problems?
02
How do I check if two strings are anagrams in Java?
03
When should I use two pointers vs sliding window for string problems?
04
What is the fastest anagram check?
05
How do you check if two strings are anagrams in O(n) time?
06
Why is String concatenation with + slow in loops?
07
How does the Math.max guard work in sliding window?
08
What is the difference between expand-around-center and DP for longest palindromic substring?
🔥

That's Arrays & Strings. Mark it forged?

5 min read · try the examples if you haven't

Previous
Dutch National Flag Algorithm
7 / 13 · Arrays & Strings
Next
Anagram and Palindrome Problems