DSA Handbook

#Strings

Strings are not one pattern. They are five, wearing the same costume: counting, two pointers, expansion, parsing, and matching. Naming which one you are in is most of the work.


#1 · The five sub-patterns

Sub-patternCueToolCanonical
Counting"anagram", "permutation", "frequency"Hash map or int[26]LC 242, 49, 438
Two pointers"palindrome", "reverse", "compare from both ends"Two indicesLC 125, 344, 680
Expansion"longest palindromic…"Expand from each centreLC 5, 647
Parsing"evaluate", "decode", "valid"Stack or an index cursorLC 20, 394, 227
Matching"find the pattern", "repeated substring"Rolling hash / KMPLC 28, 459, 214

Plus two that live on their own pages: substring-with-constraint problems are sliding window, and prefix problems are tries.

Classify before you code. "Longest substring without repeating characters" is sliding window; "longest palindromic substring" is expansion. They sound alike and share nothing.


Interactive simulation — needs JavaScript.


#2 · The performance trap

The single most common string bug in interviews, and it is invisible until someone asks about complexity.

Strings are IMMUTABLE in both Java and Python.

  s += char   inside a loop
  -> allocates a NEW string and copies everything, every iteration
  -> 1 + 2 + 3 + ... + n  =  O(n^2)

Building a 100,000-character string this way does ~5 billion copies.
LanguageWrongRight
Pythons += ch in a loopparts.append(ch) … "".join(parts)
Javas += ch in a loopStringBuilder.append(ch) … .toString()
# O(n^2) -- looks innocent
result = ""
for ch in text:
    result += ch

# O(n)
parts = []
for ch in text:
    parts.append(ch)
result = "".join(parts)

Say this out loud when you write it. "I'm using a list and joining because string concatenation in a loop is quadratic" is a free complexity point, and the interviewer was going to ask.

Other costs worth knowing:

OperationPythonJava
LengthO(1)O(1)
IndexO(1)O(1)
Slice / substringO(k) — copiesO(k) since Java 7u6 — copies
ConcatenateO(n+m)O(n+m)
in / containsO(n·m) worstO(n·m) worst
CompareO(min)O(min)

Slicing is not free, and it is the second-most-common hidden quadratic: s[i:] inside a loop copies the tail every iteration. Pass indices instead of slices.


#3 · Counting

int[26] beats a hash map when the alphabet is fixed — no hashing, no boxing, and comparing two arrays is a single loop.

def is_anagram(s, t):
    if len(s) != len(t):
        return False                      # cheap early exit
    counts = [0] * 26
    for a, b in zip(s, t):
        counts[ord(a) - ord("a")] += 1
        counts[ord(b) - ord("a")] -= 1    # one pass, not two
    return all(c == 0 for c in counts)
public boolean isAnagram(String s, String t) {
    if (s.length() != t.length()) return false;
    int[] counts = new int[26];
    for (int i = 0; i < s.length(); i++) {
        counts[s.charAt(i) - 'a']++;
        counts[t.charAt(i) - 'a']--;
    }
    for (int c : counts) if (c != 0) return false;
    return true;
}

Incrementing and decrementing in one pass is neater than building two maps and comparing, and it is the version to write.

For grouping anagrams (LC 49), the key is the question:

KeyCostNote
Sorted stringO(k log k) per wordSimple, usually fine
Count tuple (0,1,0,…)O(k) per wordFaster; the better answer

#4 · Palindromes — expand from centre

The pattern for "longest palindromic substring" (LC 5) and "count palindromic substrings" (LC 647).

Every palindrome has a centre. Try all centres, expand outward.

  2n - 1 centres:  n single characters (odd length)
                   n-1 gaps between characters (even length)

Both cases are needed. Handling only odd centres silently misses "abba".
def longest_palindrome(s):
    if not s:
        return ""
    start, length = 0, 1

    def expand(left, right):
        # Grow while the characters match and we are in bounds.
        while left >= 0 and right < len(s) and s[left] == s[right]:
            left -= 1
            right += 1
        # Loop exits one step PAST the palindrome, so the actual span
        # is (left+1 .. right-1), of length right - left - 1.
        return left + 1, right - left - 1

    for i in range(len(s)):
        for l, r in ((i, i), (i, i + 1)):        # odd centre, then even
            lo, ln = expand(l, r)
            if ln > length:
                start, length = lo, ln

    return s[start:start + length]

