您的位置:首页 > 其它

KMP算法简单分析

2016-04-09 21:31 274 查看

定义问题

字符串匹配是这样一个问题: 对于两个包含且仅包含字母表∑中的字母的串P,T,计算出所有有效的**移进**s使得
P[1..|P|] = T[s+1..s+|P|]
。(|P|为P的长度)。

或者说:求出在什么位置P被T完全包含。

为了表达方便,定义m = |P|, n = |T|。P称为模式串,T称为匹配串

朴素算法

朴素算法是一种显然的方法。直接给出伪代码:

Naive-Match (P, T)
m = |P|, n = |T|
for i = 1..n do
if P[1..m] == T then
print i" "


朴素算法可以看成模式串紧贴匹配串滑动,尝试移进s = 1..n时能否匹配。大多数情况下,朴素算法已经可以解决问题。但是当数据极大(例如在很长的基因串中寻找一组基因)时,朴素算法的效率就显得差了。因此,科学家寻找到许多种优秀的匹配算法。这是一个常用算法时间对照表。

算法预处理匹配
朴素算法0O((n-m+1)m)
Rabin-KarpΘ(m)O((n-m+1)m)
有限自动机Θ(m∑)Θ(n)
Knuth-Morris-PrattΘ(m)Θ(n)
所有的字符串算法都很麻烦(毕竟蒟蒻)。其中KMP用处比较广。在《算法导论》里KMP的介绍是以有限自动机为基础的,然而我又看不懂,gedao了半天才大致明白KMP的思想。

KMP算法

Quote:来自 zrO matrix67 Orz

假如,A=”abababaababacb”,B=”ababacb”,我们来看看KMP是怎么工作的。我们用两个指针i和j分别表示,A[i-j+ 1..i]与B[1..j]完全相等。也就是说,i是不断增加的,随着i的增加j相应地变化,且j满足以A[i]结尾的长度为j的字符串正好匹配B串的前 j个字符(j当然越大越好),现在需要检验A[i+1]和B[j+1]的关系。

- 当A[i+1]=B[j+1]时,i和j各加一;什么时候j=m了,我们就说B是A的子串(B串已经整完了),并且可以根据这时的i值算出匹配的位置。

- 当A[i+1]<>B[j+1],KMP的策略是调整j的位置(减小j值)使得A[i-j+1..i]与B[1..j]保持匹配且新的B[j+1]恰好与A[i+1]匹配(从而使得i和j能继续增加)。我们看一看当 i=j=5时的情况。

详细内容参见 http://www.matrix67.com/blog/archives/115

个人理解

我是自己推导之后才看到上面大牛的解释,真的非常通俗。所以看不懂的同学可以去哪里膜拜一下。kmp算法实在比较恶心,虽然代码秘制煎蛋,不习惯推导的童鞋直接背下来就可以了。:(语言表达能力捉急):。

ps:这里并没有使用图形辅助理解,个人认为这样更有利于理解kmp匹配原理。

kmp基于一个函数π,π表示有最大的t < i使P[1..t] = P,则t = π。或者形式化地:

π[i] = max{t | P[1..t] = P 且 t < i}


证明一个结论,对于任意T[k-i+1..k] = P[1..i],有:

π[i] = max{t | P[1..t] = T[k-t+1..k]}
用反证法,假设有π < x < i使得P[1..x] = T[k-x+1..k]
∵ P[1..i] = T[k-i+1..k]
∴ T[k-x+1..k] = P
又 P[1..t] = T[k-t+1..k]
∴ P[1..x] = P
∵ x > π[i], 根据定义,矛盾
原命题得证。


这个结论将说明kmp不会错过正确解。

以及:

如果有s使T[k-s+1..k] = P[1..s],
那么有T[k-π[s]+1..k] = P[1..π[s]]。
证明很简单,根据定义等量代换即可。


这个结论将说明kmp不会找到错误解。

这些结论并不足以证明kmp的正确性,但是基本可以看出主要思想了。事实上,通过π可以省略许多无用的比较(基于第二个结论)。kmp匹配算法代码如下:

void kmp_match(int l) {
// l是T的长度,pL是P的长度
int q = 0;
// 匹配的长度
for (int i=1; i<=l; i++) {
while (q > 0 && P[q+1] != T[i])
q = pie[q];
// 无法匹配下一位,找到可以部分匹配的最大部分,或者没有可以匹配
if (P[q+1] == T[i])
q++;
// 下一位可以匹配
if (q == pL) {
// 找到
printf("Shift %d >>> ", i-pL);
q = pie[q];
// 找下一个匹配位置
}
}
}


计算匹配函数π的方法:

void kmp_init() {
int k = 0;
pie[1] = pie[0] = 0;
// 第一位不可能找到匹配
for (int i=2; i<=pL; i++) {
while (k > 0 && P[k+1] != P[i])
k = pie[k];
// 同上,自己匹配自己罢了
if (P[k+1] == P[i])
k++;
pie[i] = k;
// 记录最长匹配
}
}


所谓自己匹配自己,就是π就是找到一对最大且相等的前缀和后缀,记录前缀出现位置。(基于定义)

kmp大概就是这样了,多思考就可以想通。。

kmp时间复杂度分析

kmp的复杂度为Θ(n)-Θ(m),这里用摊还分析中的聚合分析法给出一个kmp_init复杂度分析例子。我们试图证明while循环的执行次数为O(n)。

k的初值为0,而k的值增长有且只有一个途径:10行的k++。由于for循环一次k最多加一,n-1次循环之后k最多为n-1呢。由于π < i,因此while循环只会使k减少,且一次至少减少1。而k < n-1,所以while的循环次数为O(n)。不难得出kmp_init的复杂度为Θ(n)。用这种方法也可以得出kmp_match的复杂度为Θ(m)。

linux下装逼代码

装逼专用,仅售998,到linux上看看效果吧。

#include <iostream>
#include <cstdio>
#include <cctype>
using namespace std;
char P[10005], T[10005];
int pL;
int pie[10005];
int readfln(char *str) {
char c;
int i = 0;
str[0] = '\"';
while (c = getchar()) {
if (c!= '\n')
str[++i] = c;
else break;
}
return i;
}
void printfln(int shift,int l) {
int beg = shift-5;
if (shift <= 5)
beg = 0;
else
printf("...");
for (int i=beg+1; i<=shift; i++)
putchar(T[i]);
printf("\033[33m");
printf("%s", P+1);
printf("\033[0m");
int end = shift+pL+5;
if (shift+pL+5 > l)
end = l;
for (int i=shift+pL+1; i<=end; i++)
putchar(T[i]);
if (shift+pL+5 < l)
printf("...");
printf("\n");
}
void kmp_init() {
int k = 0;
pie[1] = pie[0] = 0;
for (int i=2; i<=pL; i++) {
while (k > 0 && P[k+1] != P[i])
k = pie[k];
if (P[k+1] == P[i])
k++;
pie[i] = k;
}
}
void kmp_match(int l) {
int q = 0;
for (int i=1; i<=l; i++) {
while (q > 0 && P[q+1] != T[i])
q = pie[q];
if (P[q+1] == T[i])
q++;
if (q == pL) {
printf("Shift %d >>> ", i-pL);
printfln(i-pL,l);
q = pie[q];
}
}
}
int main() {
pL = readfln(P);
kmp_init();
int l;
while (l = readfln(T))
kmp_match(l);
return 0;
}


参考资料:《算法导论》
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: