AlgoAlgo/12 · String Algorithms
CHAPTER 12 · String Algorithms

String algorithms

Every fast string matching algorithm rests on one idea: keep what each failed comparison told you, and use it next time. This chapter starts with the waste in naive matching, which throws away even the comparisons that succeeded. You then build the KMP prefix function by hand and watch the text pointer stop moving backwards. After that comes the rolling hash, which turns a whole window into one number, and finally palindromes, which are built on symmetry around a center instead.

§01

Naive matching: a failure throws away the successes too

See where the work is wasted first, then it is clear what KMP saves

The problem is as plain as it gets: in a text haystack of length n, find the first position where a pattern needle of length m occurs. The most natural idea is to line the pattern up with every start position and compare character by character. If the characters match, move one step right. If they do not, move to the next start position. That is naive matching, also called brute force.

It is correct, but slow. Where does the time go? Watch this run in slow motion: find abab inside abaabab. Keep your eye on the pointer i. On every mismatch it is pulled back to the position right after the current start, and everything the earlier successful comparisons established is lost.

Naive matching — i is pulled back after every mismatch
text
a0
b1
a2
a3
b4
a5
b6
pattern
a0
b1
a2
b3
Find the pattern abab inside the text abaabab. Naive matching lines the pattern up with every start position and compares character by character. Watch what the pointer i does after a mismatch.
1 / 13

How bad can the worst case get? Take the text aaaa…aab and the pattern aaab. Every start position has to be compared all the way to the last character before it fails, so the number of comparisons is about n×m. That is the O(n·m) of naive matching. The problem is not that comparing characters is slow. The problem is that every failure starts over from nothing, even though a long prefix was just confirmed to match.

Waste 01
i moves back

After a mismatch the text pointer returns to the start position plus one. Characters that were already read are read again. This is the direct source of the O(n·m) cost.

Waste 02
Information discarded

The prefix that matched before the mismatch describes the structure of the pattern itself, but naive matching throws it away.

The fix
Remember the failures

Precompute, for every possible failure point, which prefix the matched suffix is equal to, and store it in a table. Then a mismatch is one table lookup and one jump. That is KMP.

KMP: three authors, and two routes to the same algorithm

KMP is named after Knuth, Morris, and Pratt, who published it together in 1977. Morris found the technique while writing a text editor, where scanning backwards over input was awkward. Knuth reached the same algorithm from theory, while studying what two-way deterministic pushdown automata can recognize. The central insight is one sentence: where the pattern should fall back to after a mismatch depends only on the pattern, not on the text. That is why the whole table can be computed before the search starts.

§02

The prefix function: the table that turns failure into information

The core idea — the longest equal proper prefix and suffix, and why it lets i stay put

All of KMP fits into one array, called the prefix function. In code it is usually named next or fail, and in papers it is written π. Its definition is one sentence, but every word in it matters.

Definition (used everywhere in this chapter)

next[i] = the length of the longest equal proper prefix and suffix of the substring pattern[0..i]. Word by word: a prefix starts at the first character; a suffix ends at the last character; proper means it is not allowed to be the whole substring; and equal means this prefix and this suffix are the same string. Without "proper" the answer would always be the whole substring, which carries no information. next[0] = 0, because a single character has no proper prefix.

An example: the substring abab. Its proper prefixes are a / ab / aba, and its proper suffixes are b / ab / bab. The longest one that appears in both lists is ab, length 2, so the last entry of next for abab is 2.

Why does this number let the text pointer i stay where it is? Picture the moment of failure: the first j characters of the pattern have matched the text, and character j + 1 does not. Those j matched characters are both a part of the text and exactly the pattern prefix pattern[0..j−1]. Suppose that prefix has an equal proper prefix and suffix of length k. Then its last k characters are the same as its first k characters. So the pattern can slide right until its first k characters sit where those last k characters were. They are equal by construction, so there is nothing to compare again, and i does not have to move back.

Naive matching
Failure = start over

Mismatch → i returns to the start plus one, j returns to 0. The self-similarity inside the matched prefix is never used.

KMP
Failure = one table lookup

Mismatch → i does not move, j = next[j−1]. How much of the matched suffix equals a prefix decides where j lands, in one step.

Two conventions for next — do not mix them

KMP code on the internet uses two different conventions. One is the prefix function π used here: next[i] is a length, and a mismatch sets j = next[j−1]. The other shifts the array by one and starts with next[0] = −1, so the numbers and the fallback line both look different. Pick one convention and keep the whole file consistent with it. Copying half of one and half of the other will not work. This chapter uses the prefix function π from start to finish, and the solutions to LC 459 and LC 214 are based on it.

§03

Building next: match the pattern against itself

★ Key animation

The hardest and most important part of the chapter — step through every frame

How is the whole table computed? Here is the neatest step in KMP: building the next array is itself a KMP search, with the pattern searched inside the pattern. This is called self-matching.

The method is incremental. Assume next[0..i−1] is already known and compute next[i]. Keep a value j meaning "how long the equal prefix and suffix reached at the previous position", which starts at j = next[i−1]. Then compare the new character pattern[i] with pattern[j]:

  • They match (pattern[i] == pattern[j]): the equal prefix and suffix both grow by one, so j++ and next[i] = j.
  • They do not match (pattern[i] ≠ pattern[j]): the current prefix cannot be extended, so fall back with j = next[j−1] — move to a shorter prefix that may still continue, and try again, until it matches or j reaches 0.

The two-row table below animates it. The top row is the pattern abababca and the bottom row is the next value filled in cell by cell. The solid blue cell is the prefix character pattern[j] being compared, and the dashed blue cells are the prefix matched so far. Pay special attention to i=6, the character c, where the fallback happens several times in a row:

★ Building the next array — the pattern "abababca" matched against itself (solid blue = p[j], the prefix character being compared; dashed blue = the prefix matched so far)
i
pattern
a0
b1
a2
b3
a4
b5
c6
a7
next
0
?
?
?
?
?
?
?
The definition used here: next[i] = the length of the longest equal proper prefix and suffix of the substring p[0..i]. Proper means it cannot be the whole substring. Base case: a single character has no proper prefix, so next[0] = 0. From i=1 on, the pattern is matched against itself.
1 / 18

Understanding the fallback chain at i=6 is half of understanding KMP. c is compared with pattern[4]=a and fails; j falls back to next[3]=2 and c is compared with pattern[2]=a, which fails; j falls back to next[1]=0 and c is compared with pattern[0]=a, which fails again, so next[6]=0. Each jump along the next chain replaces the candidate prefix with a shorter one, and that is exactly how known information is reused instead of recomputed. The construction code and the matching code are almost the same:

prefix_function.py
1# next[i] = length of the longest equal proper prefix and suffix of p[0..i]
2def build_next(p: str) -> list[int]:
3 m = len(p)
4 nxt = [0] * m # nxt[0] = 0
5 j = 0
6 for i in range(1, m):
7 while j > 0 and p[i] != p[j]:
8 j = nxt[j - 1] # mismatch: fall back
9 if p[i] == p[j]:
10 j += 1 # match: one longer
11 nxt[i] = j
12 return nxt
Naming: next is a Python built-in function, so using it as a variable name shadows the built-in. This code uses nxt. Python strings can be indexed directly with p[i].

Why is the construction O(m)? An amortized argument

The outer loop runs m−1 times. The inner while loop looks like it could fall back many times, so count the fallbacks over the whole run instead of per step. j increases by at most 1 in each outer step, and only when the characters match. Each turn of the while loop decreases j by at least 1, because next[j−1] < j always holds. So the total increase of j over the whole run is at most m, and therefore the total decrease is also at most m. The number of inner iterations is bounded by that total, so the whole construction is O(m). The same argument applies to the matching loop, so construction plus matching is O(n + m).

§04

KMP matching: i never moves back

EASY

Worked example A · LC 28 strStr — once the next table exists, matching follows

With the next table in hand, the matching loop looks almost identical to the construction loop. Only the comparison changes, from pattern against pattern to pattern against text. Keep a value j meaning how many pattern characters currently match: compare haystack[i] with pattern[j], and on a match do j++, on a mismatch do j = next[j−1]. i only ever increases. When j reaches m, the pattern has been found. Here is the same pair abaabab and abab again, this time with KMP:

KMP matching — i never moves back; the pattern slides right along next
text
a0
b1
a2
a3
b4
a5
b6
pattern
a0
b1
a2
b3
The same text abaabab and pattern abab, with next = [0, 0, 1, 2]. This time KMP runs. Watch the pointer i: it only moves forward, never back.
1 / 11

The problem (LC 28): return the index of the first occurrence of needle in haystack, or −1 if there is none. Naive: shown above, O(n·m). Why it can be improved: on a mismatch, the suffix that already matched contains a part that is equal to a prefix. The next table measures that in advance, so a mismatch becomes a jump instead of a restart. Solution: build next in O(m), then scan the text in O(n):

