String · 19 of 43
KMP Algo
Find every place a short word appears inside a long text, without ever stepping back in the text, by remembering how the word repeats itself.
- Simple way
- time O(n * m)
room O(1) - Clever way
- time O(n + m)
room O(m)
The problem, simply
You get a long text of length n and a short pattern of length m. Give back every index in the text where the pattern starts. Matches may overlap.
The famous fast way is called KMP, after the three people who found it: Knuth, Morris and Pratt.
A story to remember it
Picture it
text = ABABABC and pattern = ABABC. The cheat sheet (lps) says how long a start of the pattern also ends at each spot. The pattern is found at index 2.
- answer
Examples
A normal case
- In
text = 'AABAACAADAABAAABAA', pattern = 'AABA'- Out
[0, 9, 13]
AABA shows up at the very start, then again at 9 and at 13.
Overlapping matches
- In
text = 'AAAAA', pattern = 'AA'- Out
[0, 1, 2, 3]
Matches can share letters. After a match, KMP jumps by the cheat sheet, not past the whole pattern, so it finds all four.
A near miss
- In
text = 'ABABABC', pattern = 'ABABC'- Out
[2]
The try at index 0 fails on the fifth letter. KMP keeps the AB it already has and finishes the match at index 2.
Not there
- In
text = 'ABC', pattern = 'D'- Out
[]
No D in the text, so the list is empty.
Way 1: the simple way
Put the pattern under the text at every start position and compare letter by letter. On the first mismatch, slide the pattern one step right and compare from its first letter again.
- For each start
sfrom 0 ton - m. - Compare
text[s + k]withpattern[k]fork = 0, 1, .... - If all
mletters match, write downs.
import java.util.*;
class Solution {
public List<Integer> search(String text, String pattern) {
List<Integer> found = new ArrayList<>();
int n = text.length(), m = pattern.length();
if (m == 0) return found;
for (int s = 0; s + m <= n; s++) {
int k = 0;
while (k < m && text.charAt(s + k) == pattern.charAt(k)) k++;
if (k == m) found.add(s); // every letter matched
}
return found;
}
}How long it takes: each of about n starts can compare up to m letters, for example text AAAA...AB and pattern AAAB. So O(n * m). Extra room: O(1).
Way 2: the clever way
The slow part of Way 1 is forgetting. After matching ABAB, it moves on and checks the same letters again. KMP builds a cheat sheet first, called the lps table (longest proper prefix that is also a suffix).
lps[i] answers: in pattern[0..i], what is the longest start that is also an end, not counting the whole piece? For ABAB that is AB, so lps[3] = 2.
Then search with two fingers: i on the text, which only moves forward, and j on the pattern, which counts how many letters match right now.
- If
text[i]matchespattern[j], move both forward. - If it does not match and
j > 0, setj = lps[j - 1]and try the sametext[i]again. - If it does not match and
jis 0, moveiforward. - If
jreachesm, record a match ati - m + 1, then setj = lps[m - 1]to keep looking.
The cheat sheet is built the same way, by matching the pattern against itself.
Building the lps cheat sheet for ABABC
01 / 06
Start. lps[0] is always 0, because one letter has no shorter start that is also its end. i starts at 1 and len at 0.
- len
- 0
- checking
- done
- rejected
- just added
Searching ABABABC for ABABC
01 / 09
i = 0: A matches pattern[0]. j moves to 1.
- j
- 1
- checking
- done
- answer
- rejected
import java.util.*;
class Solution {
// lps[i] = length of the longest start of pattern[0..i] that is also its end
int[] buildLps(String p) {
int m = p.length();
int[] lps = new int[m];
int len = 0;
for (int i = 1; i < m; i++) {
while (len > 0 && p.charAt(i) != p.charAt(len)) {
len = lps[len - 1]; // fall back, do not restart
}
if (p.charAt(i) == p.charAt(len)) len++;
lps[i] = len;
}
return lps;
}
public List<Integer> search(String text, String pattern) {
List<Integer> found = new ArrayList<>();
int n = text.length(), m = pattern.length();
if (m == 0) return found;
int[] lps = buildLps(pattern);
int j = 0; // letters matched right now
for (int i = 0; i < n; i++) { // i never goes back
while (j > 0 && text.charAt(i) != pattern.charAt(j)) {
j = lps[j - 1];
}
if (text.charAt(i) == pattern.charAt(j)) j++;
if (j == m) {
found.add(i - m + 1);
j = lps[m - 1]; // keep going, matches may overlap
}
}
return found;
}
}How long it takes: i moves forward n times. j goes up by at most 1 per step, and every fall back makes it smaller, so all the fall backs together are at most n. Building the table is the same story with m. Total O(n + m). Extra room: the lps table, O(m).
Watch out for
- After a full match, set
j = lps[m - 1], notj = 0, or overlapping matches likeAAinAAAAare missed. lps[0]is always 0. The loop that builds the table starts ati = 1.- The "proper" in the name matters: the whole word does not count as its own start and end.
- An empty pattern needs a rule. Here it returns an empty list. Ask the interviewer.
What interviewers ask next
- Does a string repeat a smaller block, like
abcabc? Build lps for it. IfL = lps[n - 1]is above 0 andn % (n - L) == 0, yes. - Shortest palindrome by adding letters at the front? Build lps of
s + "#" + reverse(s). The last value is the longest palindrome start. - Is
ba rotation ofa? Same length, and KMP findsbinsidea + a. - KMP or Rabin Karp? KMP is always
O(n + m)with no bad luck. Rabin Karp is simpler to extend to many patterns or 2D grids, but can be slow on hash collisions. - What is the Z algorithm? Another linear way.
z[i]is how long the string starting atimatches the start of the whole string. Search withpattern + "$" + text.
Remember this
Grown-up words: the cheat sheet is the lps array (also called the prefix function or failure function), the two fingers are the text index and pattern index, and the handshake trick is the Knuth Morris Pratt algorithm.