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.
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".
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.
How String Problems Think
long, so they arrive as stringsi times digit j always lands in positions i + j and i + j + 1- "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]orint[128]instead of aHashMapβ 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.
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).
Valid Parentheses
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).
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).
| Approach | Time | Space | Note |
|---|---|---|---|
| Repeatedly delete "()", "[]", "{}" | O(nΒ²) | O(n) | New string per pass |
| Stack (one pass) | O(n) | O(n) | Worst case "((((((" fills the stack |
- Popping without checking
isEmpty()first β input")"throwsNoSuchElementExceptiononArrayDeque(orEmptyStackExceptiononStack). - Returning
trueat the end of the loop instead ofstack.isEmpty()β"(("would wrongly pass. - Using
java.util.Stack: it works, but it is synchronized and extendsVector.ArrayDequeis the recommended stack. - Odd length is an instant
falseβ a free early exit, not a correctness requirement.
Longest Substring with At Most K Distinct Characters
Given a string s and an integer k, return the length of the longest substring that contains at most k distinct characters.
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).
| Approach | Time | Space | Note |
|---|---|---|---|
| All substrings, count distinct | O(nΒ²) | O(k) | Extend each start incrementally |
| Sliding window + map | O(n) | O(k) | Map never holds more than k + 1 keys |
- Decrementing the count but forgetting
map.remove(key)at zero βmap.size()then counts ghosts and the window shrinks too far. - Using
ifinstead ofwhilefor the shrink: one eviction may not be enough (R=3 above needed two). k == 0must return 0; without the guard the loop still works here, but many variants divide by or index with k.- Comparing boxed
Integercounts with==β fine below 128 because of the cache, silently wrong above it.merge(...) == 0is safe because it unboxes against an int literal.
Longest Repeating Character Replacement
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.
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.
| R | char | window after step | maxFreq | len β maxFreq | action | best |
|---|---|---|---|---|---|---|
| 0 | A | A | 1 | 0 | grow | 1 |
| 1 | A | AA | 2 | 0 | grow | 2 |
| 2 | B | AAB | 2 | 1 | grow (1 β€ k) | 3 |
| 3 | A | AABA | 3 | 1 | grow | 4 |
| 4 | B | ABAB | 3 | 5 β 3 = 2 > 1 | slide: drop s[0], L=1 | 4 |
| 5 | B | BABB | 3 | 5 β 3 = 2 > 1 | slide: drop s[1], L=2 | 4 |
| 6 | A | ABBA | 3 | 5 β 3 = 2 > 1 | slide: drop s[2], L=3 | 4 |
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.
| Approach | Time | Space | Note |
|---|---|---|---|
| Every substring, recount | O(26 Β· nΒ²) | O(1) | Too slow for n = 10β΅ |
| Window, recompute max over 26 letters | O(26 Β· n) | O(1) | Correct, simpler to argue |
| Window, stale maxFreq | O(n) | O(1) | Alphabet size is constant |
- Indexing
count[c - 'a']on an uppercase-only input β negative index,ArrayIndexOutOfBoundsException. - "Fixing" the stale
maxFreqby 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; withk β₯ nthe whole string is the answer β the loop handles it without a special case.
Minimum Window Substring
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.
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.
| R | L range while valid | windows recorded / checked | why the shrink stops |
|---|---|---|---|
| 5 | 0 | "ADOBEC" (6) β best 6 | dropping A at 0 owes an A |
| 10 | 1 β 5 | "DOBECODEBA" β¦ "CODEBA" (all β₯ 6) | dropping C at 5 owes a C |
| 12 | 6 β 9 | "ODEBANC" (7), "DEBANC" (6), "EBANC" (5) β best 5, "BANC" (4) β best 4 | dropping B at 9 owes a B |
| Approach | Time | Space | Note |
|---|---|---|---|
| Every substring vs t's counts | O(nΒ² Β· |Ξ£|) | O(|Ξ£|) | Ξ£ = alphabet |
| Window with two HashMaps | O(|s| + |t|) | O(|Ξ£|) | Boxing overhead, Integer equality traps |
| Window, int[128] + missing | O(|s| + |t|) | O(1) | 128 ints regardless of input |
- Counting distinct characters of
tinstead of total characters βt = "AA"needs two A's, not one. - Comparing
window.get(c) == tMap.get(c)with twoIntegerobjects β 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
bestStartandbestLen, cut once at the end.
Longest Palindromic Substring
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.
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.
| Approach | Time | Space | Note |
|---|---|---|---|
| All substrings + check | O(nΒ³) | O(1) | Too slow beyond n β 300 |
| DP table dp[i][j] | O(nΒ²) | O(nΒ²) | 1000 Γ 1000 booleans = 1 MB |
| Expand around center | O(nΒ²) | O(1) | Interview standard |
| Manacher | O(n) | O(n) | Reuses mirror radii; rarely expected |
- Only expanding odd centers β "cbbd" returns "c" instead of "bb".
- Returning
hi - lo + 1fromexpand: both pointers have already stepped one past the palindrome, so the length ishi - lo - 1. - Start index for even lengths:
i - len / 2is off by one for evenlen;i - (len - 1) / 2works for both parities. - Building substrings inside the loop β track
startandmaxLenand callsubstringonce.
String Compression
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.
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.
| run | char | read: start β end | length | written | write after |
|---|---|---|---|---|---|
| 1 | a | 0 β 2 | 2 | 'a', '2' | 2 |
| 2 | b | 2 β 4 | 2 | 'b', '2' | 4 |
| 3 | c | 4 β 7 | 3 | '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.
| Approach | Time | Space | Note |
|---|---|---|---|
| StringBuilder then copy back | O(n) | O(n) | Not in place |
| Read/write in place | O(n) | O(1) | Count digits are at most 4 for n β€ 2000 |
- 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 withInteger.toString(runLen)and copy each digit. - Forgetting
import java.util.Arrayswhen printing withArrays.toStringβ a compile error, not a runtime one. - Returning
chars.lengthinstead ofwrite.
Implement strStr() β Brute Force and 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.
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.
| Approach | Time | Space | Note |
|---|---|---|---|
| Brute force window | O((n β m + 1) Β· m) | O(1) | Usually fast; worst case O(nΒ·m) |
| KMP | O(n + m) | O(m) | lps array; text pointer never backs up |
| RabinβKarp rolling hash | O(n + m) expected | O(1) | Needs a collision check on hash match |
- Loop bound
i < n - mmisses a match that ends at the last character β usei + m <= n(also safe whenm > n). - Empty needle: by convention the answer is 0 (matches
String.indexOf("")). - In KMP, using
ifinstead ofwhilefor the fallbackj = 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.
Count and Say
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.
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.
| n | previous term | runs | said as | new term |
|---|---|---|---|---|
| 2 | 1 | [1] | one 1 | 11 |
| 3 | 11 | [11] | two 1s | 21 |
| 4 | 21 | [2] [1] | one 2, one 1 | 1211 |
| 5 | 1211 | [1] [2] [11] | one 1, one 2, two 1s | 111221 |
| 6 | 111221 | [111] [22] [1] | three 1s, two 2s, one 1 | 312211 |
| Approach | Time | Space | Note |
|---|---|---|---|
| Recursive build | O(Lβ + β¦ + Lβ) | O(Lβ + n) | Extra n stack frames |
| Iterative build | O(Lβ + β¦ + Lβ) β O(1.31βΏ) | O(Lβ) | Lβ = length of term n |
- 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-
whileform has no final flush to forget. - String concatenation
s += ...in the loop is O(LΒ²) per term β useStringBuilder.
Multiply Strings
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.
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.)
| i, j | digits | product | slots | pos after |
|---|---|---|---|---|
| 2, 2 | 3 Γ 6 | 18 | 4, 5 | [0,0,0,0,1,8] |
| 2, 1 | 3 Γ 5 | 15 | 3, 4 | [0,0,0,1,6,8] |
| 2, 0 | 3 Γ 4 | 12 | 2, 3 | [0,0,1,3,6,8] |
| 1, 2 | 2 Γ 6 | 12 | 3, 4 | [0,0,1,4,8,8] |
| 1, 1 | 2 Γ 5 | 10 | 2, 3 | [0,0,2,4,8,8] |
| 1, 0 | 2 Γ 4 | 8 | 1, 2 | [0,1,0,4,8,8] |
| 0, 2 | 1 Γ 6 | 6 | 2, 3 | [0,1,1,0,8,8] |
| 0, 1 | 1 Γ 5 | 5 | 1, 2 | [0,1,6,0,8,8] |
| 0, 0 | 1 Γ 4 | 4 | 0, 1 | [0,5,6,0,8,8] |
| Approach | Time | Space | Note |
|---|---|---|---|
| Shifted rows + string addition | O(m Β· n) | O(m Β· n) | n intermediate strings |
| Position array | O(m Β· n) | O(m + n) | One int array, one StringBuilder |
- 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.
All Nine Drills at a Glance
| # | Drill | Pattern | Time | Space | Difficulty |
|---|---|---|---|---|---|
| 24 | Valid Parentheses | Stack of expected closers | O(n) | O(n) | Easy |
| 25 | Longest Substring, β€ K Distinct | Longest window + map | O(n) | O(k) | Medium |
| 26 | Longest Repeating Char Replacement | Window, len β maxFreq β€ k | O(n) | O(1) | Medium |
| 27 | Minimum Window Substring | Shortest window + missing counter | O(|s| + |t|) | O(1) | Hard |
| 28 | Longest Palindromic Substring | Expand around 2n β 1 centers | O(nΒ²) | O(1) | Medium |
| 29 | String Compression | Run scan + read/write | O(n) | O(1) | Medium |
| 30 | Implement strStr() | KMP (brute force as baseline) | O(n + m) | O(m) | Easy |
| 31 | Count and Say | Run-length encoding, iterated | O(Ξ£Lα΅’) β O(1.31βΏ) | O(Lβ) | Medium |
| 32 | Multiply Strings | Position-array digit math | O(m Β· n) | O(m + n) | Medium |
- "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.
- Next page: 04 Β· Binary Search & Intervals β Drills 33β39, where "halve the search space" replaces "slide the window".
- Previous page: 02 Β· Two Pointers β the read/write compaction behind Drill 29.
- Theory: Sliding Window, Stack, HashMap, Two Pointers.
- More problems: LeetCode sliding-window set, LeetCode stack set, and LC 3 β Template 1 with a set.
β 02 Two Pointers All drills 04 Binary Search & Intervals β