Coding Drills Β· 03

Strings & Sliding Window

A string is just a char[] with opinions. Almost every string interview question collapses into one of six scanning shapes: a stack that matches pairs, a window [L, R] that grows and shrinks, a center that expands outward, a run scanner, a pattern matcher that never backs up, or digit-by-digit arithmetic. Drills 24–32 build each shape from scratch in Java.

9
Drills
2
Easy
6
Medium
1
Hard
01
Chapter One Β· Core Idea

Scanning a String Without Rescanning It

The brute-force instinct on a string of length n is to enumerate every substring β€” there are n(n+1)/2 of them β€” and test each one. That is O(nΒ²) substrings and often O(nΒ³) work once you test them. Every technique on this page exists to avoid that. The trick is always the same: carry state forward so that the work done for position i is reused for position i + 1. A stack remembers which brackets are still open. A window remembers the counts of everything between L and R. KMP remembers how much of the pattern is already matched. Each one turns "start over" into "adjust a little".

The sliding window β€” one pass, two monotonic pointers
0 5 9 12 A D O B E C O D E B A N C L R window = s[L..R] = "BANC" R only moves right (gain characters) Β· L only moves right (drop characters) each index enters once and leaves once β†’ O(n) total, not O(nΒ²)
Analogy: Reading a long line of text through a cardboard frame with adjustable edges. You never lift the frame and restart from the left β€” you only slide the right edge forward to reveal more, and slide the left edge forward to hide what you no longer need. Whatever you are counting (distinct letters, missing letters, repeated letters) is updated by the one character that entered or the one that left, never recounted from scratch.
🧱

Stack matching

Last opened must be first closed. Drill 24.

πŸͺŸ

Variable window

Grow R, shrink L while a counter says "invalid". Drills 25, 26, 27.

🎯

Expand around center

2n βˆ’ 1 centers, grow outward while mirrored. Drill 28.

πŸ”

Run scanning

Consume a run of equal chars, emit (char, count). Drills 29, 31.

πŸ”

Pattern matching

KMP's failure table means the text pointer never moves back. Drill 30.

βœ–οΈ

Digit simulation

Grade-school arithmetic on char digits with carries. Drill 32.

02
Chapter Two Β· Mental Model

How String Problems Think

Brackets close in the reverse order they open
β–Ά
The most recent unmatched opener is the only legal partner for the next closer β€” exactly what a stack's top gives you in O(1)
The question says "longest / shortest contiguous substring such that…"
β–Ά
The constraint is monotonic in the window: adding characters can only break "at most K" constraints and only help "contains all of T" constraints β€” so one pointer per direction suffices
A frequency array or map is updated by exactly one character per pointer move
β–Ά
Validity checks become O(1) (a size, a counter, a max) instead of re-scanning the window β€” the window algorithm stays O(n)
A palindrome is symmetric around its middle
β–Ά
Pick the middle first (a character or a gap), then grow outward while both ends match β€” 2n βˆ’ 1 centers, O(nΒ²) total, O(1) memory
The output describes runs of equal characters
β–Ά
An inner while loop consumes a whole run; the outer loop handles one run per iteration β€” no off-by-one "last group" flush needed
Numbers are too big for long, so they arrive as strings
β–Ά
Simulate pencil-and-paper arithmetic: digit i times digit j always lands in positions i + j and i + j + 1
open/close pairs that must nest? yes Stack β€” Drill 24 no longest/shortest substring with a rule? yes Sliding window Drills 25 Β· 26 Β· 27 no palindrome substring? yes Expand around center β€” 28 no runs of equal characters? yes Run scan β€” Drills 29 Β· 31 no find pattern β†’ KMP (30) Β· big numbers β†’ digits (32)
Read the constraint, not the story:
  • "Contiguous" plus "longest" or "shortest" is the sliding-window signal. "Subsequence" is not β€” that is dynamic programming (page 09).
  • "Only uppercase English letters" or "ASCII" is a hint to use an int[26] or int[128] instead of a HashMap β€” same big-O, far smaller constant.
  • "Return the new length, modify in place" means a read/write pointer pair, exactly like the two-pointer compaction on page 02.
03
Chapter Three Β· Pattern Toolkit

Three Templates You Will Reuse

Memorise these three skeletons. Each drill below is one of them with a different validity test plugged in. The skill an interviewer is checking is not typing the loop β€” it is choosing the right counter so that "is the window valid?" costs O(1).

