KMP 字符串匹配算法讲解

KMP 算法全称 Knuth-Morris-Pratt 匹配算法,常用于字符串匹配,时间复杂度 O(n + m),空间复杂度 O(n + m),其中 n 为文本串的长度,m 为模式串的长度。

举个例子,令文本串 T = “abaabaaabaaaabaaaaab” ,模式串 P = “aabaaaab” ,当 T 已经匹配到第8位,P 已经匹配到第6位,即 T[3..8] = P[1..6] 时,因为 T[9] ≠ P[7],故匹配失败。

对于朴素的匹配算法而言,此时 P 的下标应归 1,T 的下标应设成 4 重新匹配。但是,在这种情况下,由于 T[3..8] 已知,我们便可以通过找出P[1..6]的一个前缀和 T[3..8] 的一个后缀使其相等,即找出 P[1..6] 的一个前缀和 P[1..6] 的一个后缀使其相等,在这个例子中 P[1..6] = “aabaaa” ,找出的相等的最长前缀和最长后缀(不包括自身)为 “aa” ,这个过程可以在预处理中进行,故在进行下一轮匹配时T下标不变,P 下标改为 3 继续匹配,因为 T[7..8] = P[5..6] = P[1..2]。

推广到一般情况,若 T[i-q..i-1] = P[1..q],此时 T[i] ≠ P[q + 1],则我们可以计算出 P[1..q] 不包括自身的相等最长前缀和最长后缀的长度,记为 π[q]。如此一来,因为 T[i-π[q]..i-1] = P[q-π[q]+1..q] = P[1.. π[q]],匹配时可以不更改 T 下标,使 P 下标更改为 π[q],继续匹配 T[i] 和 P[π[q]]。由于 T 下标不降,故该过程的时间复杂度近似为 O(n)。

现在,我们有一个问题,那就是如何查找模式串的一个前缀的相等最长前缀和最长后缀的长度(不包括自身),即构建 π 数组。这个过程可以放在预处理中进行。为了构建 π[i],我们可以用模式串与自身相匹配的方法。仍以前文为例,模式串 P = “aabaaaab” ,当 P 分别匹配到第 6 位和第 2 位时,即 P[5..6] = P[1..2],因为 P[7] ≠ P[3],由于 P[6] = P[2] = P[1],所以下一轮匹配时 7 下标不变,3下标改为 π[2]+1(即为2)继续匹配。

推广到一般情况,当P分别匹配到i下标和q下标时,即 P[i-q..i-1] = P[1..q],若 P[i] ≠ P[q+1],则因为 P[i-q+π[q]-1..i-1] = P[π[q]..q] = P[1..q-π[q]+1],所以在下一轮匹配中i不变,匹配 P[i] 和 P[π[q]+1]。由于 i 不降,故该过程的时间复杂度近似为 O(m)。

KMP 算法的伪代码如下:

π[1] := q := 0
n := T . length
m := P . length
for i := [2 , m]
    while (q > 0 && P[i] ≠ P[q + 1])
        q := π[q]
    if (P[i] = P[q + 1])
        q := q + 1
    π[i] := q
q := 0
for i := [1 , n]
    while (s > 0 && T[i] ≠ P[q + 1])
        q := π[q]
    if (T[i] = P[q + 1])
        q := q + 1
    if (m = q)
        print i-m+1
        q := π[q]

当 P = “aabaaaab” 时,π 数组取值如下:

i P[i] π[i]
1 a 0
2 a 1
3 b 0
4 a 1
5 a 2
6 a 2
7 a 2
8 b 3

说到这里,我们不禁想:由于从表面看,这个算法里存在二重循环,这样能达到线性时间复杂度吗?

我们说,其实是可以的。观察 π 数组的构造过程,我们可以发现对于一切 i = [1, m],一定有 i > π[i],此时无论执不执行 while 循环则必有 π[i] ≤ π[i - 1] + 1,在最坏情况下最多执行 m 次 while 循环,而此时必有 i = m 。也就是说,π 数组的构造最多循环 2m 次。同理可得,匹配过程最多循环 2n 次。于是 KMP 算法的时间复杂度可近似为 O(m + n) 。

下面是几道 KMP 算法的习题:POJ1961,POJ2406,POJ2752,POJ3461。

你可能感兴趣的:(算法)