KMP算法

KMP是求解字符串模式匹配的经典算法。

字符串模式匹配问题:给定字符串pattern(模式),和字符串text,找出所有patterntext中出现的位置。为了方便,下面的讲述中会用到python语言。

KMP算法思想

我们从最简单的字符串匹配算法开始,一步步推导出KMP算法的思想。

对于字符串匹配,容易想到以下算法:

pos, i = 0, 0
while pos + len(pattern) < len(text):
  while True:
    # 进行到pattern末尾,找到一个匹配
    if i >= len(pattern):
      print pos
      break
    if pattern[i] == text[pos + i]:
      i += 1
    else:
      # 发现不同字符,当前位置不能匹配
      break

  i, pos = 0, pos+1

KMP算法源自于以下考虑:当上述程序运行到最后一行时,能否将pos多增加几位,i能否不从0开始?

例1:如下图所示,发现pattern[3] != text[pos + 3]时,break出内层循环,然后i = 0pos += 1

图片说明

观察发现,pattern[0:3]的三个字符("ABC")两两不同,容易判断出,位置pos+1pos+2,必然不匹配,所以可以省略掉这两个位置的比较,直接令pos += 3。而且,这个特性和text无关,是pattern自己的特点,可以描述如下:当i==3时,如果pattern[i] != text[pos+i],可以令pos += 3

图片说明

例2:如果pattern[0:5]ABCAB,如下图所示。和之前的情况一样,可以直接跳过pos+1pos+2,直接令pos += 3。此外,我们还可以发现,i可以直接等于2,而不必从0开始,因为pos += 3之后,pattern的前两个"AB"自动就和text后面两个"AB"对齐了。

图片说明

上面观察到的两个规律,都是pattern字符串本身固有的,和text无关。因此,我们可以提前将这些特征提取出来,用以加速在任何字符串中查找pattern的过程。

确切地说,我们希望预先计算出:当break时,pos可以增加多少,i可以从多少开始。

我们将pos逐步加1,直到已知的部分完全匹配为止。如下图所示

图片说明

可以发现,因为pattern[0:5]存在一对相同的前缀和后缀,长度为2,所以当pos增加到3时,后面的两个字符完全重合。这正是例2中的情况。如果不存在这样的后缀,如例1中的情况,pos就可以一直增加到i

我们用NEXT数组表示这么一个数组:NEXT[k]表示字符串pattern[0:k]包含的最大的相同前后缀的长度,如果不存在相同的前后缀,则NEXT[k] = 0。更新我们的程序如下(注意到只修改了最后一行)

pos, i = 0, 0
while pos + len(pattern) < len(text):
  while True:
    # 进行到pattern末尾,找到一个匹配
    if i >= len(pattern):
      print pos
      break
    if pattern[i] == text[pos + i]:
      i += 1
    else:
      # 发现不同字符,当前位置不能匹配
      break

  i, pos = NEXT[i], pos+(i-NEXT[i])

:为什么一定是最大的前后缀呢?因为在增大pos的过程中,重合部分逐渐减小。第一次遇到字符完全匹配时,重合的必然是最大的前后缀。

求解NEXT数组

为了求NEXT数组,我们可以把两个pattern字符串对齐,然后将其中一个向右移动,直到重合部分完全匹配,此时重合部分的长度就是我们要求的相同前后缀的长度。这个过程和上一幅图中描述的一样。

不过,假设已经求出了NEXT[0]NEXT[k],现在求NEXT[k+1],我们可以利用已经求出的部分结果加速这个过程。

首先我们有如下发现:NEXT[k+1] <= NEXT[k] + 1。也就是说,随着k增加1,NEXT[k]最多增加1。

证明:根据定义,字符串pattern[0:k+1]中,长度为NEXT[k+1]的前后缀相等。将这一对前后缀各自去掉最后一个字符,前缀仍然是长度为NEXT[k+1]-1的前缀,而后缀变成了pattern[0:k]的长度为NEXT[k+1]-1的后缀。如下图所示。

图片说明

所以pattern[0:k]至少有一对长度为NEXT[k+1]-1的相同前后缀,因此NEXT[k] >= NEXT[k+1]-1。所以,NEXT[k+1] <= NEXT[k] + 1

所以,我们得到了一个上界,可以不用从头开始,而是从重合部分长度为NEXT[k] + 1的地方开始。如下图所示。

图片说明

此时,如果对齐点右边的字符相等,就立刻得到NEXT[k+1]=NEXT[k]+1。如上图所示。

如果不相等,也不必一步一步向右移动。如下图所示,利用已经计算好的NEXT数组,可以继续将下面的字符串往右跳跃:

就像寻找字符串匹配一样,利用已经求出的NEXT数组的部分,继续将下面的字符串往右跳跃。如下图所示

图片说明

这样一步步跳的过程中,左边始终保持着匹配。而右边那个字符一旦也相同了,就找到了一对相同的前后缀。如果直到左边为空了,右边这个字符仍然不匹配,就意味着要找的相同前后缀不存在,此时返回0。

完整的KMP算法C++实现代码可以参见:https://github.com/yczhangsjtu/Algorithms/blob/master/hihocoder/kmp.cpp