- KMP算法的介绍
- KMP算法是一种模式匹配算法,相比于传统的朴素匹配算法来说,可以大大避免重复匹配的现象,降低时间复杂度。当然,它也是一种匹配过程中主串位置不断向前移动,但不回溯,子串位置可以向前移动,也可回溯的高效匹配算法,KMP算法的时间复杂度O(m+n)。
- KMP算法的相关函数
- get_next函数—获得next数组,而next数组记录对应字符下标,用于回溯。(核心)
- Index_KMP函数—获得匹配后的子串在主串中的pos位置。
- KMP算法的基本原理
- 什么是PMT?
对于一个字符串”abababca“来说,它的PMT如下图所示:
注意:上图中,value相当于next数组
就像例子中所示的,如果待匹配的字符串有8个字符,那么PMT也会有8个。在此,我先解释一下什么是前缀和后缀, 打个比方,设一字符串为”qerw”,那么它的前缀集合为{“q”, “qe”, “qer”},后缀集合为{“w”, “rw”, “erw”},我们把所有前缀组成的集合,称为字符串的前缀集合,把所有后缀组成的集合,称为字符串的后缀集合。
要注意的是,字符串本身并不是自己的后缀。
而 PMT中的值(value) 是字符串的前缀集合与后缀集合的交集中最长元素的长度。比如”ababa“,其前缀集合为{”a“,”ab“,”aba“,”abab“},后缀集合为{”baba“,”aba“,”ba“,”a“},而交集为{”a“,”aba“},交集中最长的字符串的长度表示为PMT中的值 (value),即结果为 3 。 - 如何使用PMT查找?(以下设主串为S串,子串为T,以方便解释)
我们来看下面的一个例子,假设 S = ”abcababca“ , T = “abcabx” 。
对应的T串的PMT为:
由于next[0]需要存储字符串的长度,所以 index从 j=1开始,对应next数组变成对应字符后面字符串的PMT中的值加一, 比如当 j=3时,针对字符串”ab“, 其PMT的值为0, 再加上1,最终成为 next[3]的值。
所以为了编程的方便, 我们不直接使用PMT数组,而是将PMT数组向后偏移一位。我们把新得到的这个数组称为next数组
匹配的步骤如下:
首先正常情况下,从j = 1到 j = 5 都可以正常匹配,但到了j=6时,发现 S[6] != T[6],匹配失败。
由于匹配失败,所以需要再次寻找,而根据next[6] = 3 可以知道我们需要将T串字符比对的位置 j = 6 回溯到 j = 3这个位置,而其S串字符比对的位置保持不变。 然后再次进行匹配,发现 S[6] != T[3],仍然匹配失败。
由于匹配失败,所以仍然需要再次寻找,而根据next[3] = 1 可以知道我们需要将T串字符比对的位置j = 3 回溯到 j = 1 这个位置,而其S串字符比对的位置保持不变。 然后再次进行匹配,发现 S[6] == T[1],匹配成功,然后S串和T串的字符比对位置依次向后移动,直到 彻底匹配成功 或者 T串或S串结束。
具体的程序如下:
- 什么是PMT?
int Index_KMP(char* S, char* T, int pos)
{
int j = 1;//子串T下标
int i = pos;//主串S下标
int next[255];//存储下标
get_next(T, next);
while(i <= S[0] && j <= T[0]){
if(j == 0 || S[i] == T[j]){
//若子串回到原处或者成功匹配
++j;
++i;
}
else{
j = next[j];
}
}
if(j > T[0]){
//子串结束,匹配成功
printf("子串结束,匹配成功!");
return i - T[0];
}
else{
//主串结束,匹配失败
printf("主串结束,匹配失败!");
return 0;
}
}
好了,讲到这里,其实KMP算法的主体就已经讲解完了。你会发现,其实KMP算法的动机是很简单的,解决的方案也很简单,只要搞明白了PMT的意义,其实整个算法都迎刃而解。
- 如何获得next数组?(核心)
- 这是一个关键的问题,求next数组的过程完全可以看成字符串匹配的过程,即以模式字符串为主字符串,以模式字符串的前缀为目标字符串,一旦字符串匹配成功,那么当前的next值就是匹配成功的字符串的长度。
匹配的步骤如下:(next[1]默认为0,0没有特殊意义)
首先先记录下T串中对应位置next[2] = 1,然后比较对应位置的字符是否相等,发现匹配失败。
由于匹配失败,所以 j 不变,i 加一, 记录下对应位置next[3]=1,然后比较对应位置的字符是否相等,发现匹配失败,继续下列操作。
- 这是一个关键的问题,求next数组的过程完全可以看成字符串匹配的过程,即以模式字符串为主字符串,以模式字符串的前缀为目标字符串,一旦字符串匹配成功,那么当前的next值就是匹配成功的字符串的长度。
为了更好的理解,假设后面还有”ab“字符串,从j=3处回溯到 j =1处
- 总而言之,就是上面的字符串对应字符位置不断向前移动,后面字符串对应字符位置可能向前移动,也可能回溯,而每次比较字符是否相等之前,无论是否相等,都需要记录下对应位置 (next[i] = j),这样,next数组就完全求出了。
当然,这里的next数组还未进行优化,因为可能会有某些特殊的字符串导致出现匹配错误或者匹配时间较长,因此可以利用路径压缩的原理,将next[i]= j分条件设置成next[i] = next[j],记录下当前位置记录的上一个位置,节省时间,提高效率。
具体的程序如下:
void get_next(char* T, int *next)
{
int i = 1, j = 0;
next[1] = 0;
while(i < T[0]){
if(j==0 || T[i]==T[j]){//若相等或者子串回到初始位置
++j;
++i;
if(T[i] != T[j])
next[i] = j; //前缀不同,则保存j
else
next[i] = next[j];//前缀相同,则保存j的上一个位置
}
else{
j = next[j];
}
}
}