lc28_strstr_kmp.py
1class Solution:
2 def strStr(self, haystack: str, needle: str) -> int:
3 n, m = len(haystack), len(needle)
4 if m == 0:
5 return 0
6 nxt = [0] * m # (1) build the next table
7 j = 0
8 for i in range(1, m):
9 while j > 0 and needle[i] != needle[j]:
10 j = nxt[j - 1]
11 if needle[i] == needle[j]:
12 j += 1
13 nxt[i] = j
14 j = 0
15 for i in range(n): # (2) scan the text, i never moves back
16 while j > 0 and haystack[i] != needle[j]:
17 j = nxt[j - 1]
18 if haystack[i] == needle[j]:
19 j += 1
20 if j == m:
21 return i - m + 1
22 return -1
The two j = 0 lines initialize the construction and the matching separately; do not forget to reset j before matching. A slice comparison haystack[i:i+m] == needle also works, but it copies m characters each time and degrades to O(n·m).

In practice: where the KMP family is used

For a single pattern, Boyer-Moore and its variants are the usual choice in tools such as grep and editor search: they compare from the right and can skip further ahead on a bad character. The idea behind KMP is generalised by the Aho-Corasick automaton, which is a trie of all the patterns plus failure links, so one scan of the text matches thousands of patterns at once; its cost is O(total pattern length + text length + number of matches). Antivirus signature databases, intrusion detection rule engines, and word filters are built on it. The shared starting point of all these algorithms is the same: preprocess the pattern so that the scan of the text never moves backwards.

§05

A different route: one number as the fingerprint of a window

Two solutions

Worked example A continued · a second solution to LC 28 — the Rabin-Karp rolling hash

KMP gets its speed from preprocessing the pattern. There is a completely different route: Rabin-Karp, which hashes a substring of length m into a single number, like a fingerprint. To test whether two substrings are equal, compare their hashes first. Different hashes mean the substrings are definitely different, so that position can be skipped. Equal hashes mean the characters have to be compared.

If each window's hash were computed from scratch, the total would still be O(n·m) and nothing would be gained. The point is the rolling hash: when the window moves one step right, the hash updates in O(1) — subtract the contribution of the character that leaves, multiply the rest by the base, and add the character that enters. Watch it:

Rabin-Karp rolling hash — searching for "cab" in "abcab" (base=26, mod=101)
a0
b1
c2
a3
b4
window hash pattern hash 36
Each letter takes a value: a=1, b=2, c=3, and so on. The base is 26 and the modulus is the prime 101, which keeps the numbers small. First hash the pattern cab into one number: ((3·26+1)·26+2) mod 101 = 36. That is the value to look for in the text.
1 / 5

Treat the substring as a polynomial in the base (a polynomial rolling hash): hash = c₀·baseᵐ⁻¹ + c₁·baseᵐ⁻² + … + cₘ₋₁. Here cᵢ is the character code, which may be larger than the base. That is allowed, because this is a hash function and not a positional number system. When the window moves right, drop the highest term, shift the rest up by one power (multiply by the base), and add the new character in the lowest position, so the update is O(1). Everything is taken modulo a large prime to keep the numbers in range. A base at least as large as the alphabet is normal: 26 or 31 for 26 lowercase letters, and something larger such as 131 when the text can contain any ASCII character.

lc28_strstr_rabinkarp.py
1class Solution:
2 def strStr(self, haystack: str, needle: str) -> int:
3 n, m = len(haystack), len(needle)
4 if m == 0:
5 return 0
6 if m > n:
7 return -1
8 MOD, BASE = 10**9 + 7, 26
9 high = pow(BASE, m - 1, MOD) # weight of the top position
10 hp = hh = 0
11 for i in range(m):
12 hp = (hp * BASE + ord(needle[i])) % MOD
13 hh = (hh * BASE + ord(haystack[i])) % MOD
14 for i in range(n - m + 1):
15 if hp == hh and haystack[i:i + m] == needle:
16 return i # hashes agree -> compare
17 if i + m < n:
18 hh = (hh - ord(haystack[i]) * high) % MOD # drop the top
19 hh = (hh * BASE + ord(haystack[i + m])) % MOD # add the new character
20 return -1
Python integers have unlimited precision, so nothing overflows, but the modulus is still used to lower the collision rate and keep the numbers small. A negative value modulo MOD lands in [0, MOD) automatically in Python, so no manual + MOD is needed. pow(b, e, MOD) is modular exponentiation built in.
AlgorithmPreprocessingMatchingSpaceNotes
NaiveO(n·m)O(1)Fine for short strings. Fastest to write, slowest in the worst case.
KMPO(m)O(n)O(m)O(n + m) even in the worst case. Adversarial input does not hurt it.
Rabin-KarpO(m)O(n) expectedO(1)Extends to many patterns easily. Collisions must be verified, and the worst case is O(n·m).