Template 1 β€” Longest Valid Window
Grow R every step. While the window breaks the rule, shrink L. After the inner loop the window is valid, so record its length.
Pseudocode
function longestValid(s): L = 0, best = 0, state = empty for R = 0 to n - 1: add(state, s[R]) // expand while invalid(state): remove(state, s[L]); L++ // shrink until legal again best = max(best, R - L + 1) // window is valid here return best // O(n): L and R each move n times
Template 1 on "abcabcbb" β€” longest substring without repeats
step 0 1 2 3 4 5 6 7 R=2 a b c a b c b b L=0 "abc" len 3 β†’ best 3 R=3 a b c a b c b b second 'a' β†’ drop s[0], L=1 R=6 a b c a b c b b dup 'b' β†’ drop a, b β†’ L=5 "cb" R=7 a b c a b c b b dup 'b' β†’ drop c, b β†’ L=7 "b" answer = 3 β€” L moved 7 times, R moved 8 times: 15 steps, not 36 substrings
Template 2 β€” Shortest Covering Window
The mirror image of Template 1: grow R until the window becomes valid, then shrink L as far as it stays valid, recording the length before each shrink.
Pseudocode
function shortestValid(s): L = 0, best = ∞ for R = 0 to n - 1: add(state, s[R]) while valid(state): // note: VALID, not invalid best = min(best, R - L + 1) // record BEFORE shrinking remove(state, s[L]); L++ return best
The two phases alternate β€” each pointer still moves at most n times
EXPAND R++ window does not yet cover everything it must RECORD + SHRINK L++ window is valid: save length, then try a smaller one becomes valid lost a needed char Template 1 asks "while INVALID, shrink" β€” Template 2 asks "while VALID, shrink". Pick the one whose answer you record while the window is legal.
Template 3 β€” Expand Around Center
Every palindrome has a center: a single character (odd length) or the gap between two characters (even length). Try all 2n βˆ’ 1 centers and grow outward while the ends match.
Pseudocode
function expand(s, lo, hi): while lo >= 0 and hi < n and s[lo] == s[hi]: lo--, hi++ return hi - lo - 1 // both overshot by one for i = 0 to n - 1: best = max(best, expand(s, i, i), expand(s, i, i + 1))
"abba" has 7 centers β€” the even center between the two b's wins
a b b a b=b βœ“ then a=a βœ“ then out of bounds β†’ length 4 odd center (i, i) even center (i, i+1) odd centers give "a","b","b","a" (len 1); gap a|b and b|a fail immediately skip the even centers and you will never find "abba"
04
Drill 24 Β· Stack

Valid Parentheses

🏷️

Difficulty

Easy

πŸ”—

LeetCode

LC 20 Β· full write-up

🧩

Pattern

stack

Given a string containing only the characters ( ) [ ] { }, decide whether it is valid: every opener is closed by the same type of bracket, and brackets close in the correct order (the most recently opened one closes first).

Example
Input: s = "({[]})" Output: true Input: s = "([)]" Output: false // ']' must close before ')' Input: s = "((" Output: false // openers left over
Key insight: Nesting is last-in, first-out. Think of Russian dolls: you can only close the innermost open doll. So when you see an opener, push the closer you now expect; when you see a closer, it must equal whatever is on top. Pushing the expected closer (instead of the opener) turns three separate if checks into one equality test.

Brute force β†’ optimal. A naive approach repeatedly deletes adjacent pairs (), [], {} until nothing changes; each pass is O(n) and there can be n/2 passes, so O(nΒ²). The stack does the same "cancel the innermost pair" work in a single left-to-right pass: every character is pushed or popped exactly once, O(n).

