KMP算法引入
我们先看上篇博客提到的字符串匹配问题,假设现在有一个主串S = “abcdefgab” ; 模式串 T = “abcdex” ; 如果使用暴风算法的话,前面5个字母完全相等,直到第6个字母.'f' 和 'x' 不相等; 如下图
按照暴风算法计算时我们需要②③④过程都执行
只有当执行①的时候S[i] = T[j],此时 i= 2,j = 1,然后继续循环。但是明显的从j = 1到j = 5,T
[j]均不相同,S[i]中i = 1到i = 5也不同,并且S[i] = T[j],这就浪费了②③④过程,因此我们引出KMP算法
KMP算法的原理
因此我们需要先算出当S[i] != T[j]时模式串T中j下一个移动位置,要计算j的下一个移动位置,我们先看下面的例子
第一种情况
- 如果我们知道T串中的首字母a与T串中后面的字符均不相等(这是前提,具体计算后面讲解)
- 并且T串第二个字符b跟S串的第二个字符b相等,这样我们就知道T串首字符a与S串的第二个字符不用判断,因为他们根本不可能相等,因此②步骤可以省略
- 同样的道理在T串首字符a与其后面的字符均不相同的前提下,a与S串的c、d、e也不相同,因此③④⑤就没有必要了,我们直接保留①以及⑥即可,如图
第二种情况:如果T串保留首字母字符时'a'时,假设主串S = ‘abcababca’,模式串T = 'abcabx'
- 对于刚开始的判断前5个字符完全相同,第6个字符不同
- 根据我们第一种情况的推断,T串的首字符a与T串第二个b以及第三个字符c均不相同,所以以下两个步骤就是多余的
由于T串首字符以及第二位字符跟第四位以及第五位字符分别相同,并且跟主串S的第四位、第五位相同,所有我们可以推断出下面的步骤也是多余的
因此我们可以得出对于子串中有首字符相同的字符,也是可以省略一部分不必要的步
从上面两个例子我们得出KMP算法就是为了减少不必要的步骤的,但是我们知道i是无法进行回溯的,那么我们只能对j进行回溯,我们通过刚才的分析可以得出j值得回溯跟主串S没有关系,j更多的是与模式串T的结构有关
模式串T回溯求解next()
- 我们先看以下例子
如上图所示,模式串T中没有相同的字符,所以j由6变成了1
- next[j]数组公式
next数组值推导
- 模式串T中没有相同的字符
在上图例子中,模式串T没有重复的字符,符合next公式中的第一和第三种情况,所以next = {0,1,1,1,1,1};
当模式串T中有重复的字符时,如T = "abcabx",
如上两张图所展示的,符合next推导公式的三种情况,所以next = {0,1,1,1,2,3};
- 思考,当T = “ababaaaba”时,next = {0,1,1,2,3,4,2,2,3},当T = "aaaaaaaab"时,next = {0,1,2,3,4,5,6,7,8}
- 总结next数组求值的四种情况:这里i表示需要回溯的位置,j是遍历模式串的下标
- 默认next[1] = 0;
- 当i == 0时,表示当前的比较应该从头开始,所以j++,i++,next[j] = i
- 当T[i] == T[j],表示两个字符相等,此时i++,j++,同时next[j] = i;
- 当T[i] != T[j],表示两个字符不相等,需要将i进行回溯到合理位置,i = next[i];
next数组求值代码实现
- 实现next数组求解
#pragma mark - 获取next数组
int *GetNext(String T){
if (!T) {
return NULL;
}
//注意T[0]存储的是字符串的长度
int *next = (int *)malloc(sizeof(int) * (T[0] + 1));
int i = 0, j = 1;
next[1] = 0;
while (j<T[0]) {
if (i == 0 || T[i] == T[j]) {//第一个字符或者相邻字符相同
i++;
j++;
next[j] = i;
}else{//相邻字符不相同,i值回溯
i = next[i];
}
}
return next;
}
KMP算法next数组优化
- 优化next数组的原因
KMP 算法也是有缺陷的,比如当主串S = "aaaabcde",模式串 T= "aaaaax",此时next = {0,1,2,3,4,5},
- 当我们匹配时,j = 5, i = 5,此时 ‘b’ 与 'a'不符合,如①所示则 j = next[i],即 j = next[5] = 4,此时如②还是不正确一次类推,直到j = 1
- 这样无形中多计算了j = 4、3、2、1,因此需要对计算next数组进行优化
- 由于T串的首字符与第二、第三、第四、第五位的字符相同,因此可以使用next[1]的值去替代跟它相等的字符后续的next[j]值,比如例子中next = {0,1,2,3,4,5}变为next = {0,0,0,0,0,5}
nextVal数组优化规律
初始化i = 0,j = 1
- 默认nextVal[1] = 0;
- 当T[i] == T[j],且i++,j++后T[i]依旧等于T[j],则nextVal[j] = nextVal[i]
- 当T[i] == T[j],且i++,j++后T[i]不等于T[j],则nextVal[j] = i
- 当i = 0,表示从头开始,i++,j++后,T[i] != T[j],则nextVal[j] = i,否则nextVal[j] = nextVal[i]
- 当T[i] != T[j]表示不相等,则需要i回溯,i = nextVal[i]
nextVal举例讲解
我们以模式串T = "ababaaaba"为例子,T没有优化前的next = {0,1,1,2,3,4,2,2,3},然后我们计算优化后的nextVal,下面中i是nextVal的下标,j是遍历T串的下标,temp = T[j],T[0]表示模式串T的长度,所以真实字符从下标1开始,初始化i= 0,j = 1
- j = 1时,temp = 'a',i = 0,nextVal[1] = 0(i++了)
- j = 2,temp = 'b',next[1] = 1,此时 b != (T[1] = a),所以nextVal[2] = 1,i++,j++
- j = 3,temp = ‘a’,next[2] = 1,此时 a == (T[1] = a),所以nextVal[3] = nextVal[1] = 0,i++,j++
- j = 4,temp = 'b',next[3] = 2,此时 b == (T[2] = b),所以nextVal[4] = nextVal[2] = 1,i++,j++
- j = 5,temp = 'a',next[4] = 3,此时 a == (T[3] = a),所以nextVal[5] = nextVal[3] = 0,i++,j++
- j = 6,temp = 'a',next[5] = 4,此时 a != (T[4] = b),所以nextVal[6] = 4,i++,j++
- j = 7,temp = 'a',next[6] = 2,此时 a != (T[2] = b),所以nextVal[7] = 2,i++,j++
- j = 8,temp = 'b',next[7] = 2,此时 b == (T[2] = b),所以nextVal[8] = nextVal[2] = 1,i++,j++
- j = 9,temp = 'a',next[8] = 3,此时 a == (T[3] = a),所以nextVal[9] = nextVal[3] = 0,i++,j++
- 因此推得nextVal = {0,1,0,1,0,4,2,1,0}
代码实现nextVal优化
#pragma mark - 优化nextVal数组
int *getNextVal(String T){
if (!T) {
return NULL;
}
//此处开辟T[0] + 1空间是因为nextVal的有效值从下标1开始,即nextVal[1] = 0
int *nextVal = (int *)malloc(sizeof(int)*(T[0] + 1));
nextVal[1] = 0;
int i = 0, j = 1;
while (j<T[0]) {
if (i == 0 || T[i] == T[j]) {//i == 0
i++;
j++;
if (T[i] == T[j]) {//++后依然相等则nextVal[j]跟前面的nextVal[i]相同
nextVal[j] = nextVal[i];
}else{//++后不相等,则nextVal[j]等于i
nextVal[j] = i;
}
}else{//不相等则i进行回溯
i = nextVal[i];
}
}
return nextVal;
}
KMP算法匹配字符
#pragma mark - KMP算法实现
int GetIndexKMP(String S, String T){
int sLength = S[0],tLength = T[0];
if (tLength>sLength) {
return -1;
}
//未优化next
int *next = GetNext(T);
//优化后的next
next = getNextVal(T);
int i = 1,j = 1;
while (i <= sLength && j<= tLength) {
if (S[i] == T[j] || j == 0) {//j == 0是因为j == 1时,此时不同, j = next[j],此时next[1] = 0,所以存在j == 0的情况
j++;
i++;
}else{//j回溯到next[j]
j = next[j];
}
}
if (j>tLength) {//找到了
return i - tLength;
}else{
return -1;
}
}总结
KMP算法难在理解计算i回溯的数组next或nextVal,理解了i回溯的计算,代码层面就比较简单了