A hash can lie: collisions and the fix

A hash compresses a whole substring into one number, so collisions are always possible: two different substrings can produce the same value. Equal hashes are therefore only a candidate, and the characters must be compared. This is why Rabin-Karp is expected O(n + m) rather than guaranteed O(n + m): if many hashes collide — for example on input built specifically to attack a fixed base and modulus — the verification runs often and the total degrades to O(n·m). In practice double hashing is used: two independent base and modulus pairs, and only a position where both agree is treated as a candidate. That makes a collision unlikely enough to ignore, but the character comparison is still what makes the result correct.

In practice: rolling hashes are everywhere

rsync incremental sync, git finding identical blocks, and cloud storage deduplication all use a rolling hash (the Rabin fingerprint) to cut a file into content-defined blocks, so only the blocks that changed are sent. Plagiarism detection for text and code builds a fingerprint index from hashes of sliding windows. The idea of turning a stretch of content into a fingerprint that can be updated in O(1) reaches well beyond string matching.

§06

One more use for the next array

EASY

Worked example B · LC 459 repeated substring pattern — one line of test, all prefix and suffix underneath

The problem (LC 459): decide whether a string s can be built by repeating one of its substrings two or more times. For example abab = ab×2 → true, and aba → false. Naive: try every candidate period length d that divides n and verify each one, which is about O(n²). Why it can be improved: a repeating structure leaves a clear mark in the next array.

Let n = s.length and k = next[n−1], the length of the longest equal proper prefix and suffix of the whole string. The conclusion:

Test: k > 0 and n % (n − k) == 0

The candidate period length is n − k. Why: if s is a substring of length d repeated t times with t ≥ 2, then the prefix that drops the last copy and the suffix that drops the first copy are the same string, and their length is n − d. That is exactly next[n−1], so d = n − k. It remains to check that d divides n, so the period tiles the string exactly, and that k > 0, so the string really does repeat itself.

Check it on two more examples. abcabcabc: n=9, the last next value is k=6, the candidate period length is 9 − 6 = 3, and 9 % 3 == 0 with 6 > 0 → true, with period abc. Now aba: n=3, k=1, the candidate length is 2, but 3 % 2 ≠ 0 → false. The test is exact:

lc459_repeated_substring.py
1class Solution:
2 def repeatedSubstringPattern(self, s: str) -> bool:
3 n = len(s)
4 nxt = [0] * n
5 j = 0
6 for i in range(1, n): # build the next table
7 while j > 0 and s[i] != s[j]:
8 j = nxt[j - 1]
9 if s[i] == s[j]:
10 j += 1
11 nxt[i] = j
12 k = nxt[n - 1]
13 return k > 0 and n % (n - k) == 0
One-line alternative: return s in (s + s)[1:-1] — join two copies of s and remove the first and last character; if s can still be found, it is built from a repeated substring. It is elegant, but the substring search is O(n²) in the worst case unless the runtime uses a linear algorithm internally.

Interview follow-up: be able to explain both solutions

In interviews LC 459 often follows LC 28. A safe answer: "I can use the one-line (s+s) version with the first and last character removed; it works because every rotation of s lives inside the doubled string. But it relies on the library substring search, which is O(n²) in the worst case. For a guaranteed O(n) I use the next array: k = next[n−1], and the test is k > 0 and n % (n−k) == 0, where n − k is the period length." Presenting both the short solution and the one with a guaranteed bound scores best.

§07

Palindromes: mirror outwards from the center

MEDIUM

Worked example C · LC 5 longest palindromic substring (review) plus the idea of Manacher

A different kind of structure: a palindrome reads the same forwards and backwards, such as aba or abba. The problem (LC 5): find the longest palindromic substring of a string. Naive: take all O(n²) substrings and check each one, at O(n) per check, for O(n³) in total. Why it can be improved: a palindrome is symmetric around its center. Instead of choosing the two ends and then checking, choose the center and expand outwards, which keeps the symmetry true by construction.

The DataData two-pointer chapter introduced the intuition for expanding from a center; this is a review that then connects to Manacher. The one thing to watch is that palindromes have odd and even lengths: the center of aba is a character, while the center of abba is the gap between two characters. So there are 2n−1 centers to try: n characters and n−1 gaps. Watch:

