INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY
01
00/15
01 / COVER STEP 05 · STRINGS
INVARIANT · STEP 05 · PROBLEMS + DRILLS
FIFTEEN STRING PROBLEMS

No new data structure — just the string and an index. These fifteen problems are the whole working vocabulary: single-pass parsing, two-pointer palindromes, frequency tables, word & prefix surgery, and structural checks. This deck is problems and drills only — every slide is the signal that gives a problem away, the idea, the C++ and Python, and the trap, then drills that make you recall the pattern, not just recognise it.

15Problems
5Patterns
14Drills
3Languages
← → ↑ ↓  or  W A S D  navigate SPACE next   DOUBLE-CLICK to advance I index   G goto problem   P predict H hide solutions   T close video   F fullscreen
INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
02 / START HERE READ THIS ONE FIRST

HOW THIS DECK WORKS

This is not a list of problems. It is 5 learning units, and every one runs the same four beats. Go in order and the problems stop feeling random.

01 INTROWhat the concept is, and what to watch for
02 VIDEOThe lecture, full-width in theatre mode
03 DRILLS2–4 questions checking the lecture landed
04 PROBLEMSThe sheet problems that concept unlocks

Moving around

← ↑Back a slide — or A / W → ↓Forward — or D / S / Space 2×clickDouble-click the right side to advance, left to go back. A single click never moves the deck. IThe index: every problem, clickable, with your progress GJump straight to a problem by its number FFullscreen

While you study

HHide solutions — blurs code and steps so you try first PPredict mode: call the next step before the animation plays it TClose the video — Esc works too ☐ ★Mark solved, or star to revisit. Both are saved automatically.

15 PROBLEMS · EVERY ONE LINKS TO LEETCODE TO SOLVE

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
03 / INDEX PRESS I FROM ANYWHERE

INDEX — ALL 15 PROBLEMS

Click any problem to jump straight to it. Solved ones turn green. Press I from anywhere to come back here. The number is the sheet's row, not your position — the deck teaches in the order that builds, so you will meet them slightly out of numerical sequence.

SINGLE-PASS PARSING · 04
TWO POINTERS & PALINDROMES · 02
FREQUENCY TABLES · 03
WORDS, PREFIXES & SUFFIXES · 04
MAPPING & ROTATION · 02
SOLVED HAS A LEETCODE LINK
INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
04 / SIGNALS WHEN YOU SEE X, REACH FOR Y

WHICH PATTERN, AND WHY

String problems disguise themselves badly. The phrasing names the pattern — whether order matters, whether you are walking characters or words, whether the question is about symmetry or about counts.

“PARSE / VALIDATE / CONVERT” A FORMAT

Roman numerals, atoi, nesting depth, stripping outer brackets.

One left-to-right pass with a running stateO(n) · O(1)
“IS IT A PALINDROME?” / “LONGEST PALINDROME”

Symmetry about a centre — verify one, or grow the best one.

Two pointers: inward to verify, outward to growO(n) · O(n²)
“ANAGRAM” / “SAME CHARACTERS” / ORDER IRRELEVANT

The question survives shuffling the letters, so only counts matter.

Frequency array of 26 — count, don't sortO(n) · O(1)
“REVERSE THE WORDS” / “COMMON PREFIX”

The unit of work is a word or a prefix, not a character.

Tokenise, or compare column by columnO(total length)
“IS b A ROTATION OF a?”

Circular sameness — every rotation is a window of a doubled string.

Length check, then substring of s + sO(n) with a good search
“CONSISTENT RENAMING” / ISOMORPHIC

A one-to-one letter mapping must hold in both directions.

Two maps, or a map plus a used-setO(n) · O(1)
INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
05 / CONSTRAINTS BEFORE YOU WRITE A LINE

READ THE ANSWER OFF THE CONSTRAINTS

Almost everything here is a single O(n) pass. The bound rarely rules an approach out — what does is substr(), which copies: take one per iteration and a linear scan quietly becomes quadratic.

n ≤
BUDGET
WHAT THAT BUYS YOU
n ≤ 20
O(2ⁿ)
enumerate every subsequence — not what any problem here needs
n ≤ 1000
O(n²)
every (start, end) pair — what Longest Palindromic Substring actually costs
n ≤ 10⁵
O(n)
ONE pass with a small running state. THE home row for this deck
n ≤ 10⁶
O(n)
still one pass — but watch substr(), which copies and turns O(n) into O(n²)
alphabet = 26
O(1) space
a fixed 26-slot count beats a hash map on every constant that matters

BOUNDED ALPHABET ⇒ 26-SLOT COUNT, NOT A HASH MAP

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
06 / ROADMAP WHAT YOU ARE ABOUT TO DO

THE WHOLE RUN, 5 UNITS

Five families, fifteen problems, and no lectures — this topic has no harvested series, so each family opens with its own pair of priming drills and closes with a mechanism you can step through. Go in order: the single-pass family sets up everything after it.

UNIT 01

Single-Pass Parsing

NO LECTURE2 DRILLS4 PROBLEMS
UNIT 02

Two Pointers & Palindromes

NO LECTURE2 DRILLS2 PROBLEMS
UNIT 03

Frequency Tables

NO LECTURE2 DRILLS3 PROBLEMS
UNIT 04

Words, Prefixes & Suffixes

NO LECTURE2 DRILLS4 PROBLEMS
UNIT 05

Mapping & Rotation

NO LECTURE2 DRILLS2 PROBLEMS
INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
07 / WARMUP LOAD THE STRING TOOLKIT

TWO THINGS WORTH HAVING IN MEMORY

DRILL 01 · RECALL

