一、初步理解
「天勤公开课」KMP算法易懂版 讲得很好
KMP 算法:
从主串中快速找出想要的子串(模式串)。幼稚模式串匹配算法 相比会出现比较指针回溯,效率低。
特点:
1,比较指针左边,上下两个子串匹配。
2,比较指针左边,模式串中有公共前后缀。
关键:
移动模式串,使得公共前后缀里的前缀 直接移动到原来的后缀所在位置。
(因为前后缀是匹配的,移动之前上下是匹配的,所以把前缀移动到后缀 上下也是匹配的。)
而且中间没有跳过 可能匹配的情况,现在只需要直接比较 指针所在位置即可。
实际上,分析时只需要考虑模式串。
条件:
1,如果模式串中有多对前后缀,我们要取最长的那对。
2,前后缀长度要小于比较指针左端的长度。
(如果取等于,则不需要移动了,没有意义。)
考虑一个新的模式串:
这里最开始下标对应为1(方便)。
1,假设第1个就不匹配了,那么模式串前移一位,重新比较。翻译成不含有串的移动表述方式:让1号位的字符(A) 与主串下一个位置的字符(B) 比较。
2,假设第5位不匹配,由于前后缀长度为2(AB),那么第3号位(2+1,A)与主串当前位比较。
得出一个结论,那个数字为:最大公共前缀长度+1
除了第一句话,后面的话除了数字不同,其他都一样,故将第一句话标记为0.
将语句变成数字,则为下图:
这个数组表示当模式串不匹配时,下一步哪个位置该与主串当前位比较,故叫做next数组。
二、实战代码
视频来源:天勤考研数据结构:KMP算法 我直接从p3开始看起。也讲的很好
1,字符串数据结构
//考研数据结构中好用的两种
//定长存储结构
typedef struct {
char str[maxSize + 1]; //末尾要加'\0'作为标志
int length; //为了考研答题方便
}Str;
//变长存储结构
typedef struct {
char *ch;
int length;
}Str;
//变长存储结构使用
Str S;
S.length = L;
S.ch = (char*)malloc((L + 1) * sizeof(char)); //分配存储空间
S.ch[length范围内的位置] = 某字符变量; //赋值
某字符变量 = S.ch[length范围内的位置]; //取值
free(S.ch); //释放,参数为首地址
串,这部分下标都是从1开始计数。
2,朴素解法
#include "pch.h"
#include <iostream>
#include <vector>
#include<algorithm>
#include<string>
using namespace std;
//变长存储结构
typedef struct {
char *ch;
int length;
}Str;
//朴素版查找主串所包含子串的位置
int navie(Str str, Str substr) {
int i = 1, j = 1, k = i; //下标从1开始。i为主串指针位置,j为子串,k记录主串和子串第一个开始比较的位置
while (i <= str.length && j <= substr.length) {
if (str.ch[i] == substr.ch[j]) {//如果相同,两者指针同时前移
i++;
j++;
}
else {
j = 1; //否则,j重新回到开始
i = ++k; //k需要前移一位,主串从i这个位置重新比较。可自行模拟一遍
}
}
if (j > substr.length) //说明子串已经比较完成,有完全匹配的。
return k;
else
return 0;
}
int main()
{
Str str, substr; //abc bc
str.length = 3;
str.ch = (char*)malloc((str.length + 2) * sizeof(char)); //因为第0位我们这里没用到
str.ch[0] = ' ';
str.ch[1] = 'a';
str.ch[2] = 'b';
str.ch[3] = 'c';
str.ch[4] = '\0';
substr.length = 2;
substr.ch = (char*)malloc((substr.length + 2) * sizeof(char));
substr.ch[0] = ' ';
substr.ch[1] = 'b';
substr.ch[2] = 'c';
substr.ch[3] = '\0';
int res = navie(str, substr);
cout << res;
return 0;
}
3,KMP解法
https://www.bilibili.com/video/av76933202?p=3
由naive算法改装而来:
//KMP版查找主串所包含子串的位置
int KMP(Str str, Str substr, int next[]) { //多加一个next数组 参数
int i = 1, j = 1; //下标从1开始。i为主串指针位置,j为子串。因为i不需要回溯,所以k不再需要
while (i <= str.length && j <= substr.length) {
//如果相同,两者指针同时前移
if (j==0 || str.ch[i] == substr.ch[j]) { //if条件需要修正,因为j=0时有特例情况
//j=0为特殊标记,代表i跳过当前位置前移1位,且把j设置为1。正好或上j==0后,下面代码达到效果。
i++;
j++;
}
else {
//i不需要回溯,j由next数组提供
j = next[j];
}
}
if (j > substr.length) //说明子串已经比较完成,有完全匹配的。
return i - substr.length; //因为没有了k,需要这样计算出来
else
return 0;
}
4,求解next数组
直接模拟手工方法,时间复杂度较大,重复操作。
https://www.bilibili.com/video/av76933202?p=4
next原理
针对模式串(子串)求next数组。复制一份,如下。
我们假设Pj这个位置不匹配。下面一行P1到Pt-1是t-1个,当作前缀FL;上面一行Pj-t+1到Pj-1也是t-1个,当作后缀FR。
挪动位置,假设红色部分完全相同,即FL=FR,也就是Pj前面的前缀和后缀相同,长度为t-1,那么有 next[j] = 前缀长度+1 = (t-1)+1 = t ,我们想推导一下next[j+1]的值
(1)如果Pj=Pt,则相当于FL和FR增加了一个长度,那么next[j+1] = next[j]+1 = t+1
(2)当Pj不等于Pt时,有点像不匹配时的主串和模式串,为了区分,叫假主串,如下图。
Sk状态中,1~4匹配,5位置不匹配。我们想办法往Sk+1跳,就是为了解决5位置的不匹配。
如何跳:在有前面next数组的值(已经求得)时,查对应next的值就可以跳了。(这里可以手动模拟验证,FL和FR的内容为“AB”,故next[5]为2+1=3,只需要将模式串跳到第3位进行比较即可)
同样,假模式串也类似操作。
将假模式串往前跳,翻译成t重新指向新的值比较好,也就是通过next数组的值重新赋值给t。这个过程t 可能需要多次调整,即t可能被多次赋值(跳多次),直到Sk处的不匹配被解决,也就是Pj != Pt被解决。
Pj != Pt被解决了,就回到了第(1)种情况。也就是1~被调整后的t子串( P1 ~ P新t)和上面一行匹配。此时调用(1),next[j+1] = 调整后的t + 1
(这个过程j是不动的)
当然,t在被赋值时,可能出现0的情况。就是假模式串不管怎么移动,都找不到FL=FR,可以看作FL=FR=0。可以合并这种情况,next[j+1] = 0+1 = 1 ,也就是主串第j+1个位置直接和第1个位置(最开始的位置)的模式串比较。
故,总结如下:
翻译成代码
//求解next数组
void getNext(Str substr, int next[]) { //直接修改next数组,不需要返回值
//下标从1开始
//j指向假主串,t为图中假模式串左端的长度。但t在求解过程中,t可能被next数组赋值为0
int j = 1, t = 0; //t的初值为0,直接满足if的条件,执行next[2]=1
next[1] = 0; //最开始next值设为0,作为标记
while (j < substr.length) { //j要小于等于长度,但如果取=也进入,则next[j+1]越界,故不能取=
if (t==0 || substr.ch[j] == substr.ch[t]) { //当t=0时,下面代码刚好满足next[j+1]=1
next[j + 1] = t + 1;
++t;
++j;
}
else
{
t = next[t]; //t=0时,next[j+1]=1
}
}
}
5,改进KMP算法
KMP缺点
举个特殊的例子,可以看出KMP算法的缺点。
第5个位置与主串发生不匹配。
直到j=0,执行++i; ++j;
1到4位置上的字符相同,因此next[5] 直接赋值为0即可。
对KMP的改进主要体现在next数组的改进上,改进后的数组称之为nextval数组,减少不必要的比较。
求解nextval数组
(1)如果Pj!=Pd,那么nextval[j] = d;如果Pj=Pd=Pc=Pb!=Pa,则nextval[j] = a
(2)如果Pk!=Pj,那么nextval[k] = j (=next[k]);如果Pk=Pj,那么nextval[k] = nextval[j] = nextval[next[k]] (相当于转换成只有k的参数,这里j=next[k])
将(2)转换一下:
1> Pk!=Pj,j=next[k],=> Pk!=Pnext[k] => Pj!=Pnext[j] (参数符号只是一个标记,换什么都可以)
nextval[k]=next[k] => nextval[j]=next[j]
2> Pk=Pj,j=next[k], => Pk=Pnext[k] => Pj=Pnext[j]
nextval[k] = nextval[next[k]] => nextval[j] = nextval[next[j]]
结论:
翻译成代码
//求解nextval数组
void getNextval(Str substr,int nextval[], int next[]) { //加上nextval参数
//下标从1开始
int j = 1, t = 0;
next[1] = 0; //最开始next值设为0,作为标记
nextval[1] = 0; //很明显,同上
while (j < substr.length) {
if (t == 0 || substr.ch[j] == substr.ch[t]) {
next[j + 1] = t + 1; //可以根据条语句,将下面的进行化简。没有必要留下next数组了
//根据前面分析求得
if (substr.ch[j + 1] != substr.ch[next[j + 1]]) //不等时
nextval[j + 1] = next[j + 1];
else //相等时
nextval[j + 1] = nextval[next[j + 1]];
++t;
++j;
}
else
{
t = nextval[t]; //t不会在j之后,所以nextval[t]的值早就求得了。故可以用nextval数组代替next数组
}
}
}
最终代码
//求解nextval数组-简洁版
void getNextval(Str substr, int nextval[]) {
//下标从1开始
int j = 1, t = 0;
nextval[1] = 0;
while (j < substr.length) {
if (t == 0 || substr.ch[j] == substr.ch[t]) {
//根据前面分析求得
if (substr.ch[j + 1] != substr.ch[t + 1]) //不等时
nextval[j + 1] = t + 1;
else //相等时
nextval[j + 1] = nextval[t + 1];
++t;
++j;
}
else
{
t = nextval[t]; //t不会在j之后,所以nextval[t]的值早就求得了。故可以用nextval数组代替next数组
}
}
}
(终于完了,有点绕)