O(n²) time, O(1) space. The DP solution is also O(n²) time but O(n²) space, so expansion is strictly better — worth saying when you choose it.

right - left - 1 is the off-by-one that bites. The loop exits one step past both ends, so the span shrinks by two, not one — draw it once and it stays learned.

Manacher's algorithm gets this to O(n). It is almost never required; knowing it exists and that expansion is the expected answer is the right level of knowledge.


#5 · Parsing

Nested structure means a stack. The shape is always the same:

For every character:
    opening delimiter  -> PUSH the current context, start a fresh one
    closing delimiter  -> POP, combine the finished piece into the parent
    otherwise          -> accumulate into the current context

LC 394 (Decode String), 3[a2[c]] → accaccacc:

def decode_string(s):
    stack = []                  # (previous_string, repeat_count)
    current = ""
    number = 0

    for ch in s:
        if ch.isdigit():
            number = number * 10 + int(ch)     # multi-digit: "12[a]"
        elif ch == "[":
            stack.append((current, number))    # save the OUTER context
            current, number = "", 0            # start fresh inside
        elif ch == "]":
            previous, count = stack.pop()
            current = previous + current * count   # fold inward result out
        else:
            current += ch
    return current

Two details: number * 10 + digit handles multi-digit counts (12[a]), and pushing (current, number) rather than just the number is what lets the inner result be folded back into its parent correctly.


#6 · Pattern matching

#Rolling hash (Rabin-Karp)

The one to reach for, because it is short and it generalises to "find any of these k patterns" and to LC 1044 (Longest Duplicate Substring).

Treat the window as a base-B number mod a large prime.

  hash("abc") = a·B^2 + b·B + c   (mod P)

Slide by one:
  remove the leading char, shift, add the trailing char -- all O(1)

  hash = (hash - s[i] * B^(k-1)) * B + s[i+k]   (mod P)

Collisions are possible, so VERIFY a hash match with a real comparison.
Expected O(n + m); worst case O(n·m) if an adversary forces collisions.

Always say "and I'd verify the match". A rolling hash that trusts the hash is wrong, and mentioning the verification step is what shows you understand it rather than recite it.

#KMP — sketch, do not memorise

Build a "failure" array: lps[i] = length of the longest proper prefix
of pattern[0..i] that is also a suffix of it.

On a mismatch at pattern position j, instead of restarting, jump to
lps[j-1] -- because those characters are already known to match.

The text pointer NEVER moves backwards.  O(n + m).

In an interview, s.indexOf(pattern) or pattern in s is usually the correct answer, with "I could implement KMP for O(n+m) guaranteed if you want the manual version." Being asked to write KMP from scratch is rare; being asked what it does is common.

The LPS array has a second use worth knowing: LC 459 (Repeated Substring Pattern) and LC 214 (Shortest Palindrome) both fall straight out of it.


#7 · The ladder

#Foundational

#ProblemSourceThe point
1Valid AnagramLC 242 · NeetCodeint[26], one pass
2Valid PalindromeLC 125 · NeetCodeTwo pointers + filtering
3Reverse StringLC 344In place, two pointers
4Group AnagramsLC 49 · NeetCodeCount tuple as the key
5Valid ParenthesesLC 20 · NeetCodeThe stack template
6Longest Common PrefixLC 14Vertical scan

#Medium

#ProblemSourceThe point
7Longest Palindromic SubstringLC 5 · NeetCodeExpand from centre, both parities
8Palindromic SubstringsLC 647 · NeetCodeSame expansion, counting
9Decode StringLC 394Stack of contexts
10String to Integer (atoi)LC 8Edge cases are the problem
11Valid Palindrome IILC 680One deletion allowed — branch once
12Find All Anagrams in a StringLC 438 · NeetCodeSliding window + counts
13Longest Repeating Character ReplacementLC 424 · NeetCodeSliding window on counts
14Basic Calculator IILC 227Parsing with precedence
15Encode and Decode StringsLC 271 · NeetCodeLength-prefixed framing

#Hard

