考研中,复习KMP算法的时候,以为KMP虽然写起来很简单,可是理解起来有些难度,属于那种看一遍以为本身懂了,过一下子再去想的时候又想不清楚了。因此写篇总结性的文章,以本身的理解解读一下KMP算法,加深理解的同时,也但愿读者有所收获。javascript
KMP算法在网上有个俗称叫“看毛片”算法,为了给KMP正名,我特意查了一下,KMP之因此叫KMP,是由于这种算法是D.E. Knuth 与 V.R. Pratt 和 J.H. Morris 三人同时发现的,人们便以三人名字的首字母KMP命名了这个算法。😄java
KMP算法是一种改进的串模式匹配算法,就是在一个字符串中找到子串的位置。传统的作法很简单,相似于暴力求解。好比 主串是ababc
, 模式串是abc
的传统作法中,是这样匹配的算法
能够看到,传统作法的方式是模式串与主串一一比较,若是其中一个匹配失败,则从主串上次开始比较的下一个位置在来一遍。若是用i
表示主串的位置,用j
表示模式串的位置,则不难看出在传统作法下,i
和j
都有回溯,因此KMP改进传统算法的关键点就是如何减小i
和j
的回溯。数组
紧接上文,KMP是如何减小i
和j
的回溯的呢?咱们一点点来分析性能
这里,设主串为S1S2S3...Sn ,模式串为P1P2P3...Pm ,在上节传统算法的匹配过程当中有一个关键状态ui
table-1spa
为何这里是关键状态?由于咱们能够看到,传统算法中每次遇到这个状态时,就会开始回溯i
和j
进行下一轮的匹配了,因此改进算法的关键就从这里开始。设计
i
的回溯传统作法中下一个状态i
会回溯到i-j+1
,而j
会回溯到1
。如今咱们不要这么作,替代的作法是,不要回溯i
,把P1P2...Pm向后移动,来试图消除Si处的不匹配,进而开始S(i+1)及其之后字符的比较,使得整个过程继续推动下去。code
咱们假设上面这个状态为 Status(k)
,而在P1P2...Pm向后移动过程当中某处到达Status(k+1)
cdn
table-2
到达Status(k+1)
状态时,有两种状况可能发生:
i
和j
都加1,向后继续比较j
的回溯咱们能够看到,P1P2...P(t-1)与P(j-t+1)..P(j-1)重合,这是很常见的,模式串中不免会有一些重复子串,好比abcabcb
中,abc
就出现了2次。上面说,经过移动模式串而不变主串的方式避免了i
的回溯,那这里的重复项就是有效减小j
回溯的关键所在。
在传统作法中,当Si≠Pj时,j就回溯至1
,但其实全部中间的比较都是多余的,由于只有在Status(k+1)
状态时,P1...P(t-1)才能与主串中字符相对应,而此时只须要比较Si与Pt是否相等,也就是说,j只须要回溯到t
,前面t-1
项是重复的无需比较【由于P1...P(t-1)与P(j-t+1)...P(j-1)是相等的,而P(j-t+1)...P(j-1)在Status(k)状态已经比较过了】。从这里能够看出,j回溯多少,回溯到哪里,取决于模式串自己的重复项
由于P1...P(j-1)与S(i-j+1)...S(i-1)是相等的,咱们大可去掉,只关注于模式串自己的移动
table-3
咱们假设P1...P(j-1)为F,P(j-t+1)...P(j-1)为FR , P1...P(t-1)为FL ,则边框内部就是F串中先后相互重合的部分。当Pj处发生不匹配时,咱们下一个j
的位置就刚好是FL.length+1
或者FR.length+1
,也即Pt的位置。因此咱们只要求出模式串从P1到Pm位置发生不匹配时下一个j
的位置,就能够知道下一个状态Status
应该从哪里开始比较。
Next
在KMP算法中,把模式串中每个字符的下一个回溯位置用next[j]
表示,咱们接下来聚焦于如何求解next数组
以上面的table-3
为例,此状态下,next[j]已经求得,注意next数组求解的是当前字符匹配失败时的下一个j的位置,因此这里当Pj匹配失败时,next[j]=t 。 那么求解next[j+1]
分为两种状况:
j=j+1/t=t+1
时的状况,因此 next[j+1]=t+1这里有一个特殊状况须要另外说明,就是next[0]=-1,什么意思呢?就是模式串第一个字符发生不匹配时下一个位置为 -1 。这里你或许奇怪,为啥不是 0 ,而是 -1 ,-1是不存在的位置啊?这是由于若是next[0] = 0 ,则一旦第一个字符真的不匹配就会无限死循环。根据咱们上面说的 2)的处理,t=next[t],0位置的下一个回溯位置永远是 0 ,会死循环的。因此这里用 -1 做为一个标识,表示是第一个字符不匹配的状况。
Next数组求解出来后,就比较简单了,只要对照着Next数组来“跳”值,就能够在减小j
的回溯状况下与主串匹配。
模式串依据Next数组与主串匹配的过程,与Next数组求解的过程很是类似,惟一的区别是:求解Next数组时是模式串
与模式串
匹配的过程,且要记录Next数组 ; 而KMP串匹配时是主串
与模式串
的匹配过程,但须要使用Next数组做为指导。
以上就是KMP模式串匹配的核心算法思想,巧妙使用模式串中的重复项来减小i
和j
的回溯,提升算法效率。可是这个算法不是稳定的,由于性能的好坏其实重度依赖于模式串,若是模式串中几乎没有重复项,那算法就会退化为和传统算法差很少的性能
KMP算法主要是思想比较难理解,很绕,不画例子想想Si,Pj,Pt之间的关系,很难真正搞懂为何要这样去设计算法。可是明白原理后,实现起来是真的很简单。这里我用本身擅长的语言Javascript写了一个KMP,仅供参考
function getNext(pattern){
let i = 0 , j = -1
let next = [-1] //first pos
while(i<pattern.length-1){
if(j==-1 || pattern[i] == pattern[j]){
++i
++j
next[i] = j
}else{
j = next[j]
}
}
return next
}
const kmp = function(main , pattern){
let next = getNext(pattern) //get Next Array
let i = 0 , j = 0
while(i<main.length && j<pattern.length){
if(j==-1 || main[i] == pattern[j]){
++i
++j
}else{
j = next[j]
}
}
return i-pattern.length
}
module.exports = kmp
复制代码
上面我提到若是模式串几乎没有重复项的话,算法会退化。可是模式串中重复项过多的话,也是存在一些问题的。咱们举个极端的例子来讲明,好比对于模式串aaaaab
,它的next数组
以下:
b
不匹配时,
-> j
回溯到4
-> a≠b , j
回溯到3
-> ...
-> j == -1 , i 越界,退出循环,匹配失败
咱们能够看到,其实在b
不匹配时彻底能够直接跳到 j=-1 ,而不用从j=4,j=3...j=-1,由于这些位置上对应的字符都是a
,彻底只须要比较1次就足够了。这就是KMP算法改进的关键点。
由于next
数组是由前至后逐步创建的,就是说求next[j]
时,前j-1项next是已知的。因此咱们只需没走一步往前看一个就能够了。好比,求next[j]
时,
这里改进不难, 就是在构造next数组
时作一些小改动,下面是改进后的代码示例:
function getNext(pattern){
let i = 0 , j = -1
let next = [-1] //first pos
while(i<pattern.length-1){
if(j==-1 || pattern[i] == pattern[j]){
++i
++j
if(pattern[i]!=pattern[j]) //改进求Next数组
next[i] = j //
else //
next[i] = next[j] //
}else{
j = next[j]
}
}
return next
}
const kmp = function(main , pattern){
let next = getNext(pattern) //get Next Array
let i = 0 , j = 0
while(i<main.length && j<pattern.length){
if(j==-1 || main[i] == pattern[j]){
++i
++j
}else{
j = next[j]
}
}
return i-pattern.length
}
module.exports = kmp
复制代码
有问题,请评论区指正!