文章出处:极客时间《数据结构和算法之美》-作者:王争。该系列文章是本人的学习笔记。
KMP,是三个作者(D.E.Knuth,J.H.Morris和V.R.Pratt)的简称。
KMP算法和BM一样,也是一个字符串匹配算法。其核心思想和BM也类似,查找规律,减少当发生字符不匹配的时候,回退的长度和次数,减少比较次数,降低时间复杂度。
文章内容依然主要来自极客时间的算法专栏。同时参考了知乎文章。
1 字符串匹配是什么
字符串匹配就是想看看一个字符串在另一个字符串中的位置。例如查找字符串"ababacd"在字符串"ababaeabac"中出现的位置。这就是字符串查找。
在A中查找B,A称为主串,B称为模式串。
A:ababaeabac
B:ababacd
一般的查找方法是从第0位开始一一匹配字符。当找到第5位的时候发现字符不匹配。这时候从A的第1位开始,与B重头开始一一匹配。当匹配到B串末尾,则匹配成功。
public static int indexOf(String a,String b){
int idx = -1;
int n = a.length();
int m = b.length();
int i=0;
while(i<n){
int j = 0;
int pos = i;
while(pos<n && j<m && a.charAt(pos)==b.charAt(j)){
pos++;
j++;
}
if(j==m){
idx = i;
break;
}else{
i++;
}
}
return idx;
}
2 这里介绍几个概念。
![](https://i-blog.csdnimg.cn/blog_migrate/9453eba88295a09bca21fb4f400f5a58.jpeg)
当查找匹配到不相同的字符的时候在主串中的那个字符成为坏字符。
已经匹配的部分成为好前缀。
字符串前缀子串:一个字符串S的前缀子串是指从下标0开始形成S的不同子串。例如S=ababa,S的前缀子串有:a,ab,aba,abab。
字符串后缀子串:一个字符串S的前缀子串是指以最后一位为结尾形成S的不同子串。例如S=ababa,S的后缀子串有:a,ba,aba,baba。
可匹配子串:是指长度相等的前后缀子串相等。例如上个例子中的子串’a’,‘aba’。就是可匹配子串
最长可匹配子串:在可匹配子串中找长度最长的子串。上个例子中是’aba’。
最长可匹配前缀子串和最长可匹配后缀子串,虽然最长可匹配子串是相同的,但是前缀子串和后缀子串的起始位置和结束位置不同。例如上个例子中最长可匹配子串’aba’,作为最长可匹配前缀子串起止的位置是0和2,最长可匹配后缀子串的位置是2和4。为什么这么关心位置,在下面有介绍。
在减少移动次数的过程中,我们关心最长可匹配前缀子串C的终止位置x。下面描述中将用C表示最长可匹配前缀子串。
前缀子串 | 后缀子串 | 是否匹配 | 是否C | C的终止位置 |
---|---|---|---|---|
a | a | 是 | 否 | |
ab | ba | 否 | 否 | |
aba | aba | 是 | 是 | 2 |
abab | baba | 否 | 否 |
3 尽可能少移动
3.1目标是主串位置不动
![](https://i-blog.csdnimg.cn/blog_migrate/9453eba88295a09bca21fb4f400f5a58.jpeg)
是不是能够减少移动次数呢?可不可以保持主串中的位置不动,只移动模式串?例如下面这样。
ababaeabac
ababacd
因为移动模式串,已经匹配的部分"ababa"就需要前缀子串与后缀子串能匹配得上,还要尽可能长,也就是上面概念中提到的最长可匹配前缀子串C。
从图中看到用模式串中"ababa"的前缀子串,去匹配主串中"ababa"的后缀子串。为了不会移动的过头这个匹配还需要使用最长的子串。例如下面这个就不可以。虽然"a"也是匹配子串,但是向右移动的更多了,可能会错过某些匹配。
ababaeabac
ababacd
3.2
这里发现字符串前缀子串和后缀子串匹配完全可以在模式串内进行,与主串无关。如果我们暴力枚举求字符串"ababa"的最长可匹配前缀子串C,算法复杂度必将很高,没有什么改进之处。
我们可以假设如果已经提前知道"ababa"的最长的那个可以和后缀匹配的前缀子串C=“aba”,并且知道C的终止位置是2(数组下标)。那模式串的下标就可以直接移动下标3了。
4 模式串求next数组
我们假设把"ababa"的最长可匹配前缀子串C的终止位置是2存在一个数组中,这个数组是next数组,称为失效函数。
我们假设在模式串“ababacd”的每一个位置都可能发生不匹配,则需要求模式串每个子串和本身的next数组值。
next数组下标表示模式串每个子串的结尾下标位置。值表示C字符串的终止位置。
next数组也被成为失效函数。
例如模式串:ababacd next数组值为下表。
发生不匹配时候已经匹配好的字符串 | 结尾下标 | C的结尾下标 | 值 |
---|---|---|---|
a | 0 | -1 | next[0]=-1 |
ab | 1 | -1 | next[1]=-1 |
aba | 2 | 0 | next[2]=0 |
abab | 3 | 1 | next[3]=1 |
ababa | 4 | 2 | next[4]=2 |
ababac | 5 | -1 | next[5]=-1 |
求next数组的值可以使用动态规划的思想。
假设next[i-1]=k。如果模式串S的k+1的字符与S的i是相同的,则next[i]=k+1。例如上个表格中next[2]=0,而且S.charAt(0+1)=S.charAt(2+1),所以next[3]=1。
如果模式串S的k+1的字符与S的i是不同的,说明字符串[0,i-1]的最长可匹配前缀子串C不可用。接着查找次长是不是满足条件。(为什么查找次长?因为想要利用之前已经计算的结果。否则就是从头开始计算了。也就没做什么优化了)
现在需要证明:S[0,i-1]的次长可匹配前缀子串 = S[0,k]的最长可匹配前缀子串。
S[0, i-1] 的次长可匹配前缀子串X的前缀、后缀一定在S[0,k]范围内,而且是S[0,k]的前缀、后缀,那么S[0,k]的最长可匹配前缀子串=S[0,i-1]的次长可匹配前缀子串。
如果x>k,可以推出 S[0,x]长度>S[0,k]的长度,而且S[0,x]还是S[0,i-1]的可匹配前缀子串,那么S[0,x]应该是S[0,i-1]的最长可匹配前缀子串。这与S[0,k]是最长可匹配前缀子串矛盾。这证明了x<k。
进一步,因为x<k,要想找到S[0,i-1]的次长可匹配前缀子串,也就是要在S[0,k]找最长可匹配前缀子串。
例如对于对子字符串 abababzababab 来说,
前缀有 a, ab, aba, abab, ababa, ababab, abababz, …
后缀有 b, ab, bab, abab, babab, ababab, zababab, …
next[13]=6。“ababab” 是最长可匹配前缀子串,"abab"是次长可匹配前缀子串。
因为可匹配前缀子串位置从下标0开始的,所以对于同一个字符串的次长可匹配前缀子串一定是最长可匹配前缀子串的一部分,所以"abab" 是 “ababab” 的子串。
又因为"abab"是可匹配子串,所以"abab" 一定是 “ababab” 的前后缀,也就一定是“ababab” 的最长可匹配子串。假设"abab" 不是“ababab” 的最长可匹配子串,那对于“abababzababab”的次长子串一定是其他子串而不是"abab"。
所以“ababab”的最长可匹配前缀子串的终止位置=“abababzababab”次长可匹配前缀子串的终止位置。也就是代码k=next[k]。
继而,继续判断S[k+1]是不是和S[i]相同。
public class KMP {
// b 表示模式串,m 表示模式串的长度
private static int[] getNexts(char[] b, int m) {
int[] next = new int[m];
int k = -1;
next[0] = -1;
for(int i=1;i<m;i++){
while(k!=-1 && b[k+1]!=b[i]){
k = next[k];
}
//假设next[i-1]=k,如果模式串S的k+1的字符与S的i是相同的
if(b[k+1] == b[i]){
k++;
}
next[i] = k;
}
return next;
}
// a, b 分别是主串和模式串;n, m 分别是主串和模式串的长度。
public static int kmp(char[] a, int n, char[] b, int m) {
int[] next = getNexts(b,m);
int j = 0;
for(int i=0;i<n;i++){
//当字符不匹配的时候,将模式串的指针回退到最长可匹配前缀字符的结束下标位置
while(j>0 && a[i]!=b[j]){
j= next[j-1]+1;
}
//字符匹配的时候
if(a[i]==b[j]){
j++;
}
if(j==m){
return i-m+1;
}
}
return -1;
}
public static void main(String[] args){
String a = "abbaabbaaba";
String b = "a";
int p = kmp(a.toCharArray(),a.length(),b.toCharArray(),b.length());
System.out.println(p);
}
}