A C++ std::string of length n — what are the costs of s[i], s.substr(i, k), and s += c (amortised)?

Indexing is O(1), but substr copies. s.substr(i, k) allocates and copies k characters — build a loop that takes a substring each iteration and an O(n) scan quietly becomes O(n²). Appending with += is amortised O(1) because the buffer grows geometrically. In Python the trap is sharper: strings are immutable, so s += c in a loop is O(n²) — build a list and ''.join() at the end. Knowing which string op secretly copies is half of writing fast string code.

DRILL 02 · RECALL

You need to count letters of a lowercase-only string. What is the right container, and why not a hash map?

A 26-slot array indexed by c - 'a'. For a bounded alphabet it is strictly better than a hash map: O(1) with tiny constants, no hashing, and — crucially — two frequency tables can be compared with a single == over 26 ints. Reserve the hash map for genuinely unbounded keys (Unicode, words, arbitrary tokens). Recognising the alphabet is bounded is the cue that turns a map into an array and an O(n log n) sort into an O(n) count.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
08 / PATTERN SINGLE-PASS PARSING

PRIME IT, THEN APPLY IT

DRILL 01 · RECALL

What do all four parsing problems (remove-outermost, max-depth, Roman, atoi) have in common structurally?

A single pass with a running state. None of them needs a data structure heavier than a couple of integers: a depth counter for parentheses, an accumulator with a look-ahead rule for Roman, a sign and a clamped value for atoi. The skill is picking what to carry. Once you see the family, you stop looking for a trick and start writing the loop.

DRILL 02 · TRACE

Removing the outermost parentheses of each primitive from "(()())(())" using a depth counter, what comes out?

"()()()". Walk with a depth d. On '(': if d > 0 it is an inner paren, keep it, then d++. On ')': d-- first, then if d > 0 keep it. The outermost pair of each primitive block sits exactly where d touches 0, so the test drops precisely those. No stack, one integer.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
09 / MECHANISM SINGLE-PASS PARSING · DEPTH · CODE MIRRORED

ONE COUNTER IS THE WHOLE ALGORITHM

Nesting depth looks like it wants a stack, and it does not. '(' is +1 and ')' is −1, so a single running counter already holds how deep you are — and the peak it reaches is the answer. Nothing is stored, so the space is O(1) rather than O(n). Watch the counter climb to 3 and come back to 0: ending at exactly zero is also what balanced means, which is the same one-pass scan answering a second question for free. Reaching for a stack here is the reflex this slide exists to break.

ONE COUNTER, NO STACK
STATE
CODE MIRROR
INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
10 / PROBLEM #01 · PARSE-SCAN · EASY

Remove Outermost Parentheses

EASY parse-scan ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

Parentheses (or brackets) and a notion of nesting depth. The moment a problem is about balanced symbols and 'outermost' or 'primitive' pieces, a running depth counter — not a stack — is usually enough.

INTUITION

A primitive block's outermost pair is exactly where the depth touches 0. Carry a depth d: on '(' keep it only if d > 0 (it is inner), then increment; on ')' decrement first, then keep it only if d > 0. The outermost symbols are dropped automatically.

STEPS
  1. d = 0, result empty
  2. For '(': if d > 0 append it; then d++
  3. For ')': d--; then if d > 0 append it
  4. The outermost pair of each primitive sits where d crosses 0, so it is skipped
  5. Return the built string
BRUTEO(n²)
OPTIMALO(n)
↕ SCROLL
// The outermost pair of each primitive is where depth hits 0.
// Carry a single depth counter; a stack is overkill here.
string removeOuterParentheses(string s) {
    string out;
    int d = 0;
    for (char c : s) {
        if (c == '(') { if (d > 0) out += c; d++; }
        else          { d--; if (d > 0) out += c; }
    }
    return out;
}
TIMEO(n)one pass over the string
SPACEO(n)the output buffer
TRAP

Getting the increment/decrement order wrong. For '(' you test d > 0 before incrementing; for ')' you decrement before testing. Swap either order and you keep the outermost paren or drop an inner one. Also resist reaching for a stack — the depth integer is the whole solution.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
11 / PROBLEM #02 · PARSE-SCAN · EASY

Maximum Nesting Depth of the Parentheses

EASY parse-scan ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

“Maximum nesting depth” of balanced parentheses, with other characters mixed in. The word depth is the tell: a counter that rises on '(' and falls on ')', tracking its peak.

INTUITION

Ignore everything that is not a parenthesis. Keep a running depth; the answer is the largest value it ever reaches. Because the input is guaranteed valid (VPS), the counter never goes negative and you never need to validate.

STEPS
  1. depth = 0, best = 0
  2. For each char: if '(' then depth++ and best = max(best, depth)
  3. If ')' then depth--
  4. Ignore digits, operators and everything else
  5. Return best
↕ SCROLL
// Depth rises on '(' and falls on ')'; the answer is its peak.
int maxDepth(string s) {
    int depth = 0, best = 0;
    for (char c : s) {
        if (c == '(') best = max(best, ++depth);
        else if (c == ')') depth--;
    }
    return best;
}
TIMEO(n)one pass
SPACEO(1)two integers
TRAP

Over-engineering a valid input. The string is a guaranteed valid parentheses string, so there is no need to check balance, use a stack, or handle negatives — a single counter and its running max is the entire problem. Reaching for a stack here is the mistake the previous problem warned about, again.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
12 / PROBLEM #03 · PARSE-SCAN · EASY

Roman to Integer

EASY parse-scan ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

A Roman numeral — or any place-value system where a smaller symbol before a larger one means subtraction. The signal is a fixed symbol→value table plus a look-ahead comparison.

