字符串——KMP算法

字符串——KMP算法

KMP算法问题类型

给定两个字符串,寻找其中一个字符串是否包含另一个字符串,如果包含,返回包含的起始位置。 如下面两个字符串:

char *T = "bacbababadababacambabacaddababacasdsd";
char *P = "ababababca";

普通暴力搜索的时间复杂度为O(mn),在暴力搜索中有大量重复搜索,KMP通过引入next数组大大降低了时间复杂度。

算法简述

后缀 对任意 $i∈[0, m - 1]$,$S[i ,m - 1]$ 为$S$的后缀。

前缀 对任意 $j∈[0, m - 2]$,$S[0 ,j]$为$S$的前缀。

KMP考察了P本身的特征,存储到一个next数组中。

​ $next(q) = max{k-1 : k < q \text{ and } P[0, k] \text{ is a suffix of } P[0, q]}$

next[q]存储的值为最长相同前缀后缀的长度k-1。

q 0 1 2 3 4 5 6 7 8 9
P[q] a b a b a b a b c a
next[q] -1 -1 0 1 2 3 4 5 -1 0

例,ababa的前缀有a,ab,aba,abab;后缀有a,ba,aba,baba。显然最长相同前缀后缀为aba,长度k为3,next数组值为2。

当字符串匹配时,我们不需要再一个一个遍历,P可以大幅向前移动。

假设当前匹配到模式串j位置时失配,KMP算法使用next[j]+1更新j的值。

移动位数 = (已匹配的字符数-1) - 匹配部分最后一位next值 = j - next[j] -1

例,

bacbabababababacambabacaddababacasdsd

ababababca

ababababca

移动位数 = 7 - 5 = 2,此时将P向后移动2位,继续匹配。

KMP伪代码

q := −1; 
for i:= 0 to n − 1 do:
	while q ≥ 0 and P[q + 1] != T[i] { q := next(q) }; //下一位匹配失败,迭代q去查询next
	if P[q + 1] = T[i] { q := q + 1 };
	if q = m − 1 { output i − m + 1; q := next(q) };

计算next数组

q 0 1 2 3 4 5 6 7 8 9
P[q] a b a b a b a b c a
next[q] -1 -1 0 1 2 3 4 5 -1 ?

如上表,假设此时我们已经计算了[0-8]位的值,现在我们计算第9位的值,我们可以轻易的从前一位q=8来计算。

此时next[8] = -1,即不存在相同的前后缀,那么next[9]即便存在相同的前后缀,最长也不超过1,检查q[0]与q[9],若相同,则next[9]=1。

同理,来看另一个例子。

q 0 1 2 3 4 5 6 7
P[q] a b a b a b a b
next[q] -1 -1 0 1 2 3 4 ?

假设我们计算了[0-5]的next值,现在需要计算第7位。

next[6]=4,因此next[7]最大不超过5,只要检查q[4+1]与q[7]是否相等,若相等则next[7] = next[6] + 1;若不相等,我们可以检查第二长的前缀的后一位。

第二长的前缀,是最长的前缀的一个最长前缀,即next[6] = 4,next[4] = 2,检查q[2+1]是否等于next[7],若相等,则next[7] = 2 + 1;若不相等,继续检查第三长的前缀的后一位。

计算next数组伪代码

next(0):= −1; k := −1; q := 1;
while q ≤ m − 1 {
	while k ≥ 0 and P[k + 1] != P[q] { k := next(k) }
	if P[k + 1] = P[q] { k := k + 1 }
	next(q) := k
	q := q + 1
}