字符串匹配的改进算法——KMP算法(C/C++)
字符串匹配
关于什么是KMP操作,详情见
KMP的基本思想
一般方法的弊端:
下面可以不看~~感兴趣可以看看~~,总之就是说:一般方法在面对重复率极高的串时匹配效率极低~
首先,当主串为 00000000000000000000000000000000000000000000000000001
,而模式串为 00000001
时,由于主串中前七个字符为 0
,模式串中前52个字符为 0
,每一轮比较都在模式串的最后一个字符处失配,此时需要将指针(游标) i
(指示主串),回溯到 i-6
的位置上,并从模式串的第一个字符开始重新比较。整个匹配过程 指针(游标) i
需要回溯45次, while
循环需要执行 46 * 8 * (Index * m)
。在 worst situation下 时间复杂度为O(m*n)
。
问题在哪?!
每次失配之后,模式串偏移量太小,导致重复匹配。如果你看了上面那段废话,就可以理解为,指针每次失配之后都回溯至起始位置,导致效率低下!
解决办法:指针不再回溯,一次遍历主串与模式串匹配即可完成!!
数学分析
来一段数学分析,不感兴趣的同学下滑再下滑就行~~
正文开始:
指针如何不回溯?只需要将模式串失配后,沿着主串方向滑动一定的长度,使主串部分可以与模式串匹配,不需要回溯指针。
假设主串为S[i],模式串为P[j],其中i指示主串的位置即为主串指针(浮标),j指示模式串的位置即为模式串指针(浮标)。
在一次匹配之后,设第j个字符匹配失败,假设此时主串在指针(游标)i在不回溯的前提下,应与模式串中的第 k (k<j)个字符继续匹配;则一定会有如下关系式:
P[1]P[2]...P[k-1] = S[i-k+1]S[i-k+2]...S[i-1] ----(1)
这次匹配的前一次的匹配结果:
P[j-K+1]P[j-k+2]...P[j-1] = S[i-k+1]S[i-k+2]...S[i-1] ----(2)
联立式(1)和(2),消去主串可以得到:
P[1]P[2]...P[k-1] = P[j-K+1]P[j-K+2]...P[j-1]
根据此式子可以得出如下结论:
主串中第i位应与模式串中第 k(k<j) 位对齐,且 k 仅与模式串自身有关,因此,匹配仅需从模式串中第 k 个字符和主串中第 i 个字符比较起继续进行。
如上述所得:此时应求模式串的k
值!为了确定k
,需要一个next(){next[j]=k}
来存储模式串的每一个字符对应的k值~
define next():
令next[j]=k,则next[j]表明当前模式中第j个字符与主串中相应字符“失配”时在模式中需重新和主串中该字符进行比较的字符的位置。
next[j] =
if(j==1) 0;
// Max集合非空
else if(Max{K|1<k<j && p[1]...p[k-1]=p[j-k+1]...p[j-1]})
Max{K|1<k<j && p[1]...p[k-1]=p[j-k+1]...p[j-1]
else 1; //其他情况
next数组详解
next的文字描述
废话又来了~不过可以看看~
首先,我们简要分析一下 next()
的功能。
假设 i 指示主串,j 指示模式串, k
表示应滑动到的位置。
由我们上面得到的数学公式的结论:k
只与模式串自身有关。
P[1]P[2]...P[k-1] ”=“P[j-K+1]P[j-K+2]...P[j-1]
那么这个 k
具体又是什么东西呢?
简而言之,就是模式串在第 j
个位置(失配的位置)之前,有 k-1
个字符与模式串从首字符起 k-1
个字符相同,这也是上述数学公式的结论。
那么如何求得模式串的 k
呢?
注意:需要求解的是模式串的第 j-(k-1)
到 j-1
的位置与 1
到 k-1
的位置字符相同。
如果使用两层循环求解效率过低,在此处可以使用递归的思想。
即要求 k
这个位置,在每次比较,即模式串与模式串自己匹配(模式串既作为主串,也作为模式串),而我们的next()
本身指示的就是模式串应该滑到哪个位置。
所以,应该使用两个指针指示模式串,假设分别为 i
,j
。i
依次指向模式串的每一个位置, j
指向模式串头地址并听从 next()
本身滑动。根据上述定义,模式串的首字符的 next
值应为0
。i
从第 next[1]
开始,j
从next[0]
开始。
当 i
指示到模式串中间某个位置时,j
也指示到了模式串中的某个位置(j<i)
,此时对于next()
应有两个选择:一、当前位置下,next[i] == next[j]
,那么只需要将指针顺移,并且该位置(i
指示的位置)的next
值就为 j
的值(前面一共判断相同判断了j
次)。二、当前位置下,next[i] != next[j]
,此时,根据next()
的定义,j
应该回溯到 next[j]
的位置(这里使用了递归)。
现在从初始状态检查 next[ ]
。已知next[1] = 0
。初始 j = 0
,i = 1
,但next[0]
并不存储数据,但是又要为next[1]
赋值,避免使用冗长的判断,赋值,则在判断 next[i]
是否等于 next[j]
时,当 j=0
,直接递增 i
,与 j
(next[0]
并没有实际意义,next[1]
才有实际意义)。
(这里只是在求模式串的k
值,不要与主串,模式串的i
,j
搞混,虽然本质上没有区别)
next实现:
next()的C语言代码如下:
// 求模式串T的next函数值并存入数组next。
void get_next(char *T, int **next){
int i=0,j=-1,T_len = strlen(T);
(*next) = (int *)malloc(T_len*sizeof(int));
(*next)[0] = -1;
while(i<T_len-1){
if(j==-1||T[i]==T[j]){
i++;j++;
(*next)[i] = j;
}
else
j = (*next)[j];
}
//for(int i=0;i<strlen(T);++i)
// printf("%d ",(*next)[i]);
//putchar('\n');
}
KMP算法C语言实现
代码如下:
// 从主串S的第pos个字符后开始匹配即数组的pos-1个位置
int Index_Kmp(char *S,char *T,int *next,int pos){
int i=pos-1,j=0;
int S_len = strlen(S),T_len = strlen(T);
while(i<S_len && j<T_len){
if(j==-1 || S[i]==T[j]){
++i;
++j;
}
else
j = next[j];
}
if(j>=T_len)
return i-T_len;
else
return -1;
}