INTUITION

Walk left to right. Normally you add each symbol's value; the one exception is when a symbol is smaller than the one immediately after it (IV, IX, XL…), where it is subtracted instead. One comparison with the next character decides the sign.

STEPS
  1. Build value map: I=1, V=5, X=10, L=50, C=100, D=500, M=1000
  2. total = 0; walk index i from left to right
  3. If i+1 is in range and value[s[i]] < value[s[i+1]], subtract value[s[i]]
  4. Otherwise add value[s[i]]
  5. Return total
BRUTEO(n)
OPTIMALO(n)
↕ SCROLL
// Add each value, except subtract when a symbol precedes a larger one.
int romanToInt(string s) {
    unordered_map<char,int> v = {{'I',1},{'V',5},{'X',10},{'L',50},
                                 {'C',100},{'D',500},{'M',1000}};
    int total = 0, n = s.size();
    for (int i = 0; i < n; i++) {
        if (i + 1 < n && v[s[i]] < v[s[i+1]]) total -= v[s[i]];
        else                                  total += v[s[i]];
    }
    return total;
}
TIMEO(n)one pass with a constant-size table
SPACEO(1)a 7-entry map
TRAP

Special-casing the six subtractive pairs by hand. Hard-coding IV, IX, XL, XC, CD, CM is error-prone; the single rule “subtract when this value is less than the next” covers all of them. The other slip is reading s[i+1] without the i+1 < n bound at the last character.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
13 / PROBLEM #04 · PARSE-SCAN · MED

String to Integer (atoi)

MED parse-scan ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

“Parse an integer from messy text” — leading spaces, an optional sign, digits, then junk, with overflow to clamp. atoi is the canonical state-machine parse; the difficulty is entirely in the order and the clamp.

INTUITION

Four phases in strict order: skip leading spaces, read at most one sign, read digits until a non-digit, and stop. Build the value digit by digit but clamp to the 32-bit range before it can overflow, not after.

STEPS
  1. i = 0; skip spaces while s[i] == ' '
  2. sign = +1; if s[i] is '+' or '-', set sign and i++
  3. While s[i] is a digit: check clamp, then x = x*10 + (s[i]-'0')
  4. Clamp: if x would exceed INT_MAX/INT_MIN, return the bound
  5. Stop at the first non-digit; return sign * x
BRUTEO(n)
OPTIMALO(n)
↕ SCROLL
// Skip spaces -> one sign -> digits, clamping BEFORE each multiply.
int myAtoi(string s) {
    int i = 0, n = s.size(), sign = 1;
    long x = 0;
    while (i < n && s[i] == ' ') i++;
    if (i < n && (s[i] == '+' || s[i] == '-'))
        sign = (s[i++] == '-') ? -1 : 1;
    while (i < n && isdigit(s[i])) {
        x = x * 10 + (s[i++] - '0');
        if (sign * x > INT_MAX) return INT_MAX;   // clamp before overflow
        if (sign * x < INT_MIN) return INT_MIN;
    }
    return (int)(sign * x);
}
TIMEO(n)one pass over the prefix
SPACEO(1)a few scalars
TRAP

Checking overflow after the multiply. In C++ int, x*10 + d can already have wrapped by the time you test it — carry the running value in a long (or clamp against the bound before multiplying). The second trap is phase order: a sign after a digit, or a space after a sign, must end the parse — spaces, then one sign, then digits, nothing revisited.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
14 / PATTERN TWO POINTERS & PALINDROMES

PRIME IT, THEN APPLY IT

DRILL 01 · RECALL

Valid Palindrome moves two pointers inward; Longest Palindromic Substring expands two pointers outward. What is the shared invariant?

A pair of indices bracketing a symmetric candidate. Checking a palindrome walks l up and r down comparing s[l] and s[r]; growing one starts l = r (or l, r adjacent) and pushes them apart while the ends match. Same two pointers, opposite directions — verify versus discover. Seeing them as one idea is why the second problem is not scary once the first is automatic.

DRILL 02 · BUG

This longest-palindrome expands around every index but fails on "cbbd" (returns "b" instead of "bb"). What is missing?

for (int i = 0; i < n; i++) {
    // odd centre only
    expand(i, i);
    // no even-centre call
}

It forgets the even centres. A palindrome can be centred on a character (odd length) or on the gap between two equal characters (even length, like the bb in cbbd). So you must call both expand(i, i) and expand(i, i+1) at each index — 2n − 1 centres in all. Missing the even case is the single most common longest-palindrome bug.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
15 / MECHANISM TWO POINTERS & PALINDROMES · PALIN · CODE MIRRORED

SYMMETRY MEANS A POINTER FROM EACH END

The naive palindrome check builds a reversed copy and compares — O(n) extra space for a question that needs none. Symmetry is a two-pointer shape: start at both ends, compare, and step both inward. A match retires two characters at once, so the pointers meet after n/2 comparisons; a single mismatch returns false immediately, without reading the rest. O(n) time, O(1) space, no copy. The same L/R shape drives the rest of this family.

ONE POINTER FROM EACH END
STATE
CODE MIRROR
INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
16 / PROBLEM #05 · TWO-POINTER · EASY

Valid Palindrome

EASY two-pointer ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

“Is it a palindrome” with the twist of ignoring case and non-alphanumeric characters. Two pointers from the ends, skipping what does not count — the archetypal inward two-pointer scan.

INTUITION

Put one pointer at each end. Advance each past any non-alphanumeric character, then compare the two in lowercase; if they ever differ it is not a palindrome. Meet in the middle and it is. O(1) space — no cleaned copy needed.

