kmp算法拆解
一、简介
KMP算法是一种高效的字符串匹配算法,由D.E. Knuth、J.H. Morris和V.R. Pratt共同提出。
二、KMP算法
1.基本概念
子串:子串是由一个字符串(也称为父串或主串)中任意个连续字符组成的子序列。
子序列:子序列是从原始序列中抽取一部分元素构成的新序列,这些元素的相对顺序保持不变,但不需要连续
二者关系示意图:
2.朴素匹配算法
案例:
2.1朴素匹配概念
前提:主串长度为n、模式串长度为m
**概念:**将主串中所有长度为m的子串与模式串对比,直到找到一个完全匹配的字串,或者所有子串都不匹配
2.2朴素匹配原理
2.2.1key1:在朴素算法中最多对比几个子串?
答案:n-m+1
最多->最坏的情况 :
-
即匹配每次匹配到模式串的最后一个失败
-
匹配到最后一个子串
S:aaac aaac aaac
T:ab ab ab
设初始指针都是1
显然找到最后一个子串的位置,即最多对比的子串数即可求
-
观察法:n-m+1
-
我们知道每次匹配失败模式串指针就会回溯至1,最差情况回溯至1,回溯坐标为m-(m-1)
父串也需要同样的回溯长度 m-1,回溯到n-(m-1)=n-m+1
key1结论:最多对比n-m+1个子串
2.2.2key2:通过key1,我们知道了模式串匹配失败的指针操作j=1,那么父串指针i呢?
-
子串回溯最差j = 1
-
父串回溯最差i = n-m+1 即匹配到最后一个子串
-
如果是中间的子串呢?很简单 把i = n-m+1中的n和m换成指针位置即可i=i-j+1
-
父串匹配失败需回溯再移动至下一位 即i = (i-j+1)+1 = i-j+2
key2结论:父串指针i需移动至i-j+2
2.2.3key3:代码实现
2.2.4key4:上述算法以及对应代码时间复杂度是多少?
结论:O(nm)
原因:按照最坏的情况,n-m+1个子串,每个子串匹配m次,
共匹配次数是:m(n-m+1)=mn-m²+m
实际情况中,父串长度n远大于模式串m,所以复杂度为nm,而不是m
3.kmp算法
3.1情景举例:
父串:googllgoogle
子串:google
3.1.1key5:此时我们如果按照朴素匹配,会怎么样?
父串:指针回溯i-j+1=1,向前移动
子串:指针回溯1
变成如下
父串:googllgoogle
子串: google
很显然不对,如上,会进行无效回溯。
我们需要做的是,以上述为例,第6个元素发生匹配失败,则直接
父串:googllgoogle
子串: google
key5结论:在绝大多数情况,朴素匹配会无意义指针回溯,所以需要优化。
思考:在日常生活中,我们会对例子中的匹配失败进行这么多遍的回溯吗?
3.2kmp算法
3.2.1key6:kmp算法思路引入
在3.1的例子中,模式串的第6位发生了不匹配,3.2的例子是在第5位发生了匹配,这里该如何做呢?和3.1中一样吗
正确做法:
结合3.1中 j=6发生不匹配,则让j指针回到1
依次类推:当j=1、2、3。。。时发生不匹配,j的回溯分别是?
对于j的回溯,我们可以给出一个next数组,放入数据,在第k个字符不匹配时候,通过数组拿取指针的回调
key6结论:kmp的算法意在取消i无意义的回溯,并且给j指针的回溯指定了位置形成一个数组
next[模式串匹配失败位置]=j回溯的地方
3.3kmp算法核心之模式串指针回溯数组next
3.3.1key7:如何求next数组?
以google举例,在k处发生匹配失败,则把1~k-1看成一个串S
S串的前缀:包含第一个字符且不包含最后一个字符的子串
S串的后缀:包含最后一个字符且不包含第一个字符的子串
next[j]=最长相等前后缀长度+1
第1个字符发生匹配失败:j=0
第2个字符发生匹配失败:j=1
这两个是固定的
第3个字符发生匹配失败 前缀 g 后缀o 最长相等为0 所以 next[3]=0+1 =1
。。。
第5个字符发生匹配失败 前缀g 后缀g 最长相等为1 所以next[5]=1+1=2
key7结论:通过发生匹配失败的位置,并在这个位置之前的串中找最长相等的前后缀长度并+1,填充数组
3.3.2next和kmp代码实现
比较kmp算法和朴素匹配,发现两个不同点:
-
i指针不需要回溯
-
j指针的回溯遵循了next数组
KMP算法性能分析
短一点的模式串我们可以手算,但是实际应用肯定不会这么短,参考百度搜索,所以需要代码实现
在kmp算法中,有一个关键点,不管是在kmp还是在kmp求next数组中,总有一个指针不回溯,这就是优化的地方
时间复杂度为O(n+m)
3.4kmp针对next数组再优化
3.4.1key8:为什么需要再优化
aba**?**
ababa
根据3.3的内容,可知ababa的next数组是01123
示例中,在模式串的第4个发生匹配错误,按照常规kmp的next数组应该回溯到第2个,但是很显然,第2个也是b
无效匹配出现
结论:next数组回溯的坐标对应元素和匹配失败发生位置的元素相同,需要解决该无效匹配
3.4.2key9如何优化next数组:nextval数组
aba**?** aba**?** aba**?**
ababa ababa a****b…
上述流程中,如果按照之前的next数组会进行如上图的无效匹配,如何有效(如黄色底色示)
创建一个基于next数组的nextval数组
改进关键点:如果我们按照next回溯,在下标3处 a 发生错误,会跳到S[next[3]]=a,又跟a比,浪费,需要优化
就是有点绕,但是不难:
-
目标:防止回溯的模式串的值和发生匹配错的模式串值一样
-
对模式串遍历,目的就是防止T[next[i]]==T[i]
-
针对T[next[i]]==T[i],创建一个新数组nextval[i]=nextval[next[i]]
-
如果没有T[next[i]]==T[i],就还是正常复刻 nextval[i] = next[i]