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.

Lesson 0714 min read#algorithms#strings#interviews

On this page

  1. Words used in this lesson
  2. The naive method and its worst case
  3. KMP: never move backwards in the text
  4. Rabin-Karp: compare numbers instead of strings
  5. The Z-array: how a string matches itself
  6. Tested against the built-in
  7. Timing, labelled as local
  8. When the built-in is enough
  9. Many patterns at once: Aho-Corasick
  10. Choosing the algorithm
  11. Common mistakes
  12. Try it
  13. Interview questions
  14. Key takeaways
  15. 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 n for the text length and m for 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 (ab is a prefix of abc). 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. aba has the border a. ababa has the borders a and aba.
  • 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 at i that 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.

JavaScript
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
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):

TEXT
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 150001

Ten 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]:

JavaScript
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(" "));
TEXT
0 0 1 2 3 0 10 1 0 1 2 2 3

Read the first line by hand to be sure you see it:

ipattern[0..i]longest borderf[i]
0anone0
1abnone0
2abaa1
3ababab2
4ababaaba3
5ababacnone (ends in c)0
6ababacaa1

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]:

JavaScript
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:

TEXT
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 2

The 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):

TEXT
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:

TEXT
new = ((old - first * B^(m-1)) * B + next) mod M
JavaScript
function 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:

TEXT
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:

TEXT
-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:

TEXT
random text 200000, mod 101 false positives 2225random text 200000, mod 1009 false positives 266random text 200000, mod 1000003 false positives 0

With 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 k with 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.

JavaScript
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(" "));
TEXT
7 1 0 0 3 1 05 4 3 2 1

Check 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:

JavaScript
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:

TEXT
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:

TEXT
0 0 -1 -1

Timing, 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:

TEXT
indexOf     0.9 msnaive       14.0 mskmp         13.9 msrabin-karp  13.1 msz           28.9 ms

On 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:

TEXT
aaaa 200k indexOf     0.9 msaaaa 200k naive       2674.2 msaaaa 200k kmp         4.4 msaaaa 200k rabin-karp  1.9 ms

Naive 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:

NeedUse
Does the text contain this word?text.includes(word)
Where is the first match?text.indexOf(word) (returns -1 if none)
All start positionsloop indexOf(word, from), or text.matchAll(regex)
Starts or ends withstartsWith, endsWith
A pattern with wildcards or classesa 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:

  1. It is an interview or a learning exercise. They want to see that you know why naive can be quadratic.
  2. 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.
  3. 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).
  4. You search many patterns (next section).
  5. 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):

  1. Put all patterns in a trie.
  2. 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.
  3. 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:

JavaScript
const words = ["he", "she", "his", "hers"];const ac = buildAC(words);console.log(searchAC(ac, "ushers").map(([i, w]) => `${words[w]} ends at ${i}`).join("; "));
TEXT
she ends at 3; he ends at 3; hers ends at 5

In 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):

TEXT
AC vs indexOf loop, 3000 random cases, mismatches: 0

You 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

SituationChoiceTime
Ordinary application codeincludes / indexOfbuilt-in
Interview: one pattern, worst case mattersKMP or ZO(n + m)
Many patterns of the same length, or window comparisonRabin-KarpO(n + m) average
Many patterns of different lengthsAho-CorasickO(n + total pattern length + matches)
Stream of charactersKMP (keeps only k)O(1) per character
Short text, short patternnaive; it is simple and fastO(n x m) worst
Need periods, prefix counts, "string vs itself"Z-array or failure functionO(n)

Common mistakes

  1. Using k = 0 on a KMP mismatch. That throws away the border and makes the search wrong or slow. Use k = f[k - 1].
  2. Skipping verification in Rabin-Karp. Equal hashes are not equal strings; I showed a real collision (aa and ht).
  3. Using a negative number in the rolling update. Add the modulus before taking %.
  4. Forgetting the separator in the Z search. Without it, a match can run across the pattern/text border.
  5. Handling the empty pattern wrongly. indexOf("") is 0. Decide what you return and test it.
  6. Writing your own search in production code where includes would do.
  7. Testing only on random text. Naive and KMP looked identical there. Test with a one- or two-letter alphabet.

Try it

  1. Compute failure("abcabcd") by hand, then run it to check.
  2. Use zArray to find the smallest period of "abcabcabc". (Hint: the smallest i where i + z[i] === n and n % i === 0.)
  3. Write findAll(t, p) using KMP that returns all start positions (including overlapping ones: "aaaa" and "aa" give 0, 1, 2). After a full match, set k = f[m - 1] and continue.
  4. Make Rabin-Karp fail its filter on purpose with modulus 101: count false positives on 100,000 random letters.
  5. Check your findAll against a loop over indexOf(p, i + 1) on thousands of random strings over the alphabet ab.

Answers.

  1. 0 0 0 1 2 3 0 (I ran it). The border grows with a, ab, abc, then d breaks it.
  2. The Z-array of abcabcabc is 9 0 0 6 0 0 3 0 0. At i = 3: 3 + 6 = 9 = n and 9 % 3 = 0, so the period is 3 (the string is abc three times). I ran zArray on it and printed exactly that list.
  3. After a full match, record i - m + 1, then do k = f[k - 1] so that overlapping matches are found.
  4. Expect about one false positive per 100 windows, as in the numbers above (2,225 in 200,000 for my 6-letter pattern).
  5. 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 (aa and ht with 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 includes and indexOf in 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