STEPS
  1. l = 0, r = n − 1
  2. While l < r: skip l right past non-alnum; skip r left past non-alnum
  3. Compare tolower(s[l]) and tolower(s[r]); if different, return false
  4. Otherwise l++, r--
  5. Return true
BRUTEO(n)
OPTIMALO(n)
↕ SCROLL
// Two pointers inward, skipping non-alphanumerics, comparing lowercase.
bool isPalindrome(string s) {
    int l = 0, r = s.size() - 1;
    while (l < r) {
        while (l < r && !isalnum(s[l])) l++;
        while (l < r && !isalnum(s[r])) r--;
        if (tolower(s[l]) != tolower(s[r])) return false;
        l++; r--;
    }
    return true;
}
TIMEO(n)each pointer moves inward at most n times total
SPACEO(1)two indices, no copy
TRAP

Keeping l < r in the inner skip loops. If you skip without re-checking the bound, the pointers can cross while hunting for the next alphanumeric and you compare garbage or read out of range. Build the filter into the same loop rather than allocating a cleaned string — the two-pointer scan is O(1) space.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
17 / PROBLEM #06 · TWO-POINTER · MED

Longest Palindromic Substring

MED two-pointer ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

“Longest palindromic substring.” Substrings + palindrome + “longest” points to expand around centre: every palindrome has a centre, so try all of them and grow outward.

INTUITION

A palindrome is defined by its centre and mirror symmetry. There are 2n − 1 centres — each character (odd length) and each gap between adjacent characters (even length). From each, expand while the two ends match; keep the longest span seen. O(n²), O(1) space, and far simpler than DP.

STEPS
  1. best = (start 0, length 1)
  2. For each index i: expand around (i, i) for odd-length palindromes
  3. Also expand around (i, i+1) for even-length palindromes
  4. expand(l, r): while l ≥ 0, r < n, s[l]==s[r]: l--, r++; the span is (l+1 .. r-1)
  5. Track the longest span and return that substring
BRUTEO(n³)
OPTIMALO(n²)
↕ SCROLL
// Every palindrome has a centre; try all 2n-1 and grow outward.
string longestPalindrome(string s) {
    if (s.empty()) return "";
    int bestL = 0, bestR = 0;                 // inclusive span
    auto expand = [&](int l, int r) {
        while (l >= 0 && r < (int)s.size() && s[l] == s[r]) { l--; r++; }
        if (r - l - 2 > bestR - bestL) { bestL = l + 1; bestR = r - 1; }
    };
    for (int i = 0; i < (int)s.size(); i++) {
        expand(i, i);       // odd length
        expand(i, i + 1);   // even length
    }
    return s.substr(bestL, bestR - bestL + 1);
}
TIMEO(n²)2n−1 centres, each expanded in O(n)
SPACEO(1)a few indices; no DP table
TRAP

Only expanding around single characters. That misses every even-length palindrome (abba, the bb in cbbd). You must try both (i, i) and (i, i+1) centres. Also note the post-loop span arithmetic: after the while exits, the last matching bounds are l+1 .. r-1, not l .. r.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
18 / PATTERN FREQUENCY TABLES

PRIME IT, THEN APPLY IT

DRILL 01 · RECALL

Valid Anagram can be done by sorting both strings (O(n log n)) or by counting (O(n)). When is the counting version strictly better, and how does it end?

Count up for one, down for the other, then check all zero. A single 26-slot array: increment on s, decrement on t; if the lengths match and every slot returns to 0 they are anagrams. That is O(n) versus the sort's O(n log n), and O(1) space for a bounded alphabet. The general lesson: order doesn't matter ⇒ count, don't sort.

DRILL 02 · TRACE

The 'beauty' of a substring is (max char frequency − min non-zero frequency). For "aabcb", the beauty of the whole string is:

1. In "aabcb" the counts are a:2, b:2, c:1; the most frequent letter appears 2 times, the least frequent present letter 1 time, so beauty = 2 − 1 = 1. Summing this over all substrings is O(n²·Σ): fix a start, extend the end one char at a time updating a rolling frequency table, and read max − min each step. The min must be over present letters only — zeros are not in the alphabet of the substring.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
19 / MECHANISM FREQUENCY TABLES · FREQ · CODE MIRRORED

WHEN ORDER DOES NOT MATTER, COUNT

The phrase to listen for is anagram, how many times, rearrange — all of them mean the order is irrelevant, and the instinct to sort (O(n log n)) is a trap. One pass filling a 26-slot table is O(n), and afterwards every order-free question about the string is a lookup rather than another scan. The trap this animation makes concrete is the minimum: it is over letters that are present, so the twenty-odd zero slots must be skipped — count them and the answer is always wrong by exactly the minimum.

COUNT, DO NOT SORT
STATE
CODE MIRROR
INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
20 / PROBLEM #07 · FREQUENCY · EASY

Valid Anagram

EASY frequency ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

“Are these two strings anagrams” — order does not matter, only the multiset of characters. That is the cue to count, not to sort.

INTUITION

If the lengths differ they cannot be anagrams. Otherwise a single frequency array: increment for each character of the first string, decrement for the second. They are anagrams exactly when every count returns to zero.

STEPS
  1. If lengths differ, return false
  2. count[26] = {0}
  3. For each char of s: count[c - 'a']++
  4. For each char of t: count[c - 'a']--
  5. Return true iff every entry is 0