LC 5 expand from center — mirror outwards from the center (green = current palindrome, red = characters that differ, so it stops)
l=r
b0
a1
b2
a3
d4
Find the longest palindromic substring of babad. Instead of enumerating the two ends, enumerate the center and expand outwards. Start with an odd length center, which sits on a character: index 2, the b.
1 / 6

There is a classic off-by-one in the implementation. When the expansion loop exits, the left and right pointers have each moved one step too far and stand on the pair that did not match. So the palindrome is the range [l+1, r−1] and its length is r−l−1. Keep that in mind and the code follows:

lc5_longest_palindrome.py
1class Solution:
2 def longestPalindrome(self, s: str) -> str:
3 def expand(l: int, r: int) -> str:
4 while l >= 0 and r < len(s) and s[l] == s[r]:
5 l -= 1
6 r += 1
7 return s[l + 1:r] # the palindrome is [l+1, r-1]
8
9 best = ""
10 for i in range(len(s)):
11 best = max(best, expand(i, i), expand(i, i + 1), key=len)
12 return best
expand returns the palindrome itself, so max(..., key=len) picks the longest of the three candidates in one line. The slice s[l+1:r] matches [l+1, r−1] exactly, because the right end of a Python slice is excluded.

Manacher: from O(n²) to O(n) (the idea only)

Expanding from neighboring centers re-examines the same region many times. The insight in Manacher is this: if the current center lies inside a palindrome that is already known, then its radius can be read off from the mirror position on the other side of that palindrome's center, which skips a large amount of repeated expansion. This is the same spirit as KMP, where known information replaces recomputation. The algorithm also needs the interleaved separator trick — rewriting abba as #a#b#b#a# — so that every palindrome in the new string has odd length and the odd and even cases become one case. Without that step the implementation has to handle two kinds of center and the linear bound is harder to state. With it, the total is O(n). Interviews almost never ask for a written Manacher, but knowing that it exists and that it saves work through symmetry is worth having. For most inputs the O(n²) expansion is fast enough.

The family of palindrome problems

Palindrome problems split into two lines. One is the contiguous substring, which is this problem: expand from center, Manacher, or interval DP. The other is the subsequence, such as LC 516 longest palindromic subsequence, which chapter 9 solved as the LCS of s and reverse(s) and chapter 10 revisited with interval DP. LC 214 shortest palindrome joins palindromes to KMP directly: build s + '#' + reverse(s) and read the last value of its next array, which is the length of the longest palindromic prefix of s. A palindrome is a string that is self-similar with its own reverse, and that view connects most of this chapter.

§08

Parsing: turn vague rules into exact logic

LC 205 isomorphic strings · LC 8 string to integer — no tricks, only care

Not every string problem is about matching. The other large group is parsing and mapping, where the test is not an algorithmic trick but translating a set of boundary rules into code without missing any and without duplicating any. Interviewers like these problems because they are close to real work: turning an unclear requirement into exact logic.

LC 205 isomorphic strings: can s become t by replacing characters one for one? egg → add works; foo → bar does not, because o would have to become both a and r. The key is a mapping in both directions: s → t must be consistent and t → s must be consistent too. Checking only one direction accepts the case where two different characters land on the same target:

lc205_isomorphic.py
1class Solution:
2 def isIsomorphic(self, s: str, t: str) -> bool:
3 m1, m2 = {}, {}
4 for a, b in zip(s, t):
5 if m1.get(a, b) != b or m2.get(b, a) != a:
6 return False # a conflict in either direction rejects
7 m1[a], m2[b] = b, a
8 return True
zip(s, t) walks both strings together. The default in m1.get(a, b) is b, so on the first appearance the test is b != b, which is always false and lets the character through. That removes the need for an if a in m1 branch.

LC 8 string to integer (atoi): parse text that may contain leading spaces, a sign, and invalid characters into a 32-bit integer. There is no algorithm here, only rules. Writing them as a state machine makes it clear: (1) skip leading spaces, (2) read one optional sign, (3) read digits, (4) stop at the first non-digit, (5) clamp to [INT_MIN, INT_MAX] on overflow:

