CS Foundations
String Searching: Naive, KMP, Rabin-Karp and Z
The naive worst case, KMP with a traced failure function, Rabin-Karp collisions, the Z-array and a tiny Aho-Corasick, with local timings.
On this page
- Words used in this lesson
- The naive method and its worst case
- KMP: never move backwards in the text
- Rabin-Karp: compare numbers instead of strings
- The Z-array: how a string matches itself
- Tested against the built-in
- Timing, labelled as local
- When the built-in is enough
- Many patterns at once: Aho-Corasick
- Choosing the algorithm
- Common mistakes
- Try it
- Interview questions
- Key takeaways
- What's next
In one line: finding a short pattern inside a long text looks like it needs a loop inside a loop, but KMP, Rabin-Karp and the Z-array each do it in time proportional to the text length. In real code, call the built-in includes or indexOf. Learn these three so you can explain why the naive method can be slow and what to reach for when the built-in is not enough.
Lesson 6 ended with a problem that has no known fast exact answer. This one is the opposite. "Does the word needle appear in this text?" has several fast, exact algorithms, and interviewers like it because each one teaches a different idea: reuse what you already matched (KMP), compare small numbers instead of strings (Rabin-Karp), and precompute how a string matches itself (Z).
You should know loops and strings first: DSA Lesson 9, Strings, DSA Lesson 30, String Patterns and DSA Lesson 31, Substrings and Windows (the sliding window is a close cousin of the rolling hash below). Big-O is in DSA Lesson 12. The trie, which is the base for multi-pattern search, is in DSA Lesson 58, and string dynamic programming (edit distance and friends, a different kind of "matching") is in DSA Lesson 57.
What I ran / checked. All code was run on Node v22.22.0, Linux. I wrote the naive search, KMP, Rabin-Karp, the Z-array search and a small Aho-Corasick, and tested each against the built-in String.prototype.indexOf on 20,000 random small cases (counts shown below). The traces and outputs are pasted from those runs. Timings are from one machine, best of three runs, so they show the shape and not exact values. The standard facts (KMP and Z run in O(n + m) time, Rabin-Karp is O(n + m) on average, Aho-Corasick finds all matches of many patterns in one pass) are textbook results that I wrote from memory and did not re-check against a source here. I did not read how V8 implements indexOf; the speed I report is only what I measured.
Words used in this lesson
- Text is the long string we search in. Pattern is the short string we look for. I write
nfor the text length andmfor the pattern length. - Match means the pattern appears in the text starting at some position (an index, counting from 0).
- Prefix of a string is its first few characters (
abis a prefix ofabc). Suffix is its last few characters (bc). - Border of a string is a piece that is both a prefix and a suffix, and is shorter than the whole string.
abahas the bordera.ababahas the bordersaandaba. - Failure function (also called the prefix function or LPS table) is a list that says, for each position of the pattern, the length of the longest border of the pattern up to that position. KMP uses it.
- Hash is a small number computed from a string. Equal strings always have equal hashes. Different strings can have the same hash, which is called a collision.
- Rolling hash is a hash of a window that you update in constant time when the window moves one step.
- False positive means the hashes are equal but the strings are not.
- Z-array is a list where
z[i]is the length of the longest piece starting atithat is also a prefix of the whole string. - Automaton here means a machine with states and arrows labelled by characters. Aho-Corasick is one.
The naive method and its worst case
Put the pattern at position 0 of the text and compare character by character. On a mismatch, slide the pattern one step to the right and start again.
function naive(t, p) { const n = t.length, m = p.length; if (m === 0) return 0; for (let i = 0; i + m <= n; i++) { let j = 0; while (j < m && t[i + j] === p[j]) j++; if (j === m) return i; } return -1;}There are up to n - m + 1 start positions and each can compare up to m characters, so the worst case is about n x m comparisons. On typical text it is fast, because most start positions fail on the first character. The bad input is text and pattern made of almost the same letter:
text = "aaaa...a" (n letters) pattern = "aaa...ab" (m letters, m about n/2)At every start position the naive method compares m - 1 letters that match, then fails on the last b. Then it throws that work away and starts again one step later. I counted the character comparisons for the naive search and for KMP (below):
n 1000 m 500 naive compares 250500 kmp compares 1501n 10000 m 5000 naive compares 25005000 kmp compares 15001n 100000 m 50000 naive compares 2500050000 kmp compares 150001Ten times more text gives a hundred times more naive work (that is n x m, which is quadratic when m is half of n). KMP stays at about 1.5 n. The idea of KMP is the answer to the question "why throw the work away?".
KMP: never move backwards in the text
When the pattern ababaca has matched ababa and then fails, we do not know only that there is a mismatch. We know the last five text characters are ababa. The longest piece of that which is also a prefix of the pattern is aba (a border of ababa). So we can continue as if we had just matched aba, without looking at the text again.
KMP precomputes that answer for every length. The failure function f[i] is the length of the longest border of pattern[0..i]:
function failure(p) { const f = new Array(p.length).fill(0); let k = 0; // length of the current border for (let i = 1; i < p.length; i++) { while (k > 0 && p[i] !== p[k]) k = f[k - 1]; // fall back to a shorter border if (p[i] === p[k]) k++; f[i] = k; } return f;}console.log(failure("ababaca").join(" "));console.log(failure("aabaaab").join(" "));0 0 1 2 3 0 10 1 0 1 2 2 3Read the first line by hand to be sure you see it:
| i | pattern[0..i] | longest border | f[i] |
|---|---|---|---|
| 0 | a | none | 0 |
| 1 | ab | none | 0 |
| 2 | aba | a | 1 |
| 3 | abab | ab | 2 |
| 4 | ababa | aba | 3 |
| 5 | ababac | none (ends in c) | 0 |
| 6 | ababaca | a | 1 |
The search runs the same idea over the text. k is "how many pattern characters are matched right now". On a mismatch we do not move back in the text; we only reduce k to f[k - 1]:
function kmp(t, p) { const m = p.length; if (m === 0) return 0; const f = failure(p); let k = 0; for (let i = 0; i < t.length; i++) { while (k > 0 && t[i] !== p[k]) k = f[k - 1]; if (t[i] === p[k]) k++; if (k === m) return i - m + 1; // match ends at i } return -1;}Here is a trace for text abababaca and pattern ababaca. I added a print line to the loop above:
i=0 t[i]=a k=0 => k=1i=1 t[i]=b k=1 => k=2i=2 t[i]=a k=2 => k=3i=3 t[i]=b k=3 => k=4i=4 t[i]=a k=4 => k=5i=5 t[i]=b k=5 -> fall to 3 => k=4i=6 t[i]=a k=4 => k=5i=7 t[i]=c k=5 => k=6i=8 t[i]=a k=6 => k=7match starts at 2indexOf 2The key line is i=5. We had matched ababa (k = 5) and the text has b, but the pattern wants c. The naive method would go back to text position 1. KMP looks up f[4] = 3, keeps the last three matched characters aba, and sees that b matches the next pattern character b, so k becomes 4. The text index i only ever went forward. The answer, 2, equals indexOf.
Why it is linear. Each step either raises k by one or lowers it. k can go up only once per text character, so in total it can come down at most n times. The inner while loop therefore runs at most n times over the whole search. The failure function has the same argument, so building it is O(m). Total: O(n + m), and extra memory O(m).
The most common KMP bug is the fall-back. On a mismatch, use k = f[k - 1], not k = 0 and not k = f[k]. Remember that f stores a length, so the value for the longest matched prefix is at index k - 1.
Rabin-Karp: compare numbers instead of strings
Idea: compute a hash of the pattern. Slide a window of length m along the text, keep a hash of the window, and only when the two hashes are equal, compare the actual characters.
The trick that makes it fast is the rolling hash. Treat a string as a number in some base (here 257, bigger than any character code), then take the remainder after dividing by a modulus (a big prime, to keep the number small):
hash("abc") = ((97 * B + 98) * B + 99) mod M (B = base, M = modulus)When the window moves one step, remove the old first character and add a new last one. In constant time:
new = ((old - first * B^(m-1)) * B + next) mod Mfunction rk(t, p, mod = 1000003, base = 257, stats) { const n = t.length, m = p.length; if (m === 0) return 0; if (m > n) return -1; let hp = 0, ht = 0, pow = 1; // pow = base^(m-1) mod mod for (let i = 0; i < m; i++) { hp = (hp * base + p.charCodeAt(i)) % mod; ht = (ht * base + t.charCodeAt(i)) % mod; if (i > 0) pow = (pow * base) % mod; } for (let i = 0; ; i++) { if (hp === ht) { // hashes agree: verify for real if (stats) stats.checks++; if (t.startsWith(p, i)) return i; if (stats) stats.falsePositives++; } if (i + m >= n) return -1; ht = ((ht - t.charCodeAt(i) * pow % mod + mod) * base + t.charCodeAt(i + m)) % mod; }}(The + mod keeps the number from going negative, because JavaScript's % can return a negative value. With mod near a million and base 257, every product stays well inside the exact-integer range of a JavaScript number.)
Collisions, shown on purpose
Equal strings always give equal hashes, but the reverse can fail. With a small modulus it is easy to find two different strings with the same hash. I searched all two-letter pairs with modulus 101 and base 257:
collision mod 101 base 257 [ 'aa', 'ht', 79 ]So aa and ht have the same hash, 79. Search for ht in xxaayyzz with modulus 101:
-1 { checks: 1, falsePositives: 1 }The hash matched once (at aa), the character check said no, and the answer -1 is still correct. This is why the verification step is not optional: the hash only filters. With it, the result is always right and the hash only affects speed. How often does the filter let a wrong window through? I searched a 6-letter pattern in 200,000 random lowercase letters:
random text 200000, mod 101 false positives 2225random text 200000, mod 1009 false positives 266random text 200000, mod 1000003 false positives 0With a tiny modulus about 1 window in 90 was a false alarm; with a modulus near a million, none appeared. (Rough rule: with a modulus M, a random wrong window passes about 1 time in M. This matches the numbers: 200,000 / 101 is about 1,980 and I saw 2,225.) False positives cost m extra comparisons each, so the average time is O(n + m) with a large modulus. The worst case, with many collisions, goes back to O(n x m). Attackers can target a fixed, known hash with chosen inputs, so in code that handles untrusted input use a random base or modulus.
Where Rabin-Karp is the better tool
- Many patterns of the same length. Put all pattern hashes in a
Set, slide one window over the text, and check each window hash against the set. One pass for all patterns. - Plagiarism and duplicate detection, comparing many chunks of text.
- Problems where you compare a window with many things, for example "find a repeated substring of length k" (hash every window of length k; a repeated hash means a probable repeat, then verify). Binary search on
kwith this idea solves "longest repeated substring" and "longest duplicate substring" problems in near-linear time per length. - A 2-D version (hash a block of a grid) exists as well.
The Z-array: how a string matches itself
z[i] is the length of the longest piece that starts at i and is also a prefix of the string. By convention z[0] is the whole length.
function zArray(s) { const n = s.length, z = new Array(n).fill(0); z[0] = n; let l = 0, r = 0; // [l, r) is the right-most match window found so far for (let i = 1; i < n; i++) { if (i < r) z[i] = Math.min(r - i, z[i - l]); // reuse an earlier answer while (i + z[i] < n && s[z[i]] === s[i + z[i]]) z[i]++; if (i + z[i] > r) { l = i; r = i + z[i]; } } return z;}console.log(zArray("aabxaab").join(" "));console.log(zArray("aaaaa").join(" "));7 1 0 0 3 1 05 4 3 2 1Check the first line: at index 4 the string continues aab, which equals the prefix aab, so z[4] = 3. At index 1 it continues abxaab; only a matches the prefix aab..., so z[1] = 1.
How it stays linear: the window [l, r) is a piece of text already known to equal a prefix. If i is inside it, the answer for i is related to the answer at i - l, so we start from a known lower bound and only extend by comparing new characters. r never moves left, so the total number of new comparisons is at most n.
Pattern search with Z. Join pattern + separator + text, where the separator is a character that appears in neither. Any position with z[i] = m is a match, and the separator stops a match from running over the border:
function zSearch(t, p) { const m = p.length; if (m === 0) return 0; const z = zArray(p + "\u0000" + t); for (let i = m + 1; i < z.length; i++) if (z[i] === m) return i - m - 1; return -1;}The Z-array and the KMP failure function carry the same information about a string and can be converted into each other. Pick the one you can write from memory without a bug. Z is often easier to explain; KMP uses less extra memory for a streaming text because it needs only the pattern's table and not a joined string.
Other uses of the Z-array: count how many times each prefix appears, find the smallest period of a string (the shortest p where the string is p repeated), and check if one string is a rotation of another (although for that one, (s + s).includes(t) is the short answer; see DSA Lesson 30).
Tested against the built-in
Writing a string algorithm without a test is asking for an off-by-one. The built-in indexOf is the oracle. I generated 20,000 random cases: an alphabet of 1 to 3 letters (small alphabets create many partial matches, which is where bugs show), a text of 0 to 29 letters and a pattern of 0 to 5 letters, including the empty pattern and a pattern longer than the text. All five searches had to return exactly what indexOf returns:
cases 20000 disagreements {"naive":0,"kmp":0,"rk":0,"rkBig":0,"z":0}(rk is the version above; rkBig is a second version I wrote with BigInt and a modulus of 1,000,000,007. All five agreed in every case.) The edge cases I fixed on purpose: the empty pattern returns 0, as "abc".indexOf("") does, and an empty text with a non-empty pattern returns -1:
0 0 -1 -1Timing, labelled as local
These numbers are from my machine only, best of three runs, with Node v22.22.0. The rk, kmp, naive and zSearch functions are the ones above, running as normal JavaScript.
Random lowercase text, 2,000,000 letters, a 13-letter pattern that is not present:
indexOf 0.9 msnaive 14.0 mskmp 13.9 msrabin-karp 13.1 msz 28.9 msOn ordinary text the naive method is as fast as KMP, because it almost always fails on the first letter. The built-in is about fifteen times faster than any of my JavaScript versions. The Z version is slower because it builds a joined string and a big array.
Bad case: text of 200,000 letters a, pattern of 2,000 letters a followed by b:
aaaa 200k indexOf 0.9 msaaaa 200k naive 2674.2 msaaaa 200k kmp 4.4 msaaaa 200k rabin-karp 1.9 msNaive takes 2.7 seconds; KMP takes 4 milliseconds. Rabin-Karp is fast here because the pattern's hash differs from every window's hash, so it never verifies. (Rabin-Karp's own bad case needs many hash collisions, which this input does not produce.) The built-in indexOf was fast on this input too. I do not know which algorithm V8 uses inside, and I did not read its source, so I cannot explain why; I only report the measurement.
When the built-in is enough
For almost all application code, use the built-ins:
| Need | Use |
|---|---|
| Does the text contain this word? | text.includes(word) |
| Where is the first match? | text.indexOf(word) (returns -1 if none) |
| All start positions | loop indexOf(word, from), or text.matchAll(regex) |
| Starts or ends with | startsWith, endsWith |
| A pattern with wildcards or classes | a regular expression (see the warning below) |
indexOf was 15 times faster than my hand-written code on ordinary text and did not slow down on my bad input. Writing KMP by hand in a product is rarely worth it. Write it when:
- It is an interview or a learning exercise. They want to see that you know why naive can be quadratic.
- You work on a stream (bytes arriving in pieces) and cannot keep the whole text. KMP keeps just
k, so you can feed it one character at a time. - You need a result the built-in does not give: the border or period structure, counts for every prefix, or matches in something that is not a string (an array of numbers, a DNA code in a typed array).
- You search many patterns (next section).
- You compare windows in a problem like "longest repeated substring" and need the rolling hash.
A regular expression is not guaranteed linear. Some regex patterns with nested repeats can take exponential time on certain inputs in backtracking engines such as JavaScript's. If the pattern comes from a user, either escape it and use includes, or use an engine that guarantees linear time. I did not test this here; I mention it so you do not read "regex" as a safe replacement for these algorithms.
Strings are not always "characters". JavaScript strings are sequences of 16-bit units (see Lesson 3, Unicode and UTF-8). All the code above compares those units. That is correct for finding an exact byte-for-byte piece, but an emoji uses two units, so the position you get is a unit index and not a "letter" count. Case-insensitive search and accented letters need normalisation first.
Many patterns at once: Aho-Corasick
If you must find k patterns, running a single-pattern search k times costs about k passes over the text. For a list of banned words or a dictionary of 10,000 keywords that is too slow. The standard answer is Aho-Corasick, which is KMP generalised to a trie (a tree of letters, see DSA Lesson 58):
- Put all patterns in a trie.
- For each trie node, compute a failure link: the node for the longest proper suffix of this node's text that is also a prefix of some pattern. This is the KMP failure function, but on a tree. A breadth-first search computes all links.
- Walk the text once, one character at a time, following child links, and falling back along failure links on a mismatch. At each node, report all patterns that end there (a node also reports the patterns reached through its failure links).
Time is about the text length plus the pattern lengths plus the number of matches. I wrote a short version (about 20 lines) and ran the classic example:
const words = ["he", "she", "his", "hers"];const ac = buildAC(words);console.log(searchAC(ac, "ushers").map(([i, w]) => `${words[w]} ends at ${i}`).join("; "));she ends at 3; he ends at 3; hers ends at 5In ushers, she and its suffix he end at the same place, which is why a failure link must also report the shorter words. I compared it with a loop that calls indexOf for every word, on 3,000 random cases (alphabet ab, 1 to 4 words, text up to 24 letters):
AC vs indexOf loop, 3000 random cases, mismatches: 0You rarely write this in an interview. Know the name, what it does, and the one-line reason it works (KMP on a trie). In real systems, grep -F -f patterns.txt style tools use this family of algorithms (from memory, not checked), and Rabin-Karp with a Set of hashes is the quick alternative when all patterns have the same length.
Choosing the algorithm
| Situation | Choice | Time |
|---|---|---|
| Ordinary application code | includes / indexOf | built-in |
| Interview: one pattern, worst case matters | KMP or Z | O(n + m) |
| Many patterns of the same length, or window comparison | Rabin-Karp | O(n + m) average |
| Many patterns of different lengths | Aho-Corasick | O(n + total pattern length + matches) |
| Stream of characters | KMP (keeps only k) | O(1) per character |
| Short text, short pattern | naive; it is simple and fast | O(n x m) worst |
| Need periods, prefix counts, "string vs itself" | Z-array or failure function | O(n) |
Common mistakes
- Using
k = 0on a KMP mismatch. That throws away the border and makes the search wrong or slow. Usek = f[k - 1]. - Skipping verification in Rabin-Karp. Equal hashes are not equal strings; I showed a real collision (
aaandht). - Using a negative number in the rolling update. Add the modulus before taking
%. - Forgetting the separator in the Z search. Without it, a match can run across the pattern/text border.
- Handling the empty pattern wrongly.
indexOf("")is0. Decide what you return and test it. - Writing your own search in production code where
includeswould do. - Testing only on random text. Naive and KMP looked identical there. Test with a one- or two-letter alphabet.
Try it
- Compute
failure("abcabcd")by hand, then run it to check. - Use
zArrayto find the smallest period of"abcabcabc". (Hint: the smallestiwherei + z[i] === nandn % i === 0.) - Write
findAll(t, p)using KMP that returns all start positions (including overlapping ones:"aaaa"and"aa"give0, 1, 2). After a full match, setk = f[m - 1]and continue. - Make Rabin-Karp fail its filter on purpose with modulus 101: count false positives on 100,000 random letters.
- Check your
findAllagainst a loop overindexOf(p, i + 1)on thousands of random strings over the alphabetab.
Answers.
0 0 0 1 2 3 0(I ran it). The border grows witha, ab, abc, thendbreaks it.- The Z-array of
abcabcabcis9 0 0 6 0 0 3 0 0. Ati = 3:3 + 6 = 9 = nand9 % 3 = 0, so the period is 3 (the string isabcthree times). I ranzArrayon it and printed exactly that list. - After a full match, record
i - m + 1, then dok = f[k - 1]so that overlapping matches are found. - Expect about one false positive per 100 windows, as in the numbers above (2,225 in 200,000 for my 6-letter pattern).
- They should agree. If not, check the case when the pattern is empty (an infinite loop is a classic bug).
Interview questions
What is the time complexity of the naive substring search, and what is the bad input? (Must know)
O(n x m) in the worst case, where n is the text length and m the pattern length. The bad input is almost-matching text, like all a letters with the pattern aaa...ab: each start position matches almost the whole pattern before it fails. In my run with n = 100,000 and m = 50,000 the naive method did about 2.5 billion comparisons and KMP did 150,001. On ordinary text the naive method is fine.
Explain KMP in a few sentences. (Must know)
KMP never moves backwards in the text. It precomputes, for each prefix of the pattern, the length of its longest border (a piece that is both prefix and suffix). When a mismatch happens after k matched characters, the last k text characters equal the pattern's first k, so I can continue as if I had matched f[k - 1] characters, and I do not re-read any text. Each step raises k by at most one, so the falls total at most n and the whole thing is O(n + m).
How do you build the failure function? (Must know)
I walk the pattern with a running border length k. For each i, while k > 0 and p[i] differs from p[k], I set k = f[k - 1]. If they are equal, k++. Then f[i] = k. It is KMP run on the pattern against itself. For ababaca I got 0 0 1 2 3 0 1.
In KMP, what is the bug when you write k = 0 on mismatch? (Trap)
The search still looks reasonable, but it throws away a border that may continue the match. For pattern ababaca and text abababaca, after matching ababa and meeting a b, the right move is to keep aba (k = 3 then 4). Resetting to 0 would miss the match starting at index 2. It is a common bug, so I test with a small alphabet against indexOf.
How does the rolling hash work, and why is it constant time per step? (Must know)
I treat the window as a number in some base, modulo a prime. Moving the window one step removes the first character times base^(m-1), multiplies by the base, and adds the new character. That is a fixed number of operations, whatever m is. I precompute base^(m-1). I also add the modulus before % so the value does not go negative.
Rabin-Karp hashes can collide. How is the result still correct? (Must know)
When the hashes match, I compare the real characters. The hash is only a filter. A collision costs one extra O(m) check and can never produce a wrong answer. I showed a real collision with modulus 101: aa and ht both hash to 79, and the false alarm was caught by the check. With a modulus near a million I saw 0 false positives in 200,000 random windows, against 2,225 with modulus 101.
What is the worst case of Rabin-Karp? (Senior signal)
O(n x m): if almost every window collides with the pattern's hash, I verify every window. With a good random modulus or base this is unlikely on normal data. Someone who knows my fixed base and modulus can build input that collides on purpose, so for untrusted input I would pick them at random at startup. I also say that the average case O(n + m) depends on the hash behaving like a random function.
What is the Z-array and how do you use it to search? (Senior signal)
z[i] is the length of the longest string starting at i that is also a prefix of the whole string. To search, I build pattern + separator + text, compute the Z-array, and report every index where z[i] = m. The separator is a character that is in neither string, so a match cannot cross the border. It is linear because I keep a window [l, r) that is known to equal a prefix and reuse earlier answers inside it.
KMP or Z: which would you pick? (Senior signal)
They carry the same information, and I would pick the one I can write correctly. For me Z is easier to explain. KMP is better for streaming text, because I keep only the failure table and the current k, and I can feed it one character at a time. Z on pattern + separator + text needs the whole text.
When would you not hand-write any of these? (Must know)
Almost always in application code. includes and indexOf are built in and tested. In my local run indexOf took 0.9 ms on a 2-million-letter text where my JavaScript versions took 13 to 29 ms, and it was also fast on the input that made my naive code take 2.7 seconds. I write my own when I need streaming, a different data type, many patterns, a period or prefix structure, or when it is an interview.
How do you search for many patterns at once? (Senior signal)
For many patterns of the same length I use Rabin-Karp: put the pattern hashes in a set, slide one window, and verify on a hit. For different lengths I use Aho-Corasick: build a trie of the patterns, add failure links with a breadth-first search (the KMP idea on a tree), then read the text once. In ushers, with he, she, his, hers, it reported she and he ending at index 3 and hers at index 5. I tested my version against an indexOf loop on 3,000 random cases with no mismatch. I would not write it in an interview unless asked; naming it and explaining the idea is usually enough.
Does a regular expression replace these algorithms? (Trap)
No. A regex is the right tool for patterns with wildcards or classes, but I cannot assume it runs in linear time. Backtracking engines can take exponential time on some patterns and inputs, so if the pattern comes from a user I escape it and use includes, or I use a linear-time engine. For a plain fixed string, indexOf is simpler and faster.
Your search returns a wrong index for text containing emoji. Why? (Trap)
JavaScript strings are sequences of 16-bit units. An emoji outside the basic range uses two units, so an index counts units and not visible characters. The algorithms are still right for exact unit-by-unit matching; the mistake is treating the index as a "letter number". If I need code points I convert with Array.from(text) first, or use Intl.Segmenter for visible characters. The details are in Lesson 3.
Key takeaways
- Naive search is O(n x m) in the worst case (almost-matching text) and fine on ordinary text.
- KMP precomputes the longest border of each pattern prefix, and never moves back in the text: O(n + m). On a mismatch use
k = f[k - 1]. - Rabin-Karp compares rolling hashes and verifies on equal hashes. Collisions are real (
aaandhtwith modulus 101), so verification is required. It shines for many same-length patterns and window comparison. - The Z-array gives, for each position, the match length with the string's own prefix. Search with
pattern + separator + text. - Use
includesandindexOfin real code. Hand-write only for streaming, other data types, many patterns, string structure, or an interview. - For many patterns of different lengths, name Aho-Corasick: KMP on a trie.
- Test every string algorithm against the built-in on many random inputs with a tiny alphabet.
What's next
This was the last lesson in the series so far, so there is no Lesson 8 yet. To go further, practise the DSA string lessons: DSA Lesson 30, String Patterns, DSA Lesson 31, Substrings and Windows and DSA Lesson 57, DP on Strings. A good exercise: take the kmp function above and turn it into findAll that returns overlapping matches, then check it against a loop over indexOf.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related series