BRUTEO(n log n)
OPTIMALO(n)
↕ SCROLL
// Order doesn't matter -> count, don't sort. One array, up then down.
bool isAnagram(string s, string t) {
    if (s.size() != t.size()) return false;
    int cnt[26] = {0};
    for (char c : s) cnt[c - 'a']++;
    for (char c : t) cnt[c - 'a']--;
    for (int x : cnt) if (x) return false;
    return true;
}
TIMEO(n)two linear passes over the alphabet-bounded counts
SPACEO(1)a fixed 26-int table
TRAP

Sorting when counting is O(n). Sorting both strings works but is O(n log n); for a bounded alphabet the count-array beats it and, as a bonus, generalises to “is one an anagram of a substring of the other” (sliding window). If the alphabet is Unicode, swap the 26-array for a hash map — but keep the count-don't-sort instinct.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
21 / PROBLEM #08 · FREQUENCY · MED

Sort Characters By Frequency

MED frequency ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

“Sort the characters by how often they appear” — output driven by frequency, not by the characters' natural order. Count first, then order the counts.

INTUITION

Tally every character's frequency, then emit characters most-frequent first, each repeated its count of times. You can sort the (char, count) pairs in O(k log k), or bucket by frequency in O(n) since no count exceeds n.

STEPS
  1. Count frequency of each character (map or 128-array)
  2. Order characters by descending frequency (sort the pairs, or bucket by count)
  3. Build the result: each character repeated 'frequency' times
  4. Return the assembled string
BRUTEO(n log n)
OPTIMALO(n + k log k)
↕ SCROLL
// Count, then emit most-frequent first.
string frequencySort(string s) {
    unordered_map<char,int> f;
    for (char c : s) f[c]++;
    vector<pair<int,char>> v;
    for (auto& [c, n] : f) v.push_back({n, c});
    sort(v.rbegin(), v.rend());               // by descending frequency
    string out;
    for (auto& [n, c] : v) out.append(n, c);
    return out;
}
TIMEO(n + k log k)counting is O(n); ordering the k distinct chars is k log k (or O(n) bucketed)
SPACEO(n)the counts and the output
TRAP

Sorting the characters instead of by their counts. The order is by frequency, so a rare character must come last regardless of its letter value; sort the (count, char) pairs, not the raw string. For the O(n) version, bucket characters by their frequency (1..n) and read buckets high to low — no comparison sort at all.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
22 / PROBLEM #09 · FREQUENCY · MED

Sum of Beauty of All Substrings

MED frequency ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

“Sum a per-substring statistic over ALL substrings”, where the statistic is maxFreq − minFreq. O(n²) substrings with a rolling frequency table is the intended shape.

INTUITION

There are O(n²) substrings, but you do not recount each from scratch. Fix a start i; extend the end j one character at a time, updating a frequency table incrementally, and add max − min (over present characters) at every step. Σ = 26, so each update and read is O(26).

STEPS
  1. total = 0
  2. For each start i: clear a 26-slot frequency table
  3. For each end j from i to n−1: freq[s[j]]++
  4. Compute maxFreq and the minimum NON-ZERO freq over the 26 slots
  5. total += (maxFreq − minFreq); return total
BRUTEO(n³·Σ)
OPTIMALO(n²·Σ)
↕ SCROLL
// Fix a start, extend the end, keep a rolling frequency table.
int beautySum(string s) {
    int n = s.size(), total = 0;
    for (int i = 0; i < n; i++) {
        int f[26] = {0};
        for (int j = i; j < n; j++) {
            f[s[j] - 'a']++;
            int mx = 0, mn = INT_MAX;
            for (int k = 0; k < 26; k++) if (f[k]) {   // present chars only
                mx = max(mx, f[k]);
                mn = min(mn, f[k]);
            }
            total += mx - mn;
        }
    }
    return total;
}
TIMEO(n²·Σ)n² substrings, each an O(26) max/min scan
SPACEO(Σ)one 26-slot table reused per start
TRAP

Taking the minimum over all 26 slots including the zeros. The minimum must be over characters actually present in the substring — a zero is not a frequency in that substring, and including it makes every beauty max − 0. The other trap is recounting each substring from scratch (O(n³)); extend the previous count by one character instead.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
23 / PATTERN WORDS, PREFIXES & SUFFIXES

PRIME IT, THEN APPLY IT

DRILL 01 · RECALL

“Reverse the word order of a sentence” versus “reverse each word in place” — what is the operational difference?

One reorders the words; the other reorders letters within each word. Reverse Words in a String splits on whitespace, drops the empties, reverses the list and rejoins with single spaces. Reverse Words III leaves the word order alone and reverses the characters inside each maximal non-space span. A neat one-liner for both: reverse the whole string, then reverse each word — do it once and you get order-reversal, apply the inner reversal only and you get III.

DRILL 02 · TRACE

Largest Odd Number in String scans "52427" for the answer. What is it, and what is the rule?

"52427". Any prefix ending in an odd digit is an odd number, and the longest such prefix is the largest — so scan from the right for the first odd digit and return everything up to it. Here the final 7 is odd, so the whole string qualifies. If it were "5240" you would back up to the 5 and return "5"; all-even gives "". Greedy from the correct end, O(n).

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
24 / MECHANISM WORDS, PREFIXES & SUFFIXES · LCP · CODE MIRRORED

SCAN THE COLUMN, NOT THE WORD

The common prefix is not found by comparing strings to each other in pairs — it is found by walking one index at a time across all of them. Take column 0 of every word, then column 1, and stop at the first disagreement; everything before that column is the answer. Because the very first mismatch ends it, the work is bounded by the shortest word and not the longest, and running off the end of a short string counts as a disagreement too. Here flower / flow / flight agree on f and l, then split at column 2 — so the answer is "fl", decided after three columns rather than eighteen characters.

