KMP算法
一. 梗概
主要是通过利用模式串中的重复字母,进行跳跃匹配减少匹配次数:
-
abcaefgab主串S -
abcax模式串T
假设当前匹配到第5位,e和x不相同
-
使用暴风算法,模式串就要回溯到第1个位置(
a)和主串的第2个位置(b)匹配 -
使用
KMP算法,因为已知主串中的第1个位置a和第5个位置a相同,模式串可以直接回溯到第2个位置b和主串中的第6个位置e去匹配;以此来减少匹配的次数;
那么问题来了,根据什么回溯?
二. next数组推导
2.1 原理
把模式串T中各个位置j值的变化定义为一个next数组,那么next数组的长度就是模式串T的长度,有下面的函数定义:
- 当
j = 1时,next[j] = 0 - Max {k | 1<k<j , 且’p1...pk-1’ = ‘ pj-k+1 ... pj-1’} 当此集合不为空时,
next[j] = k - 其他情况,即前面没有重复字母,
next[j] = 1
2.2 练习推导如下:
2.2.1 模式串T = abcd
-
当
j = 1时,next[1] = 0 -
当
j = 2时,j由1到j-1范围内只有字符a,属于其他情况next[2] = 1 -
当
j = 3时,j由1到j-1范围内只有字符ab,显然a不等于b,属于其他情况next[3] = 1 -
当
j = 4时,j由1到j-1范围内只有字符"abc",显然abc不存在相等的情况,属于其他情况next[4] = 1
因此,
next = [0,1,1,1]
2.2.2 模式串T = abcabd
- 当
j = 1时,next[1] = 0 - 当
j = 2时,j由1到j-1范围内只有字符a,属于其他情况next[2] = 1 - 当
j = 3时,j由1到j-1范围内只有字符"ab",显然a不等于b,属于其他情况next[3] = 1 - 当
j = 4时,j由1到j-1范围内只有字符"abc",显然abc不存在相等的情况,属于其他情况next[4] = 1 - 当
j = 5时,j由1到j-1范围内只有字符"abca",显然abca存在相等的字符"a",因此,可以得到next[5] = 2,表示当第五个位置匹配失败时,直接从模式串的第二个位置与匹配失败的那个字符进行相比。 - 当
j = 6时,j由1到j-1范围内只有字符"abcab",显然abcab存在相等的字符"ab",因此,可以得到next[6] = 3,表示当第五个位置匹配失败时,直接从模式串的第三个位置与匹配失败的那个字符进行相比。
因此,
next = [0,1,1,1,2,3]
2.2.3 模式串T = aaaabc
当j = 1时,next[1] = 0
当j = 2时,j由1到j-1范围内只有字符"a",属于其他情况 next[2] = 1
当j = 3时,j由1到j-1范围内只有字符"aa",显然存在a等a next[3] = 2
当j = 4时,j由1到j-1范围内只有字符"aaa",显然前缀aa等于后缀aa,next[4] = 3
当j = 5时,j由1到j-1范围内只有字符"aaaa",前缀aaa与后缀aaa相等,因此,可以得到next[5] = 4
当j = 6时,j由1到j-1范围内只有字符"aaaab",前缀与后缀不想等,因此,next[6]= 1
因此,
next = [0,1,2,3,4,1]
2.3 代码实现
next求解
//注意字符串T[0]中是存储的字符串长度; 真正的字符内容从T[1]开始;
void get_next(String T,int *next){
int i,j;
j = 1;
i = 0;
next[1] = 0;
//遍历T模式串, 此时T[0]为模式串T的长度;
//printf("length = %d\n",T[0]);
while (j < T[0]) {
if(i ==0 || T[i] == T[j]){
//T[i] 表示后缀的单个字符;
//T[j] 表示前缀的单个字符;
++i;
++j;
next[j] = i;
}else{
//如果字符不相同,则i值回溯;
i = next[i];
}
}
}
- 使用
KMP
//KMP 匹配算法(1)
//返回子串T在主串S中第pos个字符之后的位置, 如不存在则返回0;
int Index_KMP(String S,String T,int pos){
//i 是主串当前位置的下标准,j是模式串当前位置的下标准
int i = pos;
int j = 1;
int count = 0;
//定义一个空的next数组;
int next[MAXSIZE];
//对T串进行分析,得到next数组;
get_next(T, next);
count = 0;
//注意: T[0] 和 S[0] 存储的是字符串T与字符串S的长度;
//若i小于S长度并且j小于T的长度是循环继续;
while (i <= S[0] && j <= T[0]) {
//如果两字母相等则继续,并且j++,i++
if(j == 0 || S[i] == T[j]){
i++;
j++;
}else{
//如果不匹配时,j回退到合适的位置,i值不变;
j = next[j];
}
}
if (j > T[0]) {
return i-T[0];
}else{
return -1;
}
}
int main(int argc, const char * argv[]) {
int i,*p,*t;
String s1,s2;
int Status;
//KMP算法调用
StrAssign(s1,"abcababca");
printf("主串为: ");
StrPrint(s1);
StrAssign(s2,"abcdex");
printf("子串为: ");
StrPrint(s2);
Status = Index_KMP(s1,s2,1);
printf("主串和子串在第%d个字符处首次匹配(KMP算法)[返回位置为负数表示没有匹配] \n",Status);
StrAssign(s1,"abccabcceabc");
printf("主串为: ");
StrPrint(s1);
StrAssign(s2,"abcce");
printf("子串为: ");
StrPrint(s2);
Status = Index_KMP(s1,s2,1);
printf("主串和子串在第%d个字符处首次匹配(KMP算法)[返回位置为负数表示没有匹配] \n",Status);
StrAssign(s1,"aaaabcde");
printf("主串为: ");
StrPrint(s1);
StrAssign(s2,"aaaaax");
printf("子串为: ");
StrPrint(s2);
Status = Index_KMP(s1,s2,1);
printf("主串和子串在第%d个字符处首次匹配(KMP算法)[返回位置为负数表示没有匹配] \n",Status);
return 0;
}
- 输出
主串为: abcababca
子串为: abcdex
主串和子串在第-1个字符处首次匹配(KMP算法)[返回位置为负数表示没有匹配]
主串为: abccabcceabc
子串为: abcce
主串和子串在第5个字符处首次匹配(KMP算法)[返回位置为负数表示没有匹配]
主串为: aaaabcde
子串为: aaaaax
主串和子串在第-1个字符处首次匹配(KMP算法)[返回位置为负数表示没有匹配]
三 next优化
3.1 在求解nextVal数组的5种情况:
- 默认
nextval[1] = 0; T[i] == T[j]且++i,++j后T[i]依旧等于T[j]则nextval[i] = nextval[j]i = 0, 表示从头开始i++,j++后,且T[i] != T[j]则nextVal = j;T[i] == T[j]且++i,++j后T[i] != T[j],则nextVal = j;- 当
T[i] != T[j]表示不相等,则需要将i退回到合理的位置. 则i = next[i];
3.2 练习:
当模式串T = 中有多个相同的字符时,会进行多次重复比较,如模式串为ababaaaba,next 数组为{0,1,1,2,3,4,2,2,3}
- 当
j = 1,nextVal = 0; 继续保持next[1]的逻辑; - 当
j = 2时,也就是当j = 2发生匹配错误, 那么由于第二个字符'b'的next值是1, 而且第一个字符是'a' .两者不相等.所以nextval[2] = next[2] = 1; - 当
j = 3时, 此时因为第3个字符'a' 的next值是1, 所以得知第1位的'a' 与第3位的'a' 是相等,则此时nextVal[3] = nextVal[1] = 0; - 当
j = 4时, 因为第4个字符 "b" 的next = 2; 所以可以得知 它与第2位字符 "b" 是相等,则nextVal[4] = nextVal[2] = 1; - 当
j = 5时,next值为3, 第5个字符'a' 与第3个字符'a' 相等,则nextVal[5] = nextVal[3] = 0; - 当
j = 6时,next值为4, 第6个字符'a' 与第4个字符'b' 不相等,则nextVal[6] = 4; - 当
j = 7时,next值为2, 第7个字符'a' 与第2个字符'b' 不相等,则nextVal[7] = 2; - 当
j = 8时,next值为2, 第8个字符'b'与第2个字符'b' 相等,则nextVal[6] = nextVal[2] = 1; - 当
j = 9时,next值为3, 第9个字符'a' 与第3个字符'a' 相等,则nextVal[9] = nextVal[3] = 0;
3.3 代码实现
- nextval 求解
//求模式串T的next函数值修正值并存入nextval数组中;
void get_nextVal(String T,int *nextVal){
int i,j;
j = 1;
i = 0;
nextVal[1] = 0;
//字符串"0" 位置,存储了字符的长度
while (j < T[0]) {
if (i == 0 || T[i] == T[j]) {
++j;
++i;
//如果当前字符与前缀不同,则当前的j为nextVal 在i的位置的值
if(T[i] != T[j])
nextVal[j] = i;
else
//如果当前字符与前缀相同,则将前缀的nextVal 值赋值给nextVal 在i的位置
nextVal[j] = nextVal[i];
}else{
i = nextVal[i];
}
}
}
- 调用
//返回子串T在主串S中第pos个字符之后的位置, 如不存在则返回0;
int Index_KMP2(String S,String T,int pos){
//i 是主串当前位置的下标准,j是模式串当前位置的下标准
int i = pos;
int j = 1;
//定义一个空的next数组;
int next[MAXSIZE];
//对T串进行分析,得到next数组;
get_nextVal(T, next);
count = 0;
//注意: T[0] 和 S[0] 存储的是字符串T与字符串S的长度;
//若i小于S长度并且j小于T的长度是循环继续;
while (i <= S[0] && j <= T[0]) {
//如果两字母相等则继续,并且j++,i++
if(j == 0 || S[i] == T[j]){
i++;
j++;
}else{
//如果不匹配时,j回退到合适的位置,i值不变;
j = next[j];
}
}
if (j > T[0]) {
return i-T[0];
}else{
return -1;
}
}
int main(int argc, const char * argv[]) {
int i,*p,*t;
String s1,s2;
int Status;
//KMP算法调用
StrAssign(s1,"abcababca");
printf("主串为: ");
StrPrint(s1);
StrAssign(s2,"abcdex");
printf("子串为: ");
StrPrint(s2);
Status = Index_KMP2(s1, s2, 1);
printf("主串和子串在第%d个字符处首次匹配(KMP_2算法)[返回位置为负数表示没有匹配] \n\n",Status);
StrAssign(s1,"abccabcceabc");
printf("主串为: ");
StrPrint(s1);
StrAssign(s2,"abcce");
printf("子串为: ");
StrPrint(s2);
Status = Index_KMP2(s1, s2, 1);
printf("主串和子串在第%d个字符处首次匹配(KMP_2算法)[返回位置为负数表示没有匹配] \n\n",Status);
StrAssign(s1,"aaaabcde");
printf("主串为: ");
StrPrint(s1);
StrAssign(s2,"aaaaax");
printf("子串为: ");
StrPrint(s2);
Status = Index_KMP2(s1, s2, 1);
printf("主串和子串在第%d个字符处首次匹配(KMP_2算法)[返回位置为负数表示没有匹配] \n\n",Status);
return 0;
}
- 输出
主串为: abcababca
子串为: abcdex
主串和子串在第-1个字符处首次匹配(KMP_2算法)[返回位置为负数表示没有匹配]
主串为: abccabcceabc
子串为: abcce
主串和子串在第5个字符处首次匹配(KMP_2算法)[返回位置为负数表示没有匹配]
主串为: aaaabcde
子串为: aaaaax
主串和子串在第-1个字符处首次匹配(KMP_2算法)[返回位置为负数表示没有匹配]