回顾/本期梗概
上期我们学习了离散化(空降链接),本期我们将学习KMP算法。
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克劳特——莫里斯——普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式的局部匹配信息。KMP算法的时间复杂度。
1、KMP的原理
如:有上图所示的父串s和子串p,如果要匹配出子串p在父串s中是否出现过或者出现了几次。
暴力的做法如图所示,第1次父串s从第1的字符开始和子串匹配,由第1次匹配可知,S[2]= 'B' 而t1='A',显然匹配不成功。第3次从s的第3个字符开始尝试和子串p匹配......
如果s的长度为n,p 的长度为m,则暴力求解的时间复杂度是。
问题:是否直接跳过中间肯定失败的3次匹配呢?
回答:答案是肯定的,因为第 1 次匹配,发现 s[ 8 ] != p[ 8 ] 时,观察 p 的前 7 个字符发现,p 有 3 个前缀子串和 3 个后缀子串完全匹配,可以利用这个性质,直接跳过中间的 3 次无效的匹配。
2、next 数组的计算方法
(1)加快匹配的原理
上述案例中,我们使用 i 来循环 s 主串,j 循环 p 子串,我们希望:s 主串的 [ i - j + 1,i ] 字符和 p 子串的 [ 1,j ] 字符完全一致,也就是 s 串以 s[ i ] 结尾的长度为 j 的子串和 p 字符串的前 j 个字符一致,显然样例可以分析出 i = 7、j = 7 时是能做到的,接下来检验 s[ i + 1 ] 和 p[ j + 1 ]的关系。
由于 s[ 8 ] != p[ 8 ],此时 i 和 j 无法继续增加,要尝试继续匹配,必须减少 j,且为了提升效率,新的 j 越大越好。因为 p[ 1... 3] == p[ 5...6 ],因此新的 j 最大可以设置为 3,到下图所示的状态,重新检验 s[ i + 1 ] 和 p[ j + 1 ] 的关系。
当发现 s[ i + 1 ] != p[ j + 1 ] 时,j 的最大可以回跳的值 == p 字符串 1... j 这 j 个字符前缀和后缀相等的最大长度。
(2)next 数组的含义
定义 next 数组的含义:next[ j ] 代表字符串 p 中前 j 个字符的最大相同前缀和后缀的值。特别注意:这个值不含本身,比如字符串“ ABABA ” 最大相同前缀和后缀的值是 3,而不是 5,因为任何字符串如果取自身,那么从前缀和后缀必定相等。因此 next[ 0 ] = next[ 1 ] = 0。
(3)如何高效匹配
当 s[ i + 1 ] != p[ j + 1 ],通过 j = next[ j ] 回跳,如果 s[ i + 1 ] == p[ j + 1 ],那么往后继续匹配。(注意:为了避免 next 和系统函数同名,一般 next 数组定义时可以使用缩写 ne )
(4)如何高效求 next 数组
类似字符串 s 和 p 匹配的过程,只是求 p 的 next 数组本质上是 p 自己和自己匹配的过程。