KMP算法
KMP是求解字符串模式匹配的经典算法。
字符串模式匹配问题:给定字符串pattern
(模式),和字符串text
,找出所有pattern
在text
中出现的位置。为了方便,下面的讲述中会用到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 = 0
,pos += 1
。
观察发现,pattern[0:3]
的三个字符("ABC"
)两两不同,容易判断出,位置pos+1
和pos+2
,必然不匹配,所以可以省略掉这两个位置的比较,直接令pos += 3
。而且,这个特性和text
无关,是pattern
自己的特点,可以描述如下:当i==3
时,如果pattern[i] != text[pos+i]
,可以令pos += 3
。
例2:如果pattern[0:5]
是ABCAB
,如下图所示。和之前的情况一样,可以直接跳过pos+1
和pos+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