KMP算法是一种常用的在字符串中寻找子串的算法,相比于最普通的一一比较的暴力法(BF法),KMP法在时间效率上明显更优。为了更好地分析KMP,首先简要介绍BF法。
BF法
假设要在长度为m的字符串text中查找长度为n的子串pattern(假设n小于m,否则显然是不可能找到的)。那么最传统的方法即是在text中开一个长度为n的“窗口”,如果这一窗口中字符串与pattern完全符合,则表示找到了子串所在的位置,此时窗口中第一个字符在text中的位置即是找到的子串位置。
说人话……BR法简单而言是这样:在text中找出以第0个字符为开始、长度为n的子串(也就是所谓的窗口),将这个子串与pattern逐字符地比较,若完全一致则为找到。如果不吻合,则在text中找出以第1个字符为开始、长度为n的子串,将这个子串与pattern逐字符地比较;在text中找出以第2个字符为开始、长度为n的子串,将这个子串与pattern逐字符地比较……以此类推,直到找到或者到达text末尾为止。
BF法每一次比较都要逐字符地比较text中抽出的子串和pattern,且每次比较失败(不吻合)时只把“窗口”向后移动一位,使得查找效率较低。
KMP法
上面提到了,BF法每次比较失败时都把窗口只向后移动一位,这导致了低效率。那么如果每次比较失败时把窗口多向后移动几位,那么效率自然就高了不少。那么移动几位呢?
假设用k表示每次比较失败时窗口向后移动的位数,则k=(pattern中第一个不吻合的字符的脚标i)-(next【i】)。算法的关键在于这个next!
简单想想可能会觉得如果在第t位比较失败了,则直接跳过前面的字符,把窗口向后移t位不就好了?考虑如下的情形:假设text为“abdabdabc……”,pattern为“abdabc”,开始时比较text[0]与pattern[0],吻合,再比较text[1]与pattern[1],吻合……一直到text[5]与pattern[5],不再吻合。按照我们在刚刚最简单的假设,是不是应该把窗口直接向后移动5位,将text从text[5]开始的子串与pattern继续比较?显然不对,我们明显发现text[4]开始的abdabc是符合pattern的,分明已经找到了才对!所以,我们需要前式中的next【i】。
回到我们的例子来讲解next究竟是什么,我们选择的pattern是“abdabc”,那么假设在pattern[4]处比对失败,那么因为pattern[4]前一个字符是“a”,而pattern的开头字符也是“a”,因此比起前一段所述的简单向后跳4位,还需要向前回退1位,因为从text中与pattern[3]对应的那个“a”,就有可能是符合pattern的那个窗口的开头字符。或者,假设我们之前一直比较成功,但到了pattern[5]处比对失败,那么因为pattern[5]前面的两个字符是“ab”而pattern开头的两个字符也是“ab”,因此此时的next应该是2,即需要先将窗口向后移5位,然后回退2位。
现在我们可以归纳出next【i】怎么计算了。Next【i】等于可以使pattern[0: i-1]这个子串中长度为n的前缀与长度为n的后缀相等的最大的n值。特例是next【0】,next【0】应为-1,因为k=(pattern中第一个不吻合的字符的脚标i)-(next【i】),此时(pattern中第一个不吻合的字符的脚标i)为0,为了让窗口仍然可以后移,需要将next【0】设为-1。
在本例中,pattern为“abdabc”,那么next为{-1, 0, 0, 0, 1, 2}。
下面就可以总结性地给出KMP算法的描述了:开始时,将text中从0开始、长度为n的窗口,与pattern比较,如若比较失败,把窗口向后移动k位,k=(pattern中第一个不吻合的字符的脚标i)-(next【i】),在之后每次的比较中可以跳过前next【i】的字符位比较(因为显然是一样的)……不断执行前述步骤,直到找到pattern或者到达text的末尾为止。
以下为我的C++实现:
#include<iostream>
#include<string>
using namespace std;
int* getNext(string instr) {
int length = instr.length();
int* next;
next = new int[length];
next[0] = -1;
for(int i = 1; i < length; i++) {
int max = 0;
for(int j = 1; j < i-1; j++) {
if(instr.compare(0, j, instr, i-j, j) == 0) {
max = j;
}
}
next[i] = max;
}
return next;
}
int KMP(string text, string pattern) {
if(pattern.length() > text.length()) {
return -1;
}
int* next;
int ne = 0;
next = getNext(pattern);
for(int i = 0; i <= text.length() - pattern.length(); /*注意这里没有++*/) {
int j;
for(j = ne; j < pattern.length(); j++) {
if(text[i+j] != pattern[j]) {
break;
}
}
if(j == pattern.length()) {
return i;
}
i = i + j - next[j];
if(next[j] == -1) {
ne = 0;
} else {
ne = next[j];
}
}
return -1;
}
int main() {
int result;
string string1("ab abd ababcabcd"), string2("abcabcd");
result = KMP(string1, string2);
cout << result << endl;
return 0;
}