SCAN BY COLUMN, NOT BY WORD
STATE
CODE MIRROR
INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
25 / PROBLEM #10 · WORDS-PREFIX · MED

Reverse Words in a String

MED words-prefix ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

“Reverse the ORDER of the words” and normalise spacing (leading, trailing, and multiple inner spaces collapse to one). A tokenise-and-reverse on the list of words.

INTUITION

Split the sentence on whitespace, discarding the empty tokens that leading/trailing/double spaces produce, reverse the resulting list of words, and rejoin with single spaces. The O(1)-space version reverses the whole string then reverses each word back.

STEPS
  1. Split s on runs of whitespace, dropping empty pieces
  2. Reverse the list of words
  3. Join with a single space between words
  4. (In-place variant: reverse whole string, then reverse each word span)
  5. Return the result
BRUTEO(n)
OPTIMALO(n)
↕ SCROLL
// Tokenise on whitespace, drop blanks, reverse the word list, rejoin.
string reverseWords(string s) {
    vector<string> w;
    stringstream ss(s);
    string tok;
    while (ss >> tok) w.push_back(tok);       // >> skips runs of spaces
    reverse(w.begin(), w.end());
    string out;
    for (int i = 0; i < (int)w.size(); i++)
        out += (i ? " " : "") + w[i];
    return out;
}
TIMEO(n)one pass to tokenise, one to join
SPACEO(n)the word list / output (O(1) with the in-place reversal trick)
TRAP

Splitting on a single space and forgetting the empties. Leading, trailing and doubled spaces create empty tokens; a naive split(' ') keeps them and you emit stray spaces. Split on runs of whitespace (C++ >>, Python split() with no argument) so the blanks vanish, then join with exactly one space.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
26 / PROBLEM #11 · WORDS-PREFIX · EASY

Reverse Words in a String III

EASY words-prefix ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

“Reverse the letters of each word but KEEP the word order” and keep the spaces where they are. Reverse each maximal non-space span in place — the mirror image of the previous problem.

INTUITION

Word order and spacing stay exactly as given; only the characters inside each word flip. Walk the string, and each time you hit a maximal run of non-space characters, reverse that span with two pointers. Spaces are left untouched.

STEPS
  1. i = 0
  2. While i < n: if s[i] is a space, i++ and continue
  3. Mark j = i; advance j to the end of the current word (next space or end)
  4. Reverse the characters in [i, j−1] with two pointers
  5. Set i = j; continue. Return s
BRUTEO(n)
OPTIMALO(n)
↕ SCROLL
// Reverse each maximal non-space span in place; spaces stay put.
string reverseWords(string s) {
    int n = s.size(), i = 0;
    while (i < n) {
        if (s[i] == ' ') { i++; continue; }
        int j = i;
        while (j < n && s[j] != ' ') j++;     // word is [i, j)
        reverse(s.begin() + i, s.begin() + j);
        i = j;
    }
    return s;
}
TIMEO(n)each character is visited a constant number of times
SPACEO(1)in-place reversal, no extra buffer
TRAP

Confusing this with word-order reversal. Here the words stay in place and the letters flip; there the words move and the letters do not. If the problem guarantees single spaces, a plain split(' ') is correct — but do not collapse spaces, because unlike problem 10 the spacing is meant to be preserved.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
27 / PROBLEM #12 · WORDS-PREFIX · EASY

Longest Common Prefix

EASY words-prefix ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

“Longest common prefix of an array of strings.” A prefix is shared only if every string agrees at that position, so scan column by column and stop at the first disagreement.

INTUITION

Look at character position 0 across all strings, then position 1, and so on. The moment one string is too short or differs from the first string's character at that column, the common prefix ends. Return everything before that column.

STEPS
  1. If the array is empty, return ""
  2. For column j = 0, 1, 2, …: let c = first_string[j]
  3. For each other string: if j ≥ its length or its[j] ≠ c, return first_string[0..j)
  4. If the first string runs out, return it whole
  5. Return the accumulated prefix
BRUTEO(n·m)
OPTIMALO(n·m)
↕ SCROLL
// Scan column by column; stop at the first string that disagrees.
string longestCommonPrefix(vector<string>& strs) {
    if (strs.empty()) return "";
    for (int j = 0; j < (int)strs[0].size(); j++) {
        char c = strs[0][j];
        for (int i = 1; i < (int)strs.size(); i++)
            if (j >= (int)strs[i].size() || strs[i][j] != c)
                return strs[0].substr(0, j);
    }
    return strs[0];
}
TIMEO(n·m)n strings, m = shortest length; stops early on the first mismatch
SPACEO(1)just indices
TRAP

Forgetting the length guard when a string is shorter than the prefix. Reading strs[i][j] without checking j < len(strs[i]) reads out of bounds on the shortest string, which is exactly where the prefix must end. Handle the empty array too — the answer there is the empty string, not a crash.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
28 / PROBLEM #13 · WORDS-PREFIX · EASY

Largest Odd Number in String

EASY words-prefix ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

“Largest odd number that is a substring” of a digit string — and the answer must be a prefix, because dropping trailing digits keeps the start. That makes it a greedy scan from the right for the last odd digit.

INTUITION

A number is odd iff its last digit is odd. Any prefix ending at an odd digit is an odd number, and the longest such prefix is the largest. So scan from the right for the first odd digit; the answer is the whole prefix up to and including it. All-even means no odd number exists.

