想学会“看毛片”算法,今天和同学捣鼓了半天,终于理解并自己实现了它。主要参考的博客是http://blog.csdn.net/yutianzuijin/article/details/11954939/ 。由于自己实现了一遍KMP代码,为了加深印象和加强理解,写篇博客记录一下,欢迎讨论。
首先解释一下几个基本的名词:
1.模式串:用于匹配的子串
2.主串:可能包含模式串的字符串
3.失配:匹配失败
比如:
串A:"abcdefgabcde"
串B:"defg"
判断串A中是否包含串B,则串B为模式串,串A为主串
KMP算法的主要目的是判断模式串是否存在于主串中,若存在,返回匹配位置的下标
KMP算法与朴素的字符串匹配算法的最大区别就是:
朴素的字符串匹配算法是每次失配后将模式串向右移一个字符,继续从模式串首开始比较,这样做缺点是没有利用已经匹配的字符的信息,降低了效率;
KMP算法利用了已经匹配了的字符的信息,一次失配后尽可能的右移更多的字符,但又保证不会漏掉可能的匹配串,从而在保证精确的前提下提高了效率。
举个例子:
主串A: "abccdabcbc"
匹配串B:"abcd"
朴素匹配方法:
前三次比较"abc"都匹配成功,第四次由于"c"和"d"不相同,所以失配;
这时将子串右移一位,继续比较:
abccdabcbc
abcd
发现第一位"a"和"b"失配,于是继续右移:
abccdabcbc
abcd
.....
KMP的匹配方法:
同样前三次比较发现"abc"匹配成功,第四位"d"失配;
这时KMP算法将会利用“已经匹配了abc这3个字符”这个信息,显然
a|bc| |a|bc
|ab|c ab|c|
都是不匹配的,所以这时可以直接将模式串右移3位,从一开始失配时的这样:
abccdabcbc
abcd
变成这样:
abccdabcbc
abcd
经过观察和思考,KMP的发明者们发现,对于每一个已经匹配的长度(如上例中已经匹配了3个字符),都可以计算出一个“最大右移值”,且这个“最大右移值”仅与已经匹配的字符串有关(注意到上面的例子中我计算右移值为3位仅仅用到了’abc’这个已经匹配的字符串)
所以,对于每一个给定的模式串,我们都可以对它的每一个长度的匹配长度计算相应的“最大右移值”,在匹配主串时,对于每一个匹配长度,都按这个提前计算出的“最大右移值”进行右移,这就是KMP算法的基本思路。
那么,根据已经匹配的字符串,如何计算出所谓的“最大右移长度”呢?
这里引入“前缀”和“后缀”的概念:
还是上面的例子,“abc”是“abcd”的一个前缀,“ab”也是“abc”的一个前缀,“a”也是...
而“bcd”、“cd”、“d”都是“abcd”的后缀
但是"abcd"不是"abcd"的前缀和后缀,因为这样的前后缀对KMP算法是没有意义的。
当一个字符串的前缀和后缀完全相同时,我们记相同部分的长度为len,当len最大时,用这个字符串的长度减去l,就是我们的”最大右移值”
在确保自己理解了上面那句话之后,再看下面的内容就比较容易了。
KMP算法使用一个next数组来存储上面提到的len。
注意!注意!注意!重要的事情:
next数组的下标表示的是已经匹配的字符长度,是长度!由于匹配长度为0时KMP和朴素匹配方法的处理方法是一样的),所以next数组的下标从1开始。而匹配串的下标是从0开始的。
这点很重要,我和同学看了好多博客,发现看得越多越晕,很大的原因就是很多网上的代码没有交代清楚next数组的下标和匹配串下标的区别,导致代码总是看不懂….下面贴代码的时候我还会强调这个问题。
我们知道,对于每一个匹配长度,都可以计算出一个len值,比如:
模式串"abcbcabc"
的next数组中的值如下,下标从1开始:
next数组中len的值:[x,0,0,0,0,0,1,2,3]
对应next下标值 : 0,1,2,3,4,5,6,7,8
那么,如何用代码计算next数组呢?
下面是我写的代码:
#pragma once
#include<stdlib.h>
#include<iostream>
using namespace std;
#define MAXSIZE 100
#define PSIZE 9
void calcNext(int * next,char*pattern,int psize){
//参数解析:next为存放计算之后的next数组
//pattern为模式串
//psize为模式串长度
next[1] = 0;//匹配长度为1时,len必为0
for (int i = 1; i < psize; i++){
int n = next[i];
while (n != 0 && pattern[n] != pattern[i]){
//这里就体现了上文说的下标问题。注意这里做的工作是:
//比较模式串的第next[i] + 1个元素和模式串的第i + 1个元素是否相等
//next[i]==0时,则比较pattern的第一个元素和模式串的第i + 1个元素
//不相等则比较模式串的第next[next[i]]+1个元素和第i+1个元素是否相等
//如此迭代直到两者相等或next[n]为0
n = next[n];
}
if (n == 0){
if (pattern[n] == pattern[i]) next[i + 1] = 1;
//如果只有模式串的第一个元素和第i+1个元素相同,则next[i+1] = 1.
else next[i + 1] = 0;
}
else next[i + 1] = n + 1;
}
}
从代码中可以看到,next[i+1]是可以通过next[i]迭代计算得到的,这么做的原理,篇首提到的那个参考链接中讲的很清楚,这里不再赘述。
next数组计算结束了,我们开始使用它来高效地检测字串,下面是我的测试代码:
int main(){
char pattern[PSIZE + 1] = { 0 };
int next[PSIZE + 1] = { 0 };//next[0]不用
char buffer[MAXSIZE] = { 0 };
int j = 0;
cout << "Input the pattern string :(less than 9)" << endl;
cin >> pattern;
cout << "Input the string to match substring:" << pattern << endl;
cin >> buffer;
//计算next
calcNext(next, pattern, PSIZE);
//开始匹配,匹配方法和计算next数组神似,大家好好体会
for (int i = 0; i < strlen(buffer); i++){
while (j > 0 && pattern[j] != buffer[i]) j = next[j];
if (pattern[j] && buffer[i] == pattern[j]){
j++;
}
if(!pattern[j]){
cout << "find substring at index: " << i - strlen(pattern) + 1 << endl;
j = next[j];
}
}
cout << "match over" << endl;
system("pause");
return 0;
}