首页 > 技术文章 > KMP算法的理解

lijihong 2015-08-18 23:26 原文

一直以来,对KMP算法都不是很理解。最近又对其学习一遍,总算有所收获,基本理解此算法的思想。其根本思想:尽可能多地利用已有知识,使匹配过程,目标串不走回头路。

1 问题定义

记:$S[0]S[1] \cdots S[M]$为目标(字符)串,$P[0]P[1] \cdots P[N]$为模式(字符)串,在目标串中找到模式串的位置。

2 普通串匹配算法

普通串匹配算法非常简单,一句话概括:遇到不匹配的位置,模式串回退至起始位置,目标串亦回退到当前匹配过程起始位置的下一个位置。图示如下:

失配发生时的匹配关系:

匹配一:

下一次的匹配关系:

匹配二:

根据匹配一的结果,要想匹配二成立,则首先要确保:$P[0]=P[1], P[1]=P[2], \cdots, P[K-1]=P[K]$,换一种表述方式,即

$$ P[0]P[1] \cdots P[K-1] = P[1]P[2]\cdots P[K] $$

得到如下事实:

结论一:如果上式成立,则直接比较$S[L+K+1]$与$P[K]$及后续的字符即可;【注:这是一种非常特殊的情况,可看做KMP的特例】

结论二:如果上式不成立,匹配二必然不成立,其实做了很多无用功。

3 KMP串匹配算法

下面我们对上节得到的结论二做一个扩展:结论二是目标串指针移动一位后失配的情况,此时如果按上述算法,依然往下移一位,此时如果要使目标子串$S[L+2] \cdots S[L+K-1]$与$P[0] \cdots P[K-2]$匹配,则需要有:

$$ P[0]P[1] \cdots P[K-2] = P[2]P[3] \cdots P[K] $$

依次类推,由此得到结论三:

结论三:要使目标串指针不改变,那么需要将模式串指针调整到位置$k$处,使得:

$$ P[0]P[1] \cdots P[k] = P[K-k+1]P[K-k+2] \cdots P[K] $$

满足上式最大的$k$便是我们要求的$next$函数值,这也是KMP算法的核心思想:利用已有匹配结果$P[0]P[1] \cdots P[K-1] = S[L]S[L+1] \cdots S[L + K - 1]$,尽量减少无效匹配。特别注意,我着重强调了“减少”二字。结论三也许是初学者最难理解的地方了。

结论三指出了:失配发生时不移动目标串指针,如何调整模式串的指针,使得匹配继续进行。可以看到:匹配发生时,目标串指针调整后的位置仅和目标串本身有关,故可以事先求出,放在一个数组中,这就是$next$数组。下面是$next$函数的求法:

$$

next[K]=\left{

  \begin{array}{c}

  a \\

  b

  \end{array}\.

$$

如果按结论三的计算方式,其时间复杂度可能是$O(N^2)$量级的。KMP算法给出了一种新的求$next$数组的方法——递推求解,从而确保时间复杂度为$O(N)$,其中$N$为目标串的长度。

这么有名的串模式匹配算法,在此不作详细介绍了。如果有不了解的请看参考文献的两篇文章。

  这里,我只准备介绍一下该算法核心next数组的含义(怎么求,相关博客也很详细)。很多文章介绍next数组的时候,一上来会介绍字符串前缀和后缀的概念,我这里也提一下。给定一个字符串T[0...n],其前缀有:T[0], T[0,1], ..., T[0...n-1],其后缀有:T[1], T[1,2], ..., T[1...n]。

  既然是串匹配,则必然会有目标串和模式串,当然目标串的长度要比模式串大。这些基础知识在此不再赘述。

  传统暴力匹配算法(所有介绍KMP算法都会介绍,我在此不做阐述),一旦模式串和目标串发生失配时,模式串将回退至起始位置,目标串将回退至该轮比较起始位置的下一个位置(有点绕哈)。KMP算法解决的问题是,失配发生时,确保目标串不发生回退,仅改变模式串的位置(有的文章也称为滑动模式串)。

  设:目标串为S[0]S[1]...S[m],模式串为T[0]T[1]...T[n],m>n,现在处于如下匹配状态:

                 ↓

  目标串:s[0]...s[i-j]...s[i-1]s[i]...s[m]

  模式串:    T[0]...T[j-1]T[j]...T[n]

                ↑

  即:目标串S[i]和模式串T[j]发生了失配,现在怎么才能保持上面那个箭头,也就是i不变呢?换句话说,S[i]下一次该与T[?]比较呢?我们做如下分析:

  要想模式串向前滑动一个位置,也就是S[i]与T[j-1]比较,我们要保证什么?我们必须要确保:

        T[0]T[1]...T[j-2] == T[1]T[2]...T[j-1]      (1),

  若(1)做不到,则必然会导致S[i]与T[j-1]之前就发生失配了。为什么?因为根据之前匹配,我们有:

        S[i-j]==T[0], S[i-j+1]==T[1], ... ,S[i-1]==T[j-1]  (2)

  模式串向前滑动一步,我们必须要保证:S[i-j+1]==T[0], S[i-j+2]==T[1], ... ,S[i-1]==T[j-2],才能将匹配进行到S[i]与T[j-1],综合上述,我们必须确保(1)式成立,否则滑动没意义;

  同样的,要模式串向前滑动两部,类似的,我们必须保证:T[0]T[1]...T[j-3] == T[2]T[3]...T[j-1]。——上述分析的这些,不都是T[0]T[1]...T[j]前缀和后缀吗?!

  因此,根据上面分析,我们不难得出下述结论:要确保目标串中指针不变,那么下一个匹配的位置就是最大公共前缀的下一个元素。因为大多数语言数组索引以0开始,所以下一个位置就是最大公共前缀的长度了,对不对?于是,我们得到:

  失配发生时,有:下一个匹配位置 = k, T[0]T[1]...T[k-1] == T[j-k]T[j-k+1]...T[j-1] ,这个值定义为next[j],即

                next[j]=k  (3)

  那next[0]是什么呢?它没有前缀啊,这里我们将其定义为next[0]=-1,为什么这么定义,我想可能是特殊处理吧!如果没有公共前后缀,此时next[j] = 0,这也是符合(3)式定义的。

  另外还有一种情况我们没有讨论,就是参考文献[2]的优化部分:(3)式的得出没有考虑T[k] != T[j]这种情况,如果出现这一情况,则可以进一步优化,有next[j] = next[k],这样就进一步减少了比较。

  综合上述,我们有next数组表达式如下:

  next[j] = { -1, j == 0;

       { k, T[0]T[1]...T[k-1] == T[j-k]T[j-k+1]...T[j-1], T[k] == T[j]

        {   next[k],  T[0]T[1]...T[k-1] == T[j-k]T[j-k+1]...T[j-1], T[k] != T[j]

       { 0,  others.

  关于next数组解释至此为止,上述为个人学习总结,如有问题,请留言。时间紧张,故写的比较简单。

 

附:参考文献:

[1]从头到尾彻底理解KMP:http://blog.csdn.net/tukangzheng/article/details/38438481

[2]KMP串匹配算法解析与优化:http://www.cnblogs.com/tr0217/p/4735279.html

  

推荐阅读