STEPS
  1. Scan i from n−1 down to 0
  2. If s[i] is an odd digit (s[i] − '0' is odd), return s[0..i]
  3. If the loop finishes, no odd digit exists — return ""
  4. (Leading zeros are fine to return per the problem's definition)
BRUTEO(n²)
OPTIMALO(n)
↕ SCROLL
// Odd <-> last digit is odd. The largest odd substring is the longest
// prefix ending in an odd digit -> scan from the right.
string largestOddNumber(string num) {
    for (int i = num.size() - 1; i >= 0; i--)
        if ((num[i] - '0') % 2 == 1)
            return num.substr(0, i + 1);
    return "";
}
TIMEO(n)a single right-to-left scan for the last odd digit
SPACEO(1)one index; the returned prefix is a view/copy
TRAP

Scanning from the left or trimming leading zeros. The greedy direction is from the right: you want the longest prefix, so you keep as many leading digits as possible and only chop the even tail. Do not strip leading zeros — the problem treats the substring as-is — and return "" (not "0") when every digit is even.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
29 / PATTERN MAPPING & ROTATION

PRIME IT, THEN APPLY IT

DRILL 01 · RECALL

Why does an isomorphic-strings check need TWO maps (or a map plus a used-set) rather than one?

Isomorphism is a bijection, and one map only guards one side. A single s → t map ensures each source character is consistent, but it would happily accept “ba” → “aa”, mapping both b and a onto a. Enforcing the reverse map t → s too (or a set of already-used targets) forbids that collision. Any “consistent renaming” problem needs both directions.

DRILL 02 · TRANSFER

Rotate String asks if goal is a rotation of s. What is the one-line reduction?

|s| == |goal| and goals + s. Concatenating s with itself lays out every rotation of s as a length-|s| window, so a single substring search settles it — after the length guard, which stops a shorter goal from matching by accident. It is the same doubling trick that turns many circular-array problems into linear ones.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
30 / MECHANISM MAPPING & ROTATION · ROTATE · CODE MIRRORED

EVERY ROTATION IS A WINDOW OF s + s

This is the sharpest trick in the deck. Instead of generating each rotation and comparing — a loop with an off-by-one waiting in it — double the string. Every rotation of s appears in s + s as one contiguous window of length n, so “is goal a rotation of s?” collapses into a single substring search. Watch the answer land at offset 2, straddling the seam between the two copies — that is precisely the rotation a naive loop is most likely to fumble. Guard the equal-length check first, or "ab" matches inside "abab" for the wrong reason.

EVERY ROTATION IS A WINDOW OF s + s
STATE
CODE MIRROR
INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
31 / PROBLEM #14 · MAP-ROTATE · EASY

Isomorphic Strings

EASY map-rotate ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

“Isomorphic” / “consistent one-to-one replacement” of characters. The word bijection is the tell: you need a mapping that is consistent in both directions.

INTUITION

Walk both strings in lockstep. Maintain a map s → t and a map t → s. At each position, if either map already binds the character to a different partner, it is not isomorphic. If both agree (or are unset, in which case you set them), continue.

STEPS
  1. If lengths differ, return false
  2. Maps st (s→t) and ts (t→s), both empty
  3. For each index i with a = s[i], b = t[i]:
  4. If st has a but st[a] ≠ b, or ts has b but ts[b] ≠ a, return false
  5. Otherwise set st[a] = b, ts[b] = a. After the loop, return true
BRUTEO(n)
OPTIMALO(n)
↕ SCROLL
// A bijection needs BOTH directions consistent: s->t and t->s.
bool isIsomorphic(string s, string t) {
    if (s.size() != t.size()) return false;
    int st[128], ts[128];
    memset(st, -1, sizeof st); memset(ts, -1, sizeof ts);
    for (int i = 0; i < (int)s.size(); i++) {
        unsigned char a = s[i], b = t[i];
        if (st[a] == -1 && ts[b] == -1) { st[a] = b; ts[b] = a; }
        else if (st[a] != b || ts[b] != a) return false;
    }
    return true;
}
TIMEO(n)one pass; map operations O(1) for a bounded charset
SPACEO(1)two 128-slot maps
TRAP

Mapping only one direction. A single s → t map accepts “badc” → “baba”, letting two different source characters collapse onto the same target. Enforce the reverse map too (or a used-target set). It is the exact bug the group drill flagged — isomorphism is a bijection, and a bijection is two-way.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
32 / PROBLEM #15 · MAP-ROTATE · EASY

Rotate String

EASY map-rotate ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

“Is goal a ROTATION of s”. Rotation of a string is the doubling trick: every rotation of s is a window of s + s, so it reduces to a substring search.

INTUITION

If the lengths differ, no rotation is possible. Otherwise concatenate s with itself: it contains every rotation of s as a contiguous block of length |s|. So goal is a rotation iff it appears as a substring of s + s.

STEPS
  1. If |s| ≠ |goal|, return false
  2. Form the doubled string s + s
  3. Return whether goal is a substring of s + s
  4. (Substring search is O(n) with KMP, O(n²) naive — both pass here)
BRUTEO(n²)
OPTIMALO(n)
↕ SCROLL
// Every rotation of s is a window of s+s -> one substring search.
bool rotateString(string s, string goal) {
    return s.size() == goal.size() &&
           (s + s).find(goal) != string::npos;
}
TIMEO(n)one length check plus a linear substring search (KMP)
SPACEO(n)the doubled string
TRAP

Skipping the length guard. Without |s| == |goal|, a goal that is a genuine but shorter substring of s + s passes as a rotation when it is not. Check equal lengths first, then the substring — the guard is what makes the doubling trick exact.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
33 / RECALL RETRIEVAL, NOT RECOGNITION

NAME THE PATTERN FROM THE STATEMENT

DRILL 01 · TRANSFER

Three different statements: “is b a rotation of a”, “does a contain b”, and “are the two strings anagrams”. Which pair shares a core trick?

Rotation is a substring search in disguise. Every rotation of a appears as a contiguous window of a + a, so “is b a rotation of a” becomes “is b a substring of a+a” (after the length check) — the same primitive as “does a contain b”. Anagram is the odd one out: it throws away order entirely and only counts letters. Sorting the statement into “does order matter?” picks the tool before you write a line.

DRILL 02 · RECALL

Across these fifteen, which single idea recurs most — and names its own family?

The single left-to-right pass with a running state. Remove Outermost Parentheses carries a depth; Max Nesting Depth carries a depth and its max; Roman to Integer carries an accumulator with a look-ahead rule; atoi carries a sign and a clamped value; anagram and beauty carry a frequency table. Most string problems are not about a clever data structure — they are about choosing the right thing to carry through one pass. Naming that as a family is what lets you start writing immediately instead of hunting for a trick.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
34 / TRAPS PLAUSIBLE, WRONG, AND SILENT

SIX WAYS TO BE PLAUSIBLY WRONG

String bugs are quiet: an off-by-one in a skip loop, an overflow after the multiply, a one-way map. Every one of these compiles and returns a believable answer.

MUTATING / COPYING IN A LOOP

s = s + c (Python) or s.substr each iteration turns an O(n) scan into O(n²). Build a vector<char> / list and join once, and take substrings by index range, not by copying.

ASSUMING ASCII / LOWERCASE

A 26-slot array crashes or misses on uppercase, digits, spaces or Unicode. Valid Palindrome and atoi are explicitly mixed-case with punctuation — size the table 128, or filter first, and read the constraints for the real alphabet.

atoi: OVERFLOW AND THE STATE ORDER

The classic bug is checking overflow after multiplying — it has already wrapped. Clamp before each x = x*10 + d, and respect the exact order: skip spaces → one optional sign → digits → stop at the first non-digit.

PALINDROME: ONE CENTRE KIND

Expanding only around single characters misses even-length palindromes like abba. There are 2n − 1 centres — every index AND every gap between adjacent indices.

ISOMORPHIC: CHECKING ONLY ONE DIRECTION

A map from s to t alone accepts “badc” → “baba” wrongly. The bijection must hold BOTH ways — two maps, or a map plus a used-set — or two different source chars collapse onto one target.

ROTATION / CONTAINS: THE LENGTH GUARD

b can only be a rotation of a if |a| == |b|; without that guard, b being a shorter substring of a+a passes falsely. Cheap check first, then the substring search.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
35 / CHEATSHEET REVISION SURFACE

ONE SCREEN, THE WHOLE DECK

Fourteen recognitions, one page. The right-hand column is the tell — the phrase in the statement that should trigger the pattern before you write a line.

ALGORITHM
TIME
SPACE
REACH FOR IT WHEN
Depth counter (parens)
O(n)
O(1)
remove-outermost / max-nesting — one running depth
Look-ahead accumulate
O(n)
O(1)
Roman to Integer — subtract if this < next
Careful state machine
O(n)
O(1)
atoi — spaces, sign, digits, clamp before multiply
Two pointers inward
O(n)
O(1)
Valid Palindrome — skip non-alnum, compare ends
Expand around centre
O(n²)
O(1)
Longest Palindromic Substring — 2n−1 centres
Frequency array (26)
O(n)
O(1)
Valid Anagram — count up, count down, all zero
Count then order
O(n + k log k)
O(k)
Sort Characters By Frequency — bucket or sort
Rolling frequency
O(n²·Σ)
O(Σ)
Sum of Beauty — max−min freq over each substring
Tokenise & reverse
O(n)
O(n)
Reverse Words — split, drop blanks, reverse order
Reverse each word
O(n)
O(1)
Reverse Words III — reverse between spaces in place
Vertical scan
O(n·m)
O(1)
Longest Common Prefix — column by column
Greedy suffix
O(n)
O(1)
Largest Odd Number — rightmost odd digit, take prefix
Two-way mapping
O(n)
O(1)
Isomorphic — bijection both directions
Double & search
O(n)
O(n)
Rotate String — |a|==|b| and b in a+a
INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
36 / CLOSE STEP 05 · PROBLEMS + DRILLS

FIFTEEN, WRITTEN FROM MEMORY

None of these needed a new data structure — just the right thing carried through one pass, or two pointers, or a count. Once the pattern behind each statement is automatic, the string round of an interview stops being about strings and starts being about reading the signal.

00%
OF THIS DECK SOLVED
← ALL TOPICSSTEP 03 · ARRAYSSTEP 04 · BINARY SEARCH

Problem links are LeetCode. No lecture playlist was supplied for this topic, so there are no concept videos — the intros, drills and the five mechanism visualisers carry it instead.

INVARIANT · STRINGS · FIFTEEN PATTERNS, WRITTEN FROM MEMORY · PROBLEMS + DRILLS
■ TOO SMALL TO READ
1280×720
INVARIANT · STEP 05 · PROBLEMS + DRILLS

This one needs a laptop

Not a preference — an arithmetic one. Every slide is a fixed 1280 × 720 stage: a graph animating beside the code that drives it, with the problems laid out two columns wide. It scales as a single piece, so on a screen this size the body text comes out around 4px tall.

Shrinking it further would not help, and rebuilding it to reflow would mean losing the thing that makes it worth reading.

YOUR SCREEN0 × 0
NEEDED1060 × 610
WHAT IS WAITING ON THE LAPTOP
READ IT ONCE, THEN WRITE IT FROM MEMORY
Your progress is saved per device, so anything you tick on the laptop will be waiting there.