#ProblemSourceThe point
16Minimum Window SubstringLC 76 · NeetCodeThe hardest sliding window
17Implement strStr()LC 28KMP or rolling hash
18Shortest PalindromeLC 214KMP's LPS array
19Longest Duplicate SubstringLC 1044Binary search + rolling hash
20Regular Expression MatchingLC 102D DP, not strings

If you only do six: 242, 125, 49, 5, 20, 76.


#8 · Worked example — LC 271, Encode and Decode Strings

Problem: serialise a list of strings into one string and back. Strings may contain any characters.

Why the obvious answer fails, and this is the entire question:

Join with a delimiter:   "a,b,c"
But what if a string CONTAINS the delimiter?  ["a,b", "c"] -> "a,b,c"
Decoding gives ["a","b","c"]. Wrong, and there is no safe delimiter
because any character can appear in the data.

LENGTH PREFIXING solves it:

  "4#abcd3#xyz"
   ^ ^         length, sentinel, then exactly that many characters

Read digits until '#', then take exactly that many characters. The
content is never scanned for delimiters, so it cannot be misread.
def encode(strs):
    # Length, then a sentinel, then the raw content.
    return "".join(f"{len(s)}#{s}" for s in strs)

def decode(s):
    out = []
    i = 0
    while i < len(s):
        j = s.index("#", i)          # the '#' terminating the length
        length = int(s[i:j])            # the digits before it
        start = j + 1
        out.append(s[start:start + length])   # take EXACTLY length chars
        i = start + length
    return out

This is the framing problem every network protocol solves the same way — length-prefixed rather than delimiter-separated — and saying that connects it to real systems work.


#9 · Failure modes

BugSymptomFix
s += ch in a loopO(n²), TLE on large inputsjoin / StringBuilder
Slicing inside a loopHidden O(n²)Pass indices
Only odd palindrome centresMisses "abba"Try (i,i) and (i,i+1)
right - left instead of right - left - 1Length off by twoThe loop exits past both ends
Assuming lowercase-onlyCrash on uppercase or digitsCheck the constraints; use a map
Case/whitespace in palindromesWrong answer on "A man, a plan…"Normalise or filter while scanning
Trusting a rolling-hash matchRare wrong answersVerify with a real comparison
Java == on stringsCompares references.equals()
Not handling the empty stringCrashGuard early

Java == on strings deserves emphasis — it sometimes works, because of the string pool, which makes it worse: the bug survives your tests and fails on runtime-constructed strings.


#10 · Interview questions

QuestionWhat to say
⭐ "What's the complexity of building a string in a loop?"O(n²) — strings are immutable, so each concatenation copies everything. I'd collect into a list and join, or use a StringBuilder, for O(n).
⭐ "Longest palindromic substring."Expand from every centre, trying both odd and even parities — 2n−1 centres, O(n²) time and O(1) space. The DP version is the same time but O(n²) space, so expansion is strictly better. Manacher's is O(n) but almost never expected.
"Anagram check?"int[26] incremented from one string and decremented from the other in a single pass, then check all zeros. Fixed alphabet means no hashing and no boxing. For grouping, the count tuple is a better key than the sorted string — O(k) instead of O(k log k).
⭐ "Find a pattern in a text."The library method is the honest answer for an interview. If asked to implement it: rolling hash, which is short and generalises to multiple patterns — verifying every hash match with a real comparison, since collisions are possible. KMP gives guaranteed O(n+m) using the LPS array.
⭐ "Serialise a list of strings."Length-prefix each one — 4#abcd — rather than delimit them, because any delimiter can appear in the data. The decoder reads the length and then takes exactly that many characters, so content is never parsed for delimiters. It is how network protocols frame messages.
"Sliding window or expansion?"Sliding window for substring-with-a-constraint — no repeats, at most k distinct. Expansion for palindromes, because a palindrome is defined by its centre rather than by a window property. They sound similar and share nothing.
"Is slicing free?"No — it copies, O(k). Slicing inside a loop is a common hidden quadratic; pass indices instead.

#Stop condition

You know this pattern when you can:

  1. name the five sub-patterns and classify a problem into one,
  2. explain why loop concatenation is O(n²) and give both fixes,
  3. write centre expansion with both parities and the right-left-1 span,
  4. write the stack parsing template,
  5. sketch a rolling hash and say you would verify the match, and
  6. give the length-prefix argument for LC 271.