KMP算法

假如我们需要在一段很长的文本中查找一个单词出现的位置,你会怎么做?朴素的做法是从文本的字母开始,向后和这个单词中的字母一一匹配,若完全匹配则说明找到了一个单词,若有一个字母匹配失败,就紧接着换文本的下一个字母重新匹配,即使成功匹配到一个单词,也要紧接着从下一个字母开始匹配。显然,最坏情况复杂度可以达到O(nm)(n为文本长度,m为要查找的单词长度)。事实上,那段文本我们称为文本串(T),那个单词称为模式串(P),这个问题叫做字符串匹配。

同样的结果,KMP算法却可以做到O(n+m),因为他避免了许多不必要的比较,比如abababc为文本串,ababc为模式串,一开始我们从头开始匹配,当模式串匹配到c时失配,朴素做法会重新从模式串开头匹配,而KMP算法却不会。为什么呢?只是一个字母失配,在此之前我们已经成功匹配了一些,难道没有用处吗?可能当年KMP算法的提出者也是这样想的。我们可以利用已匹配的字母,设fail数组保存某个字母的前缀(不包括该字母)开头和结尾最长的公共部分长度,一旦失配,我们就可以以此跳到一个合适的位置再去匹配,比如上面的例子,KMP算法就会再从模式串的第三个字母a开始匹配。还有,fail数组可以保存不同的信息,我习惯这样。

下面简述一下如何得到fail数组,我们假设已经得到了fail[i],那么可以去推出fail[i+1],若P[i]==P[fail[i]],fail[i+1]=fail[i]+1;否则,说明fail[i+1]为0。最开始就有fail[0]=fail[1]=0。

得到了fail数组,去匹配文本串也很容易,过程十分相似。我们枚举文本串每个字母Ti,设j表示在模式串中已匹配的长度,若P[j]==T[i],则++j;否则j=fail[j]。当j==m时,说明成功匹配到了一个。继续进行上述过程就可以找出所有模式串的匹配。

仔细想想,可以知道,代码这样做,字符串下标是从0开始的。

 1 int n, m, fail[maxn]; //n和m是t和p的长度
 2 char t[maxn], p[maxn];
 3 void getfail() {
 4     fail[0] = fail[1] = 0;
 5     for (int i = 1; i < m - 1; ++i) {
 6         int j = fail[i];
 7         while (j && p[j] != p[i]) j = fail[j];
 8         fail[i + 1] = p[j] == p[i] ? j + 1 : 0;
 9     }
10 }
11 void kmp() {
12     getfail();
13     int j = 0;
14     for (int i = 0; i < n; ++i) {
15         while (j && p[j] != t[i]) j = fail[j];
16         if (p[j] == t[i]) ++j;
17         if (j == m) printf("%d\n", i - m + 1);
18     }
19 }

 

posted @ 2018-09-16 14:20  Mr^Kevin  阅读(145)  评论(0编辑  收藏  举报