KMP2年前反复看的时候,对next数组和基本原理,记一遍忘一遍,最近要用到字符串匹配得问题,要造轮子的时候又想到它了,这次好好整理一遍,要再次深刻的理解一遍。
KMP–简介
kmp是由Knuth-Morris-Pratt三位发明者共同命名的
传统匹配
EFEFEVDDCSAFFFEFERFTEWV
当我们想让FEFER这个pattern字符串去匹配上面的text字符串时,我们通常的做法是尝试是回朔算法。
PS: 回溯算法是一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回。
第一步:匹配不正确在下一个字符重新开始
第二步:匹配成功继续对FEFER这个pattern字符串进行后续的验证
第三步:直到我们发现最后一个字符不一致,此次从第二个字符开始匹配的方式失败,
第四步:回朔,从第三个字符重新开始,失败,继续对第四个字符重新开始匹配,直至匹配成功。
这样的方式不难猜出,当被匹配的text字符串长度为N,匹配得pattern字符串长度为M,那么时间复杂度达到O(N*M)
KMP优化
EFEFEVDDCSAFFFEFERFTEWV
同样当我们想让FEFER这个字符串去匹配上面的字符串时,我们这次尝试的算法为KMP算法。
第一步:匹配不正确在下一个字符重新开始
第二步:匹配成功继续对FEFER字符串进行后续的验证
第三步:直到我们发现最后一个字符不一致,从这里开始发生改变,我们无需回到第二个字符开始
提取FEFE这个已匹配字符数
我们去寻找这个FEFER他的子串的前后缀最大公共子串
- F的前后缀子串都为空,相同子串为0
- FE的前后缀子串分别为{F} | {E},最大相同子串为0
- FEF的前后缀子串分别为{F},{FE} | {F},{EF},最大相同子串长度为1
- FEFE的前后缀子串分别为{F},{FE},{FEF} | {E},{FE},{EFE},最大相同子串长度为2
- FEFER的前后缀子串分别为{F},{FE},{FEF},{FEFE} | {R},{ER},{FER},{EFER},最大相同子串长度为0
计算下次应该在哪个位置重新开始匹配
移动距离=已匹配成功字符长度-最大相同子串长度
第四步:我们经过计算需要移动2=4-2个单位,所以我们直接从第四个字符开始匹配,省去了一些匹配次数
第五步:同理我们经过计算需要移动2=2-0个单位,所以我们直接从第6个字符开始匹配,省去了一些匹配次数,反复如此,直到匹配成功
提取FE这个已匹配字符数
- F的前后缀子串都为空,相同子串为0
- FE的前后缀子串分别为{F} | {E},最大相同子串为0
这样的方式不难猜出,当被匹配的字符串pattern长度为N,匹配得字符串长度为M,那么时间复杂度达到O(N+M),当然如果没有出现类似这种匹配字串的重复现象,他又会退回成回朔算法。
next数组的求法
我们应用到编程中,最难实现的点是如何根据待匹配的pattern字符串求出对应每一位的最大相同前后缀的长度
在程序中,我们可以将其转化为对next数组的求解
next数组next[k]本质是对pattern字符串的子串长度为k+1时的最大相同前后缀子串长度
例如abab:如果列出它最大前后缀子串长度表(此处所说子串不包含本身字符串)
k=0 | a | k=1 | ab | k=2 | aba | k=3 | abab | |
---|---|---|---|---|
前缀子串(s[0~k]) | 空集 | {a} | {a}{ab} | {a}{ab}{aba} |
后缀子串(s[(3-k)~3]) | 空集 | {b} | {a}{ba} | {a}{ab}{bab} |
相同前后缀子串长度 | 0` | 0 | 1 | 2 |
next[0] | next[1] | next[2] | next[3] | |
next数组 | -1 | -1 | 0 [a的下标] | 1 [b的下标] |
由此可以得出:
next[k]是s[0~k]字符串的最长相等前后缀的前缀最后一位的下标
以此来更好的将其代码化:
了解到这里我们将抽象的语言已经转化为代码的语言,接下来实现的方式我们选择递推,通过已知的next[0]~next[k-1]来推出next[k]
public void getNext(char s[], int len){
int j = -1;
next[0] = -1;
for(int i = 1; i < len; i++){
while(j != -1 && s[i] != s[j + 1]){
j = next[j];
}
if(s[i] == s[j + 1]){
j++;
}
next[i] = j;
}
}