参考资料:《数据结构-C语言版 第二版》 严蔚敏、李冬梅、吴伟民 编著
「天勤公开课」KMP算法易懂版 【up主:天勤率辉】
最浅显易懂的 KMP 算法讲解 【up主:奇乐编程学院】
帮你把KMP算法学个通透! 【up主:代码随想录】
先以文本串“aabaabaaf”,模式串“aabaaf”为例。介绍比较基础的“笨办法”(熟悉BF算法的可以立马跳过)
第一次尝试匹配
下标 1 2 3 4 5 6 7 8 9 主串 a a b a a b a a f 模式串 a a b a a f 此处不匹配
第二次尝试匹配(不匹配时模式串无脑右移一位)
下标 1 2 3 4 5 6 7 8 9 主串 a a b a a b a a f 模式串 a a b a a f 此处不匹配
第三次尝试匹配
下标 1 2 3 4 5 6 7 8 9 主串 a a b a a b a a f 模式串 a a b a a f 此处不匹配
第四次尝试匹配
下标 1 2 3 4 5 6 7 8 9 主串 a a b a a b a a f 模式串 a a b a a f 匹配成功
总结:
在用“笨办法”匹配的过程中,我们发现,第一次不匹配时,我们就可以获得信息:主串前5位一定为“aabaa”,且利用这一信息就可以得到第二、第三次匹配就是“多余的”。
由此我们提出需求:如何能跳过不必要的匹配呢?
在第一次匹配不成功后,我们可以将文本串抽象地看成
下标 1 2 3 4 5 6 7 8 9 文本串 a a b a a ? ? ? ? 利用这一信息就可以判断第二、第三次匹配一定失败,过程如下:
原第二次匹配(抽象后)
原第三次匹配(抽象后)
下标 1 2 3 4 5 6 7 8 9 主串 a a b a a ? ? ? ? 模式串 a a b a a f 不匹配
下标 1 2 3 4 5 6 7 8 9 文本串 a a b a a ? ? ? ? 匹配串 a a b a a f 不匹配 不难发现,这里产生的信息,是可以由模式串本身得出的。也就是说,事先得出模式串的所有可能的不匹配点并找到对应的优化策略,即可在匹配时跳过不必要的匹配。
因此,我们接下来尝试做一次匹配前优化,也就是生成next数组。
如果模式串第一个就不匹配,当然不用优化,模式串必定得右移一位,但此时主串下标也应右移一位,因为如果主串与模式串如果第一个就不匹配,那么他们的匹配起始点一定不是当前下标 j 。这里要弄清楚,KMP算法主要通过移动模式串的指针来实现回溯,一开始的“笨办法”是主串指针和模式串指针一起移动的,在KMP算法中,除非主串的匹配子串不是从当前指针位置开始(如模式串第一个就不匹配),否则主串的指针只需不动,模式串的指针移动就相当于模式串向右移动。
下标 。。。 j j+1 j+2 j+3 j+4 j+5 j+6 j+7 主串 。。。 ? ? ?
? ? ? ? ? 模式串 a 如果模式串第二个不匹配,由于我们不知道不匹配的字符具体是哪个,因此用“?”代替(反正它一定不是“a”,这里的一定不是“a”的信息用于后面优化next数组)
下标 。。。 j j+1 j+2 j+3 j+4 j+5 j+6 j+7 主串 。。。 a ? ?
? ? ? ? ? 模式串 a a 显然这里也得右移一位,这里的右移与第一次不匹配的右移有些许不同,这里右移只需移动模式串即可,第一个不匹配时主串和模式串都需要右移。
如果模式串第三个不匹配,同样地
下标 。。。 j j+1 j+2 j+3 j+4 j+5 j+6 j+7 主串 。。。 a a ? ? ? ? ? ? 模式串 a a b 此时模式串仍该右移一位,但含义同样发生了变化,因为我们不知道匹配的主串[j+2]是不是"a",为了防止漏解,因此得右移一位检查主串[j+2]是不是“a”。
如果模式串第四个不匹配时
下标 。。。 j j+1 j+2 j+3 j+4 j+5 j+6 j+7 主串 。。。 a a b ? ? ? ? ? 模式串 a a b a 将模式串尝试右移一位,显然不行(如下表)
下标 。。。 j j+1 j+2 j+3 j+4 j+5 j+6 j+7 主串 。。。 a a b ? ? ? ? ? 模式串 a a b 不匹配 再右移一位,还是不行(如下表)
下标 。。。 j j+1 j+2 j+3 j+4 j+5 j+6 j+7 主串 。。。 a a b ? ? ? ? ? 模式串 a a 不匹配 后面分析过程略
综上:如果当模式串第四个不匹配时,向右移动一位与两位还是不匹配,因此可以优化掉这两步过程。
由此得出,当模式串第四个不匹配时,我们就可以直接将模式串右移三位,也就将我们提出的问题解决了。一开始的模式串匹配问题就可以直接化简为两步。同理,匹配串第五个匹配时,可以直接移动3位。匹配串第六个不匹配时,可以移动3位。这里不做分析,为后面做铺垫
现在我们就可以用“笨办法”优化“笨办法”了。上代码!!
#include <iostream> #include <cstdio> #include <cstdlib> #include <cstring> #define MAX (int)(1e1+5) #define _for(i,n) for(int i=1;i<=n;i++) int next[MAX]; int IndexKMP(char* str ,char* t_str,int p);//KMP算法查找是否匹配子串 void PrintNext(int n);//输出next数组 void GetNextFoolVersion(char* t_str);/“笨办法”求next数组 void GetChar(char* m_str,char* t_str,int i,int j);//复制字符串m_str的第i到j字符至t_str int main(int argc, const char * argv[]) { // insert code here... char t_str[]=" aabaaf"; char m_str[]=" aabaabaaf"; GetNext(t_str); PrintNext((int)strlen(t_str)-1); printf("匹配起始点:%d\n",IndexKMP(m_str, t_str, 1)); return 0; } int IndexKMP(char* str ,char* t_str,int p){ //利用模式串t_str的next函数求t_str在主串str中第p个字符之后的位置 //其中,t_str非空,1<=p<=str.length int i=p,j=1; while(i<strlen(str) && j<strlen(t_str)){ //两个字符串皆未匹配到串尾,则继续比较后续的字符串 if(j==0 || str[i]==t_str[j]){ printf("%c=%c\n",str[i],t_str[j]); ++i;++j; }else{ j=next[j]; } } if(j>strlen(t_str)-1){ return i-(int)strlen(t_str)+1; }else{ return 0; } } void GetNextFoolVersion(char* t_str){ //"笨办法"生成next数组 //第一个与第二个不匹配不用算,必定右移一位,因此从第三个不匹配开始算起 next[1]=0;//这里的右移要包括文本串的右移,因此为0。详情见IndexKMP函数if条件判断部分 next[2]=1; for(int i=3;i<strlen(t_str);i++){ //当第i位不匹配时,匹配串最多可以优化i-2步,因为直接跳过i-1步,就跳出了已匹配的所有字符 int j=1; for(j=1;j<=i-2;j++){ char *str1,*str2; int n=i-1-j;//需要比较的字符串的长度 str1=(char* )malloc(sizeof(char)*(n+2));str1[0]=' ';str1[n+1]='\0';//如果不这样初始化,默认会在0位上初始化结束符 str2=(char* )malloc(sizeof(char)*(n+2));str2[0]=' ';str2[n+1]='\0'; GetChar(t_str, str1, 1+j, 1+j+n-1); GetChar(t_str, str2, 1, n); if(memcmp(str1, str2, strlen(str1))==0){ /* int memcmp(const void *str1, const void *str2, size_t n) 把 str1 和 str2 的前 n 个字节进行比较。 如果返回值 < 0,则表示 str1 小于 str2。 如果返回值 > 0,则表示 str1 大于 str2。 如果返回值 = 0,则表示 str1 等于 str2。*/ next[i]=i-j; break; } } if(j==i-1){ next[i]=1; } } } void GetChar(char* m_str,char* t_str,int i,int j){ int n=1; for(;i<=j;i++){ t_str[n++]=m_str[i]; } } void PrintNext(int n){ _for(i,n){ printf("next[%d]=%d\n",i,next[i]); } }
可能一开始看不懂这个next数组怎么求出来的,next数组的值具体是什么意思,变量i,j什么意思,可以在单步调试稍微理解代码的基础上继续往后看。
从代码里可以看出“笨办法”求next数组代码量很大(或许有更简洁的方法),当匹配串也十分长时,“笨办法”时间复杂度很高的问题会在长匹配串的情况下暴露出来,因此我们得完全放弃“笨办法”。
我们可以发现上述的“笨办法”生成next数组过程其实在寻找最长公共前后缀。
如字符串“aabaaf”的前缀有:
“a” ,“aa”,“aab”,“aaba”,“aabaa”
后缀有:
“f”,“af”,“aaf”,“baaf”,“abaaf”
前后缀不会是整个字符串
补完前后缀的基础,现在以上面留的铺垫为例(当匹配串“aabaaf”的第五个不匹配时,它的next数组怎么找)
如果模式串第四个不匹配时
下标 。。。 j j+1 j+2 j+3 j+4 j+5 j+6 j+7 主串 。。。 a a b a ? ? ? ? 模式串 a a b a a 尝试右移一位
下标 。。。 j j+1 j+2 j+3 j+4 j+5 j+6 j+7 主串 。。。 a a b a ? ? ? ? 模式串 a a b a 它就是在比较,同为长度三的前缀“aab”与后缀“aba”是否相同,显然不相同,因此不匹配
所以继续右移一位
下标 。。。 j j+1 j+2 j+3 j+4 j+5 j+6 j+7 主串 。。。 a a b a ? ? ? ? 模式串 a a b 这次就是在比较,同为长度为二的前缀“aa”与后缀“ba”,显然也不同
因此继续右移
下标 。。。 j j+1 j+2 j+3 j+4 j+5 j+6 j+7 主串 。。。 a a b a ? ? ? ? 模式串 a a 这次就是在比较,同为长度为1的前缀“a”与后缀“a”,这回相同了。
因此当匹配串第五位不匹配时,匹配串可以直接向右移动三位。也就是将文本串指针固定在j+4,匹配串的指针回溯到2。(这里理解了,也就能看懂代码next数组值的含义。)
那么,我们大概知道next数组中的数值其实就是已匹配成功子串的最长公共前后缀的长度+1
因此我们将上面用“笨办法”求next数组的方法转换为,找最长公共前后缀问题。如下:
当第二个未匹配时,就要找“a”的最长公共前后缀,很显然没有, next[2]为1
当第三个未匹配时,就要找“aa”的最长公共前后缀,是“a” next[3]为2
因为最长公共前后缀长度为1,当匹配串第三位不匹配时,需要检查匹配串第二位是否匹配,因此next为2(最长公共前后缀长度+1)
当第四个未匹配时,就要找“aab”的最长公共前后缀,没有 next[4]为1
当第五个未匹配时,就找“aaba”的最长公共前后缀,是“a” next[5]为2
当第六个未匹配时,就找“aabaa”的最长公共前后缀,是“aa” next[6]为3
接下来在解释一个next数组求法的联系
当第五个未匹配时,我们已经知道“aaba”的最长公共前后缀长度是1
那么在第六个未匹配时,“aabaa”的最长公共前后缀其实是在前一结论的基础上查找的,也就是后缀“成长”为“aa”对应的前缀“aa”是否相同。因为之前的后缀“aba”与前缀“aab”已经不相同,后缀即使“成长”为“abaa”对应前缀“aaba”也一定不相同。
在后缀成长的时候有两种情况
1. 后缀的新增的字符与前缀对应的字符相同:如第五个next值,最长公共前后缀为“a”,到第六个next值在第五次上查找,后缀新增“a”与前缀对应的“a”刚好一样,那么next[6]=next[5]+1
2. 后缀的新增的字符与前缀对应的字符不同:如第三个next值,最长公共前后缀为“a”,但在求第四个next值时,后缀新增“b”与对应的前缀“a”不一致,那么这里就可以看成模式串第二个不匹配问题:
求完next[3]后的状态
下标 | 。。。 | j | j+1 | j+2 | j+3 | j+4 | j+5 | j+6 | j+7 |
主串 | 。。。 | a | a | ? | ? | ? | ? | ? | ? |
模式串 | a | a | |||||||
求next[4]时的初始状态
下标 | 。。。 | j | j+1 | j+2 | j+3 | j+4 | j+5 | j+6 | j+7 |
主串 | 。。。 | a | a | b | ? | ? | ? | ? | ? |
模式串 | a | a | |||||||
不匹配,就可以抽象看成“模式串第二个不匹配问题”:
下标 | 。。。 | j | j+1 | j+2 | j+3 | j+4 | j+5 | j+6 | j+7 |
主串 | 。。。 | ? | a | ? | ? | ? | ? | ? | ? |
模式串 | a | a | |||||||
模式串接下来下标得移动到next[2]的位置上检查前后缀是否相同
因此我们由此可写出简洁的next数组求法,上代码!!
void GetNext(char* t_str){ //生成模式串的next数组 int i=1,j=0; next[1]=0; while(i<strlen(t_str)){ if(j==0 || t_str[i]==t_str[j]){ ++i;++j; next[i]=j; }else{ j=next[j]; } } }
这个代码可直接粘在之前的程序中,加上对应的声明即可。
——————————————分割线————————————(后面内容待完善)
下面以模式串“abaabcac”为例
j | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
模式 | a | b | a | a | b | c | a | c |
next[j] | 0 | 1 | 1 | 2 | 2 | 3 | 1 | 2 |
next[1]=0这是默认的
因为“模式串”在匹配“主串”的时候,当“模式串”的第一个字符串就不匹配时,“主串”如果包含该“模式串”,就一定不是以第该位置为起点。因此“主串”的指针得移动到下一位。具体为什么是0,就得结合后面的代码一起理解。
“模式串”应回溯到起点,从头开始匹配(即主串与模式串的当前匹配成功字符个数为0),想想如果这里回溯到1位置的话,会造成主串的指针还没后移,模式串的指针已经移动到第一个位置了,因此为了配合主串指针移动到下一个的同时,模式串也刚好移动到第一位,因此0是比较合适的。
例如:
主串 | ..... | b | ? | ? | ? | ? |
模式串 | a | |||||
第一个就没匹配 |
如果第二个没有匹配会怎么办呢?
主串 | ..... | a | c | ? | ? | ? |
模式串 | a | b | ||||
第二个没有匹配 |
第一种算法会将这个情况,抽象地看成这样
主串 | ..... | a | ? | ? | ? | ? |
模式串 | a | b | ||||
第二个没有匹配,将这个情况抽象化 |
这时我们知道主串第i个“?”不和模式串第j个“b”匹配,因此我们接下来应该看看它是否能和第1个“a”匹配,我们不知道这个“?”是不是“a”,因此得抱着试一试的心态去看看。