KMP算法真的很简单1

KMP 算法真的很简单

KMP 是字符串匹配的经典算法,曾经一度对其敬而远之,感觉很难写出来正确的 KMP 算法,这都是拜那些“教科书”所赐,在它们的教授下,不禁感觉 KMP 很难!

其实理解 KMP 算法很简单,今天就来看个究竟,我的目标就是从几个简单的数学等式推导出 KMP 算法,简单但严谨。

串匹配

先来回忆一下串匹配场景,不外乎是给定两个字符串 S T ,然后在 S 串中查找 T 串,如果查找成功就是匹配,否则就是不匹配。比如:

S = “avantar-titanic”; T = “avantar”; C = “tistan” ;那么结果就是 T 匹配 S ,而 C 不能匹配 S

朴素的串匹配算法

再来回顾一下朴素的匹配思想,从 S[i] 开始,逐个检查 S[i+pos] 是否与 T[pos] 相等,如果 S[i+pos] = T[pos] ,那么 pos++ ;如果 S[i+pos] != T[pos] ,那么 T 就要回退到 T[0] ,然后再从 S[i+1] 开始尝试匹配 T ,这就是下面的伪代码。

 

 

上面算法的问题就在于,如果出现了不匹配的情况, T 就要回退到 T[0] ,算法复杂度为 O(len[S]*len[T])

需要从头再来吗?

让我们来重新审视上面的问题,在 S 串中查找 T 串,我们使用表示法 C [i, j] 表示 C 的字串 S i S i+1 …S j 假设当前时刻 S 从位置 i 开始,已经匹配了 pos 个长度,也就是 S[i, i+pos-1] = T[0, pos-1] ,继续向下进行比较;

如果 S[i+pos] = T[pos] ,那么依然有 S[i, i+pos] = T[0, pos]

如果 S[i+pos] != T[pos] ,那么将有 S[i, i+pos] != T[0, pos]

这时候 朴素的串匹配思想就是回退 T T[0] S[i+1] T[0] 开始再次逐 字符的比较。然而仔细观察这个时刻,假设我们不让 T 回退到 T[0] ,而是回退到 T[j] 的位置,并让 S 停留在 S[i+pos-1] 的位置,并且满足

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

那么必然 j<pos-1 ,然后再从 S[i+pos] T[j+1] 开始 字符的比较,这样不就更加高效了吗?下面就来看看能不能找到这个 j

采用假设法,假设 j 已经找到了,那么等式 (1) 成立。

当匹配进行到 S[i+pos] != T[pos] 时,我们已经有 S[i, i+pos-1] = T[0, pos-1] ,取后面的 j 个字符串就有:

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

根据等式 (1) 和等式 (2) ,我们便可得到

T[pos-j, pos-1] = T[0, j] ----------------------------------(3)

也就是说,如果找到了这样的 j ,那么必然满足等式 (3) 。于是我们可以采用一个数组 P ,记住每个 pos 对应的 j ;也就是 P[pos] = j ,当 T[pos] 不能匹配时,就让 T 回退到 j ,继续和 S[i+pos] 匹配; pos 的范围是 0~len[T]

如果 j = 0 ,意味着 S[i+pos-1] = T[0] = T[pos-1]

如果这个 j 不存在呢,这是很可能发生的,世界不是总是这么美好的嘛,这个时候意味着 S[i+pos-1] != T[0] ,那么我们只能从 S[i+pos] T[0] 开始匹配了。

如果对应于每个 pos 我们都找到对应的 j ,那么不就可以改进上面朴素的匹配算法了吗?姑且把这个结果存储在数组 next 中,于是 next 的定义就是:

next[pos] 表明如果在 pos T[pos] S[m] 不能匹配,但是 T[0, next[pos]] = S[m-next[pos]-1, m-1] 成立,于是应该回退到 T[next[pos]+1] 处和 S[m] 进行匹配。

这时候匹配算法的逻辑就像下面那样:

If S[i] = T[j] then // 如果匹配就继续向后

       i++

       j++

       if j = Len[T] then

              find a match position

       endif

else

       loop // 找到使 S[i] = T[j’] j’

              j = next[j]

until S[i] = T[j] or j < 0

if j < 0 then // j < 0 表明 S[i] != T[0] ,从 i+1 位置匹配 T[0]

       j = -1

endif

i++

       endif

改进后的匹配算法

根据上面的思想,那么我们可以很容易的写出匹配算法的代码。

 

 

 

计算next数组

现在另外一个问题出现了,如何求next数组呢?根据前面的分析可以知道next数组只和T串有关,再说一下next的定义:

next[pos]表明如果在posT[pos]S[m]不能匹配,但是T[0, next[pos]] = S[m-next[pos]-1, m-1]成立,于是应该回退到T[next[pos]+1]处和S[m]进行匹配。

最简单的情况开始,next[0]=-1,因为如果T[0] != S[m]那么只能尝试T[0] ? S[m+1];

不难理解,如果T[0] = T[1],那么next[1] = 0,否则next[1] = -1

对于next[pos] = j 满足T[pos-j, pos-1] = T[0, j],因此可以通过蛮力方法找到这个next数组,只是效率太低,还有更好的方法吗?

如果当前已知next[0],...,next[pos-1],能否通过它们的值来计算next[pos]呢,假设next[pos-1] = j

1 如果T[j+1] = T[pos],那么结合T[pos-j-1, pos-1] = T[0, j]可知

T[pos-j-1, pos] = T[0, j+1]

于是next[pos] = j+1

2 如果T[j+1] != T[pos],那么T[pos-j-1, pos] != T[0, j+1],这时候如何求next[pos]呢?假设next[pos] = j’,如果j’ > -1,那么有T[pos-j’, pos] = T[0, j’]

再考虑对于任意i < pos-1, next[i] = p’ T[i-p’, i] = T[0, p’],那么p’肯定<j

现在已经有T[pos-j-1, pos-1] = T[0, j],于是T[pos-p’-1, pos-1] = T[0, p’]

如果T[pos] = T[p’+1]的话,就意味着T[pos-p’-1, pos] = T[0,p’+1]

于是next[pos] = next[i] +1

看来我们找到了求next数组的方法,看看是不是和匹配算法很相似呢,伪代码如下所示:

next[0] = -1

i = 1

j = -1 // j next[0]开始

// 循环直到i=Len[T]

If T[i] = T[j+1] then // 如果匹配就继续向后

       next[i] = j+1 // 计算next[i]

       i++

       j++

else

       loop // 找到使T[i] = T[j’+1]j’

              j = next[j]

until T[i] = T[j] or j < 0

if j < 0 then // j < 0表明T[i] = -1

       next[i] = j = -1

endif

i++

       endif

c++代码如下:

KMP匹配算法

KMP相对于朴素的匹配思想就是当不匹配时T不必回退到T[0]从而提高了匹配效率。

上面的代码似曾相似啊,其实它就是KMP匹配算法,简单调整一下代码逻辑就和常见的KMP算法相似了。

KMP算法其实不难,几个简单的数学等式就能推出来了嘛,其余的复杂性证明就略过了。

 

没有更多推荐了,返回首页