lc8_atoi.py
1class Solution:
2 def myAtoi(self, s: str) -> int:
3 i, n = 0, len(s)
4 while i < n and s[i] == ' ': # (1) skip spaces
5 i += 1
6 if i == n:
7 return 0
8 sign = 1
9 if s[i] in '+-': # (2) sign
10 sign = -1 if s[i] == '-' else 1
11 i += 1
12 ans = 0
13 while i < n and s[i].isdigit(): # (3) digits
14 ans = ans * 10 + int(s[i])
15 i += 1
16 ans *= sign
17 INT_MIN, INT_MAX = -2**31, 2**31 - 1 # (5) clamp once at the end
18 return max(INT_MIN, min(INT_MAX, ans))
Python integers have unlimited precision, so the whole value can be computed first and clamped at the end; no per-digit overflow check is needed. s[i].isdigit() tests for a digit and s[i] in '+-' tests for a sign. Note that isdigit() is also true for some non-ASCII digit characters, which does not matter for this problem's input but does matter in real parsing code.

The recurring problem with parsing: the edge cases are the exam

Almost all the credit in atoi is in the edge cases: empty string, only spaces, a sign with no digits after it, leading zeros, overflow in both directions, and letters after the digits. List them as a checklist before writing and test them one by one. In a parsing problem, getting the main path right is not close to enough; a single missed edge case is a wrong answer. LC 205 is the same: reject different lengths first, and note that two empty strings are isomorphic.

§09

Problem set: 9 string algorithm problems

Core + extra

Grouped as KMP, uses of next, palindromes, and parsing, from easier to harder. Think for 30 seconds before opening the hint

§10

Chapter quiz

✎ Chapter quiz

Answer all 8 correctly to mark this chapter complete

QUESTION 01 / 8

Naive string matching costs O(n·m) in the worst case. Where exactly is the work wasted?

QUESTION 02 / 8

In the prefix function (the next array), what is next[i] exactly? (This chapter defines next[i] over the substring s[0..i].)

QUESTION 03 / 8

With next[i] = the length of the longest equal proper prefix and suffix of s[0..i], compute next[4] (the last entry) for the pattern "aabaa".

QUESTION 04 / 8

KMP is scanning haystack and hits a mismatch (haystack[i] ≠ pattern[j], with j > 0). What is the correct action?

QUESTION 05 / 8

LC 459 asks whether a string s of length n is a substring repeated. Using k = next[n−1], which test is correct?

QUESTION 06 / 8

Which statements about the Rabin-Karp rolling hash are true? (Select all.)

QUESTION 07 / 8

When you find the longest palindromic substring by expanding from centers, why are there 2n−1 centers instead of n?

QUESTION 08 / 8

For LC 205, checking only the one-way map s[i] → t[i] gives the wrong answer on which kind of input?

What to take away from this chapter
  • The idea behind the whole chapter: keep what each failure told you and use it next time. The flaw in naive matching is that a mismatch discards the information from the prefix that did match, and moves the text pointer back.
  • next[i] = the length of the longest equal proper prefix and suffix of pattern[0..i]. Proper means it cannot be the whole substring. This value depends only on the pattern, so it can be computed before the search starts, and that is why KMP reaches O(n + m). It is built by matching the pattern against itself, falling back with j = next[j−1] on a mismatch.
  • KMP matching: the text pointer i never moves back, only j does. Build in O(m) plus scan in O(n) gives O(n + m), and the bound is amortized: j rises by at most 1 per outer step and each fallback lowers it by at least 1. This holds in the worst case, so adversarial input does not hurt it.
  • Rabin-Karp rolling hash: hash the window into one number and update it in O(1) when the window moves (drop the top term, multiply by the base, add the new character), all modulo a large prime. Equal hashes are only a candidate, so the characters must be compared. That makes it expected O(n + m); with many collisions the worst case is O(n·m). Double hashing lowers the collision rate in practice.
  • Uses of the next array: LC 459 tests for a period (k = next[n−1], then k > 0 and n % (n−k) == 0), LC 1392 just reads the last value, and LC 214 finds the longest palindromic prefix on s + # + reverse(s). When a problem mentions repetition or self-similarity, think of next.
  • Palindromes rest on symmetry around a center: try all 2n−1 centers, both the n characters and the n−1 gaps, and expand outwards, for O(n²). When the expansion stops, the palindrome is [l+1, r−1] — the off-by-one to remember. Manacher reaches O(n) by reusing symmetry, together with the separator trick that makes every palindrome odd-length; knowing the idea is enough.
  • Parsing problems such as LC 205 and LC 8 test no trick. They test translating vague rules into exact logic with nothing missed and nothing duplicated: isomorphic strings need a mapping in both directions, and atoi is a state machine plus an edge-case checklist. The edge cases are the exam.