Trace on "({[]})" β€” the stack holds expected closers
step 1 step 2 step 3 step 4 step 5 step 6 ( { [ ] } ) ) ) } ) } ] ) } ) empty push ) push } push ] pop ] = ] βœ“ pop } = } βœ“ pop ) = ) βœ“ gold = top of stack, the only closer allowed next Β· empty at the end β†’ true
Java β€” ArrayDeque stack, push the expected closer
import java.util.ArrayDeque; import java.util.Deque; public class ValidParentheses { public static boolean isValid(String s) { if (s.length() % 2 == 1) return false; // odd length can never balance Deque<Character> stack = new ArrayDeque<>(); // faster than legacy Stack for (char c : s.toCharArray()) { if (c == '(') stack.push(')'); // push the EXPECTED closer else if (c == '{') stack.push('}'); else if (c == '[') stack.push(']'); else if (stack.isEmpty() || stack.pop() != c) return false; } return stack.isEmpty(); // leftovers = unclosed openers } public static void main(String[] args) { System.out.println(isValid("({[]})")); // true System.out.println(isValid("([)]")); // false System.out.println(isValid("((")); // false } }
ApproachTimeSpaceNote
Repeatedly delete "()", "[]", "{}"O(nΒ²)O(n)New string per pass
Stack (one pass)O(n)O(n)Worst case "((((((" fills the stack
Common bugs:
  • Popping without checking isEmpty() first β€” input ")" throws NoSuchElementException on ArrayDeque (or EmptyStackException on Stack).
  • Returning true at the end of the loop instead of stack.isEmpty() β€” "((" would wrongly pass.
  • Using java.util.Stack: it works, but it is synchronized and extends Vector. ArrayDeque is the recommended stack.
  • Odd length is an instant false β€” a free early exit, not a correctness requirement.
05
Drill 25 Β· Sliding Window

Longest Substring with At Most K Distinct Characters

🏷️

Difficulty

Medium

πŸ”—

LeetCode

LC 340 (premium)

🧩

Pattern

sliding window hash map

Given a string s and an integer k, return the length of the longest substring that contains at most k distinct characters.

Example
Input: s = "eceba", k = 2 Output: 3 // "ece" uses only 'e' and 'c' Input: s = "aa", k = 1 Output: 2
Key insight: "Distinct characters in the window" is exactly map.size() if the map drops a key when its count reaches zero. So validity is an O(1) check β€” map.size() <= k β€” and the problem is Template 1 verbatim.

Brute force β†’ optimal. Checking all O(nΒ²) substrings and counting distinct characters in each is O(nΒ³), or O(nΒ²) if you extend each start incrementally. The window improves on that by noticing that if s[L..R] has more than k distinct characters, so does every longer window starting at L β€” so L can be abandoned forever. Both pointers only move right: O(n).

Trace on "eceba", k = 2 β€” green = window after shrinking, red = evicted this step
0 1 2 3 4 L Β· counts len Β· best R=0 e c e b a L=0 Β· e:1 1 Β· 1 R=1 e c e b a L=0 Β· e:1 c:1 2 Β· 2 R=2 e c e b a L=0 Β· e:2 c:1 3 Β· 3 ← "ece" R=3 e c e b a L=2 Β· e:1 b:1 2 Β· 3 R=4 e c e b a L=3 Β· b:1 a:1 2 Β· 3 R=3: adding 'b' makes 3 distinct β†’ evict e (e:2β†’1), still 3 β†’ evict c (c:1β†’0, key removed) answer = 3
Java β€” HashMap counts, drop keys at zero
import java.util.HashMap; import java.util.Map; public class LongestKDistinct { public static int lengthOfLongestSubstringKDistinct(String s, int k) { if (k == 0) return 0; Map<Character, Integer> freq = new HashMap<>(); int best = 0, left = 0; for (int right = 0; right < s.length(); right++) { freq.merge(s.charAt(right), 1, Integer::sum); // expand while (freq.size() > k) { // too many distinct: shrink char out = s.charAt(left++); if (freq.merge(out, -1, Integer::sum) == 0) freq.remove(out); } best = Math.max(best, right - left + 1); // window is valid here } return best; } public static void main(String[] args) { System.out.println(lengthOfLongestSubstringKDistinct("eceba", 2)); // 3 ("ece") System.out.println(lengthOfLongestSubstringKDistinct("aa", 1)); // 2 } }
ApproachTimeSpaceNote
All substrings, count distinctO(nΒ²)O(k)Extend each start incrementally
Sliding window + mapO(n)O(k)Map never holds more than k + 1 keys
Common bugs:
  • Decrementing the count but forgetting map.remove(key) at zero β€” map.size() then counts ghosts and the window shrinks too far.
  • Using if instead of while for the shrink: one eviction may not be enough (R=3 above needed two).
  • k == 0 must return 0; without the guard the loop still works here, but many variants divide by or index with k.
  • Comparing boxed Integer counts with == β€” fine below 128 because of the cache, silently wrong above it. merge(...) == 0 is safe because it unboxes against an int literal.
06
Drill 26 Β· Sliding Window

Longest Repeating Character Replacement

🏷️

Difficulty

Medium

πŸ”—

LeetCode

LC 424 Β· full write-up

🧩

Pattern

sliding window int[26]

Given an uppercase string s and an integer k, you may change at most k characters to any other uppercase letter. Return the length of the longest substring that can be made of one repeated letter.

Example
Input: s = "AABABBA", k = 1 Output: 4 // "AABA" β†’ change the B β†’ "AAAA" Input: s = "ABAB", k = 2 Output: 4 // change both B's (or both A's)
Key insight: In any window, the cheapest repaint keeps the most frequent letter and repaints everything else. So a window is valid when length βˆ’ maxFreq ≀ k. You never need to know which letter wins β€” only how many times it appears.

Brute force β†’ optimal. Trying every substring and computing its majority count is O(26Β·nΒ²). The window version tracks counts in an int[26]. The subtle optimisation: maxFreq is never decreased when L moves. A stale (too large) maxFreq can only make the window look valid when it is not β€” but the window then merely slides (L and R both +1) instead of growing, so best can only increase when a genuinely higher frequency appears. That makes the shrink a single if, not a while.

Walkthrough β€” "AABABBA", k = 1
Rcharwindow after stepmaxFreqlen βˆ’ maxFreqactionbest
0AA10grow1
1AAA20grow2
2BAAB21grow (1 ≀ k)3
3AAABA31grow4
4BABAB35 βˆ’ 3 = 2 > 1slide: drop s[0], L=14
5BBABB35 βˆ’ 3 = 2 > 1slide: drop s[1], L=24
6AABBA35 βˆ’ 3 = 2 > 1slide: drop s[2], L=34

The "len" in the check is the length before sliding (R βˆ’ L + 1 = 5). After the last slide the window is "ABBA", whose true maximum is 2 β€” yet maxFreq still reads 3. That stale value is harmless: it could only let a window of width 4 pass, and 4 was already recorded. The answer can only grow when some letter genuinely reaches a count of 4.

Java β€” int[26] counts, sliding (non-shrinking) window
public class CharacterReplacement { public static int characterReplacement(String s, int k) { int[] count = new int[26]; int maxFreq = 0, best = 0, left = 0; for (int right = 0; right < s.length(); right++) { maxFreq = Math.max(maxFreq, ++count[s.charAt(right) - 'A']); // cells to repaint = window length - most frequent letter if (right - left + 1 - maxFreq > k) { count[s.charAt(left) - 'A']--; left++; // slide, never shrink below best } best = Math.max(best, right - left + 1); } return best; } public static void main(String[] args) { System.out.println(characterReplacement("AABABBA", 1)); // 4 ("AABA" or "ABBB") System.out.println(characterReplacement("ABAB", 2)); // 4 } }
ApproachTimeSpaceNote
Every substring, recountO(26 · n²)O(1)Too slow for n = 10⁡
Window, recompute max over 26 lettersO(26 Β· n)O(1)Correct, simpler to argue
Window, stale maxFreqO(n)O(1)Alphabet size is constant
Common bugs:
  • Indexing count[c - 'a'] on an uppercase-only input β€” negative index, ArrayIndexOutOfBoundsException.
  • "Fixing" the stale maxFreq by decrementing it when L moves β€” it then under-counts and the window shrinks wrongly. Either leave it stale or recompute it from all 26 counts.
  • Answer can never exceed n; with k β‰₯ n the whole string is the answer β€” the loop handles it without a special case.
07
Drill 27 Β· Sliding Window

Minimum Window Substring

🏷️

Difficulty

Hard

πŸ”—

LeetCode

LC 76 Β· full write-up

🧩

Pattern

shortest window missing counter

Given strings s and t, return the shortest substring of s that contains every character of t including duplicates. If there is none, return the empty string.

Example
Input: s = "ADOBECODEBANC", t = "ABC" Output: "BANC" Input: s = "a", t = "aa" Output: "" // t needs two a's, s has one
Key insight: Keep one array need[c] = how many more of c the window still owes, and one integer missing = total owed. Adding s[R] pays a debt only if need[s[R]] was positive; removing s[L] creates a debt only if need[s[L]] becomes positive. The window covers t exactly when missing == 0 β€” an O(1) test instead of comparing two maps.

Brute force β†’ optimal. Testing every substring against t's counts is O(nΒ² Β· |Ξ£|). Template 2 cuts it to one pass: expand R until missing hits 0, then shrink L while it stays 0, recording each candidate. Characters not in t go negative in need, which is exactly what lets them be dropped for free later.

The three moments that matter on s = "ADOBECODEBANC", t = "ABC"
A D O B E C O D E B A N C 0 5 9 10 12 R=5: first cover "ADOBEC" (len 6) β†’ best = 6 R=10: shrunk to "CODEBA" (len 6) β€” not shorter R=12: "BANC" (len 4) β†’ best gold cells are characters of t Β· every other char only ever pushes need[c] below zero
Every valid window the inner loop sees
RL range while validwindows recorded / checkedwhy the shrink stops
50"ADOBEC" (6) β†’ best 6dropping A at 0 owes an A
101 β†’ 5"DOBECODEBA" … "CODEBA" (all β‰₯ 6)dropping C at 5 owes a C
126 β†’ 9"ODEBANC" (7), "DEBANC" (6), "EBANC" (5) β†’ best 5, "BANC" (4) β†’ best 4dropping B at 9 owes a B
Java β€” need[] array + missing counter
public class MinWindowSubstring { public static String minWindow(String s, String t) { if (t.isEmpty() || s.length() < t.length()) return ""; int[] need = new int[128]; for (char c : t.toCharArray()) need[c]++; int missing = t.length(); // chars of t still not covered int bestLen = Integer.MAX_VALUE, bestStart = 0, left = 0; for (int right = 0; right < s.length(); right++) { if (need[s.charAt(right)]-- > 0) missing--; // a useful char entered while (missing == 0) { // window covers t: shrink if (right - left + 1 < bestLen) { bestLen = right - left + 1; bestStart = left; } if (++need[s.charAt(left++)] > 0) missing++; // lost a needed char } } return bestLen == Integer.MAX_VALUE ? "" : s.substring(bestStart, bestStart + bestLen); } public static void main(String[] args) { System.out.println(minWindow("ADOBECODEBANC", "ABC")); // BANC System.out.println(minWindow("a", "aa")); // "" (not enough a's) } }
ApproachTimeSpaceNote
Every substring vs t's countsO(nΒ² Β· |Ξ£|)O(|Ξ£|)Ξ£ = alphabet
Window with two HashMapsO(|s| + |t|)O(|Ξ£|)Boxing overhead, Integer equality traps
Window, int[128] + missingO(|s| + |t|)O(1)128 ints regardless of input
Common bugs:
  • Counting distinct characters of t instead of total characters β€” t = "AA" needs two A's, not one.
  • Comparing window.get(c) == tMap.get(c) with two Integer objects β€” breaks once a count passes 127. Use .intValue() or primitive arrays.
  • Recording the best window after shrinking β€” you record a window that is no longer valid.
  • Storing the substring on every improvement β€” O(n) copy each time. Store bestStart and bestLen, cut once at the end.
08
Drill 28 Β· Expand Around Center

Longest Palindromic Substring

🏷️

Difficulty

Medium

πŸ”—

LeetCode

LC 5 Β· full write-up

🧩

Pattern

expand around center

Given a string s, return its longest substring that reads the same forwards and backwards. If several have the same maximum length, any one of them is accepted.

Example
Input: s = "babad" Output: "bab" // "aba" is also accepted Input: s = "cbbd" Output: "bb" // even-length: needs the gap center
Key insight: Checking a substring for palindrome-ness from the outside in wastes work. Growing from the inside out reuses it: if s[lo+1..hi-1] is a palindrome, then s[lo..hi] is one exactly when s[lo] == s[hi]. One comparison per extra character.

Brute force β†’ optimal. Checking all O(nΒ²) substrings with an O(n) palindrome test is O(nΒ³). Expand-around-center is O(nΒ²) time and O(1) space and is what interviewers expect. A DP table dp[i][j] is also O(nΒ²) but uses O(nΒ²) memory. Manacher's algorithm reaches O(n) β€” worth naming, rarely worth coding in an interview.

Expanding every center of "babad"
0 1 2 3 4 b a b a d center i=1 "bab" len 3 βœ“ kept center i=2 "aba" len 3 β€” tie, ignored odd expand(i, i): i=0β†’1 i=1β†’3 i=2β†’3 i=3β†’1 i=4β†’1 even expand(i, i+1): all 0 β€” no two neighbours are equal start = i βˆ’ (len βˆ’ 1) / 2 = 1 βˆ’ 1 = 0 β†’ s.substring(0, 3) = "bab"
Java β€” expand around 2n βˆ’ 1 centers
public class LongestPalindrome { public static String longestPalindrome(String s) { if (s.length() < 2) return s; int start = 0, maxLen = 1; for (int i = 0; i < s.length(); i++) { int odd = expand(s, i, i); // center on a character int even = expand(s, i, i + 1); // center between two characters int len = Math.max(odd, even); if (len > maxLen) { maxLen = len; start = i - (len - 1) / 2; // works for both odd and even } } return s.substring(start, start + maxLen); } private static int expand(String s, int lo, int hi) { while (lo >= 0 && hi < s.length() && s.charAt(lo) == s.charAt(hi)) { lo--; hi++; } return hi - lo - 1; // length of the last valid palindrome } public static void main(String[] args) { System.out.println(longestPalindrome("babad")); // bab System.out.println(longestPalindrome("cbbd")); // bb } }
ApproachTimeSpaceNote
All substrings + checkO(nΒ³)O(1)Too slow beyond n β‰ˆ 300
DP table dp[i][j]O(nΒ²)O(nΒ²)1000 Γ— 1000 booleans = 1 MB
Expand around centerO(nΒ²)O(1)Interview standard
ManacherO(n)O(n)Reuses mirror radii; rarely expected
Common bugs:
  • Only expanding odd centers β€” "cbbd" returns "c" instead of "bb".
  • Returning hi - lo + 1 from expand: both pointers have already stepped one past the palindrome, so the length is hi - lo - 1.
  • Start index for even lengths: i - len / 2 is off by one for even len; i - (len - 1) / 2 works for both parities.
  • Building substrings inside the loop β€” track start and maxLen and call substring once.
09
Drill 29 Β· Run Scan + Read/Write

String Compression

🏷️

Difficulty

Medium

πŸ”—

LeetCode

LC 443

🧩

Pattern

run scan read/write pointers

Compress a char[] in place: each maximal run of a repeated character becomes the character followed by its count β€” but the count is written only when the run is longer than 1, and a count like 12 is written as two characters '1','2'. Return the new length; only the first that many cells matter.

Example
Input: chars = ['a','a','b','b','c','c','c'] Output: 6 // chars starts with ['a','2','b','2','c','3'] Input: chars = ['a','b','b','b','b','b','b','b','b','b','b','b','b'] Output: 4 // "ab12" β€” a single 'a' gets no count
Key insight: The compressed form of a run is never longer than the run itself (a run of 1 writes 1 char; a run of length L β‰₯ 2 writes 1 + digits(L) ≀ L chars). So the write pointer can never overtake the read pointer, and writing into the same array is safe β€” the same read/write compaction you used for Move Zeroes on page 02.

Brute force β†’ optimal. The easy version builds a StringBuilder and copies it back: O(n) time but O(n) extra space, which violates the in-place requirement. The optimal version keeps a read index that consumes one whole run per outer iteration and a write index that emits the character and (if needed) the digits. Consuming the run with an inner while avoids the classic "flush the last group after the loop" bug.

Walkthrough β€” ['a','a','b','b','c','c','c']
runcharread: start β†’ endlengthwrittenwrite after
1a0 β†’ 22'a', '2'2
2b2 β†’ 42'b', '2'4
3c4 β†’ 73'c', '3'6

After run 1 the array is ['a','2','b','b','c','c','c'] β€” the write landed on cells read has already passed. Final return value: 6.

Java β€” consume a run, emit char + digits
import java.util.Arrays; public class StringCompression { public static int compress(char[] chars) { int write = 0, read = 0; while (read < chars.length) { char c = chars[read]; int runStart = read; while (read < chars.length && chars[read] == c) read++; // consume the run chars[write++] = c; int runLen = read - runStart; if (runLen > 1) { // count only if > 1 for (char d : Integer.toString(runLen).toCharArray()) chars[write++] = d; } } return write; } public static void main(String[] args) { char[] chars = {'a', 'a', 'b', 'b', 'c', 'c', 'c'}; int len = compress(chars); System.out.println(len); // 6 System.out.println(Arrays.toString(Arrays.copyOf(chars, len))); // [a, 2, b, 2, c, 3] char[] single = {'a', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b'}; len = compress(single); System.out.println(new String(single, 0, len)); // ab12 } }
ApproachTimeSpaceNote
StringBuilder then copy backO(n)O(n)Not in place
Read/write in placeO(n)O(1)Count digits are at most 4 for n ≀ 2000
Common bugs (including one in the original version of this drill):
  • Writing a count of 1 β€” the problem writes "a2bc3", not "a2b1c3". For ['a','a','b','c','c','c'] the correct answer is 5, not 6.
  • Writing a two-digit count as one char: (char) ('0' + 12) is '<'. Convert with Integer.toString(runLen) and copy each digit.
  • Forgetting import java.util.Arrays when printing with Arrays.toString β€” a compile error, not a runtime one.
  • Returning chars.length instead of write.
10
Drill 30 Β· Pattern Matching

Implement strStr() β€” Brute Force and KMP

🏷️

Difficulty

Easy

πŸ”—

LeetCode

LC 28

🧩

Pattern

fixed window KMP

Return the index of the first occurrence of needle in haystack, or -1 if it does not occur. This is what String.indexOf does β€” the drill is to write it yourself.

Example
Input: haystack = "hello", needle = "ll" Output: 2 Input: haystack = "abxabcabcaby", needle = "abcaby" Output: 6 Input: haystack = "aaaaa", needle = "bba" Output: -1
Key insight: When a match fails after j characters, you already know those j characters of the haystack β€” they equal needle[0..j-1]. KMP precomputes, for every prefix of the needle, the longest proper prefix that is also a suffix (lps). On a mismatch you keep that overlap and continue, so the haystack index never moves backwards.

Brute force β†’ optimal. Sliding a fixed window of length m and comparing character by character is O((n βˆ’ m + 1) Β· m) β€” fine for most inputs, but "aaaa…ab" in "aaaa…aaab" degrades to O(nΒ·m). KMP is O(n + m): building lps is O(m), and the scan is O(n) because j can only decrease as many times as it increased. In an interview, write the brute force first, state its worst case, then offer KMP.

KMP on haystack "abxabcabcaby", needle "abcaby"
needle lps a b c a b y 0 0 0 1 2 0 lps[4] = 2: "abcab" starts and ends with "ab" text 0 3 6 8 11 a b x a b c a b c a b y i=8: c β‰  y a b c a b y j=5 j = lps[4] = 2 a b c a b y j=6 βœ“ earlier, at i=2 ('x' vs 'c'): j = lps[1] = 0, and 'x' β‰  'a' so j stays 0 gold "ab" is reused, never re-read Β· match ends at i=11 β†’ return 11 βˆ’ 6 + 1 = 6
Java β€” brute force and KMP side by side
public class StrStr { // Brute force: try every alignment. O((n - m + 1) * m) public static int strStrBrute(String haystack, String needle) { int n = haystack.length(), m = needle.length(); for (int i = 0; i + m <= n; i++) { int j = 0; while (j < m && haystack.charAt(i + j) == needle.charAt(j)) j++; if (j == m) return i; } return -1; } // KMP: never move the haystack pointer backwards. O(n + m) public static int strStr(String haystack, String needle) { int m = needle.length(); if (m == 0) return 0; int[] lps = buildLps(needle); int j = 0; // chars of needle matched for (int i = 0; i < haystack.length(); i++) { while (j > 0 && haystack.charAt(i) != needle.charAt(j)) j = lps[j - 1]; if (haystack.charAt(i) == needle.charAt(j)) j++; if (j == m) return i - m + 1; } return -1; } // lps[i] = length of the longest proper prefix of needle[0..i] that is also a suffix private static int[] buildLps(String p) { int[] lps = new int[p.length()]; int len = 0; for (int i = 1; i < p.length(); i++) { while (len > 0 && p.charAt(i) != p.charAt(len)) len = lps[len - 1]; if (p.charAt(i) == p.charAt(len)) len++; lps[i] = len; } return lps; } public static void main(String[] args) { System.out.println(strStrBrute("hello", "ll")); // 2 System.out.println(strStr("hello", "ll")); // 2 System.out.println(strStr("abxabcabcaby", "abcaby")); // 6 System.out.println(strStr("aaaaa", "bba")); // -1 } }
ApproachTimeSpaceNote
Brute force windowO((n βˆ’ m + 1) Β· m)O(1)Usually fast; worst case O(nΒ·m)
KMPO(n + m)O(m)lps array; text pointer never backs up
Rabin–Karp rolling hashO(n + m) expectedO(1)Needs a collision check on hash match
Common bugs:
  • Loop bound i < n - m misses a match that ends at the last character β€” use i + m <= n (also safe when m > n).
  • Empty needle: by convention the answer is 0 (matches String.indexOf("")).
  • In KMP, using if instead of while for the fallback j = lps[j - 1] β€” one fallback may not be enough.
  • The complexity claim "O(nΒ·m)" for the brute force is the worst case, not the typical case β€” say both.
11
Drill 31 Β· Run Scan

Count and Say

🏷️

Difficulty

Medium

πŸ”—

LeetCode

LC 38

🧩

Pattern

run-length encoding simulation

The count-and-say sequence starts with "1". Each next term is produced by reading the previous term aloud, run by run: "21" is read "one 2, one 1" and becomes "1211". Return the n-th term.

Example
Input: n = 4 Output: "1211" // 1 β†’ "1" Β· 2 β†’ "11" Β· 3 β†’ "21" Β· 4 β†’ "1211" Β· 5 β†’ "111221"
Key insight: Each step is run-length encoding with the order flipped to (count, digit). It is the same inner-while run scanner as Drill 29, just emitting into a new StringBuilder instead of overwriting in place.

Brute force β†’ optimal. There is no shortcut formula β€” you must build every term. The recursive version (countAndSay(n - 1) then encode) is correct but uses O(n) stack frames for no benefit; the loop is simpler. What matters for the complexity claim is how long the terms get: they grow by roughly 30% per step (Conway's constant Ξ» β‰ˆ 1.3036), not by doubling. Term 30 β€” LeetCode's maximum β€” has 4,462 characters.

Walkthrough β€” reading each term aloud
nprevious termrunssaid asnew term
21[1]one 111
311[11]two 1s21
421[2] [1]one 2, one 11211
51211[1] [2] [11]one 1, one 2, two 1s111221
6111221[111] [22] [1]three 1s, two 2s, one 1312211
Java β€” iterative run-length "say"
public class CountAndSay { public static String countAndSay(int n) { String term = "1"; for (int step = 2; step <= n; step++) { StringBuilder next = new StringBuilder(); int i = 0; while (i < term.length()) { char digit = term.charAt(i); int run = 0; while (i < term.length() && term.charAt(i) == digit) { i++; run++; } next.append(run).append(digit); // "say" the run: count then digit } term = next.toString(); } return term; } public static void main(String[] args) { for (int n = 1; n <= 5; n++) System.out.println(n + " -> " + countAndSay(n)); // 1 -> 1, 2 -> 11, 3 -> 21, 4 -> 1211, 5 -> 111221 } }
ApproachTimeSpaceNote
Recursive buildO(L₁ + … + Lβ‚™)O(Lβ‚™ + n)Extra n stack frames
Iterative buildO(L₁ + … + Lβ‚™) β‰ˆ O(1.31ⁿ)O(Lβ‚™)Lβ‚™ = length of term n
Common bugs:
  • Claiming O(2ⁿ) β€” the original version of this drill did. Terms grow by about 1.3036Γ— per step, so O(2ⁿ) is a loose upper bound, not the real cost.
  • Appending digit then count ("12" for one 2 written as "21") β€” the count comes first.
  • A "compare with next" loop that forgets to flush the final run. The inner-while form has no final flush to forget.
  • String concatenation s += ... in the loop is O(LΒ²) per term β€” use StringBuilder.
12
Drill 32 Β· Digit Simulation

Multiply Strings

🏷️

Difficulty

Medium

πŸ”—

LeetCode

LC 43

🧩

Pattern

grade-school math position array

Given two non-negative integers num1 and num2 as strings, return their product as a string. You may not convert the inputs to integers directly or use BigInteger β€” the inputs can be 200 digits long.

Example
Input: num1 = "123", num2 = "456" Output: "56088" Input: num1 = "0", num2 = "52" Output: "0"
Key insight: An m-digit number times an n-digit number has at most m + n digits. Digit num1[i] times digit num2[j] contributes to exactly two positions of that result: its ones digit lands in pos[i + j + 1] and its carry in pos[i + j]. So you can accumulate every partial product directly, instead of building n shifted rows and adding them.

Brute force β†’ optimal. The schoolbook method β€” multiply num1 by each digit of num2, shift, then add the rows with a string-addition helper β€” is also O(mΒ·n) but allocates n intermediate strings. The position-array version does the same arithmetic in one int[m + n]. (Karatsuba gets O(n^1.585) for huge numbers; not expected here.)

Where 2 Γ— 6 lands in "123" Γ— "456"
num1 1 2 3 i = 1 num2 4 5 6 j = 2 pos (final) 0 5 6 0 8 8 0 1 2 3 = i+j 4 = i+j+1 5 2 Γ— 6 = 12 sum = 12 + pos[4] (6) = 18 pos[4] = 18 % 10 = 8 pos[3] += 18 / 10 β†’ 3 + 1 = 4 (later steps settle pos[3] to 0) skip the leading 0 β†’ "56088"
All nine digit products, right to left
i, jdigitsproductslotspos after
2, 23 Γ— 6184, 5[0,0,0,0,1,8]
2, 13 Γ— 5153, 4[0,0,0,1,6,8]
2, 03 Γ— 4122, 3[0,0,1,3,6,8]
1, 22 Γ— 6123, 4[0,0,1,4,8,8]
1, 12 Γ— 5102, 3[0,0,2,4,8,8]
1, 02 Γ— 481, 2[0,1,0,4,8,8]
0, 21 Γ— 662, 3[0,1,1,0,8,8]
0, 11 Γ— 551, 2[0,1,6,0,8,8]
0, 01 Γ— 440, 1[0,5,6,0,8,8]
Java β€” accumulate into pos[i + j] and pos[i + j + 1]
public class MultiplyStrings { public static String multiply(String num1, String num2) { if (num1.equals("0") || num2.equals("0")) return "0"; int m = num1.length(), n = num2.length(); int[] pos = new int[m + n]; // product has at most m + n digits for (int i = m - 1; i >= 0; i--) { for (int j = n - 1; j >= 0; j--) { int mul = (num1.charAt(i) - '0') * (num2.charAt(j) - '0'); int sum = mul + pos[i + j + 1]; // add onto the ones slot pos[i + j + 1] = sum % 10; pos[i + j] += sum / 10; // carry into the tens slot } } StringBuilder sb = new StringBuilder(); for (int d : pos) { if (sb.length() == 0 && d == 0) continue; // skip leading zeros sb.append(d); } return sb.toString(); } public static void main(String[] args) { System.out.println(multiply("123", "456")); // 56088 System.out.println(multiply("99", "99")); // 9801 System.out.println(multiply("0", "52")); // 0 } }
ApproachTimeSpaceNote
Shifted rows + string additionO(m Β· n)O(m Β· n)n intermediate strings
Position arrayO(m Β· n)O(m + n)One int array, one StringBuilder
Common bugs:
  • Forgetting to add the existing pos[i + j + 1] before taking % 10 β€” partial products overwrite each other.
  • Leaving leading zeros: pos[0] is 0 whenever the product has fewer than m + n digits.
  • Returning "" for "0" Γ— "52" β€” the early return (or the empty-builder fallback) must produce "0".
  • Parsing with Long.parseLong β€” overflows past 19 digits and is explicitly disallowed.
13
Chapter Thirteen Β· Summary

All Nine Drills at a Glance

#DrillPatternTimeSpaceDifficulty
24Valid ParenthesesStack of expected closersO(n)O(n)Easy
25Longest Substring, ≀ K DistinctLongest window + mapO(n)O(k)Medium
26Longest Repeating Char ReplacementWindow, len βˆ’ maxFreq ≀ kO(n)O(1)Medium
27Minimum Window SubstringShortest window + missing counterO(|s| + |t|)O(1)Hard
28Longest Palindromic SubstringExpand around 2n βˆ’ 1 centersO(nΒ²)O(1)Medium
29String CompressionRun scan + read/writeO(n)O(1)Medium
30Implement strStr()KMP (brute force as baseline)O(n + m)O(m)Easy
31Count and SayRun-length encoding, iteratedO(Ξ£Lα΅’) β‰ˆ O(1.31ⁿ)O(Lβ‚™)Medium
32Multiply StringsPosition-array digit mathO(m Β· n)O(m + n)Medium
The one question to ask first:
  • "What do I need to remember about the part of the string I have already seen?" β€” open brackets (stack), counts in a window (array/map), matched prefix length (KMP), or nothing beyond the current run.
  • If the answer fits in O(1) or O(alphabet) memory and is updated by one character at a time, the solution is linear.
What to practice next

← 02 Two Pointers All drills 04 Binary Search & Intervals β†’