KMP算法是一个经典的字符串匹配算法,也是一种常用的字符串匹配算法。在KMP算法没出现之前,大家在字符串匹配的时候,都是两个for循环嵌套完成字符串之间的匹配,这种算法称作 BF算法(暴力求解)。
当时Knuth,Morris,Pratt三为大师发现这种方法太慢了,他们发现了一个规律,有些字符串匹配失败后,不需要从头重新开始匹配下一个,就这样,在他们的努力之下,完成了这个算法,后人为了纪念这三个大神,取他们名字的首字母将这个算法称为KMP算法,KMP的思想是字符串匹配历史上的重要转折点,后续基于KMP的思想,后人又开发出来了BF算法, Sunday算法等。
下面带领大家开始KMP算法的剖析之路。
1、字符串匹配问题
题目: 给定字符串 mother = “QABADABACDE” ,判断字符串 son = “ABACD”是否是mother的亲儿子,即字符串son是否是mother的子串。
青铜求解思路:暴力求解法
bool BF(const string &mother, const string &son) {
for (int i = 0; i <= mother.size() - son.size(); i++) {
int j = 0;
for (; j < son.size(); j++) {
if (mother[i+j] != son[j]) { // 不匹配,退出
break;
}
}
if (j >= son.size()) {
cout << "true" << endl;
return true;
}
}
cout<< "false" << endl;
return false;
}
这种求解思路也是大家最容易想到的匹配方法。但是在匹配过程中,参考下图,大家有没有发现什么可以优化的地方?
2、KMP算法思路来源
聪明的童鞋可能已经发现了,说明你跟上面的三位大神有的一拼,只不过是生不逢时 (_).
在BF算法中,上图中母串第i个元素跟子串第j个元素比较发现不一致,则子串整体往后移动一个位置,重新匹配。而聪明的KMP三位童鞋发现,子串的“ABA”部分,第一个A和最后一个A相同,何不将子串移动到i-1的位置进行匹配呢?(见下图)
KMP三位大神一起抽了根烟,想起了数学上的归纳方法,就从简单到复杂列举了几个例子如下:
- 提取出j前面的子串“ABCD” ,发现前后没有相同的元素,此时下一次比较时,i与j=0对应进行比较。
2. 提取j前面的子串“ABA”,发现最前面的A和最后的A相同,此时下一次比较时,i与j=1对应进行比较。
3.提取j前面的子串“ABCAB”,发现字符串最前面和最后面有两个字符相同:“AB”,此时下一次比较时,i与j=2对应进行比较。
根据归纳方法,由一般到特殊,总结出下面的表格:
j前面的字符串前后相同元素个数 | 下次跟i比较的子串的位置 j |
---|---|
0 | 0 |
1 | 1 |
2 | 2 |
3 | 3 |
4 | 4 |
… | … |
n | n |
3、如何表示这种规律?
KMP三位大神发现了这个规律后,开始思考如何表示呢?他们发现,j前面的字符串都是son的子串,因此是否可以根据son字符串构造出,母串第i个元素跟子串的第j个元素不同的时候,下一次j的取值呢?因此,next数组出生了。
规定: next[0] = -1;
next[k]表示:表示son字符串中**[0, k-1]**子串中前后相同字符的个数
例如:
“ABA” : next[] = {-1, 0, 0};
“ABCDABCD”: repeat[] = {-1, 0, 0, 0, 0, 1, 2, 3} // k=7时,取[0,6]字串 “ABCDABC”前面的ABC和后面的ABC相同,因此next[7]=3
next数组如何求呢?
下面给出求解方法:
void GetNext(string son, int *next) {
next[0] = -1; // 0位置默认为-1
int j = 0; // j 表示next数组的下标
int k = -1; // 表示前后相同字符串的个数
while (j < son.size() - 1) {
if (k == -1 || son[j] == son[k]) { // k==-1时,设置next[++j] = 0 如果son[j] == son[k],如果相同,设置next[++j] = ++k
next[++j] = ++k;
} else {
k = next[k];
}
}
return;
}
4、KMP算法实现
有了next数组了,接下来就是水到渠成的事情了,下面给出KMP算法的算法框架代码:
int KMP(const string &mother, const string &son)
{
int motherId = 0;
int sonId = 0;
int next[son.size() + 1];
GetNext(son, next);
while (motherId < mother.size() && sonId < son.size()) {
if (sonId == -1 || mother[motherId] == son[sonId]) { // 字符相同,则继续比较下一个
sonId++;
motherId++;
} else { // 字符不匹配,根据next查找出下一个j的位置,继续比较。
j = next[j];
}
}
if (sonId >= son.size()) {
return motherId - sonId;
}
return -1;
}
5、是否还可以优化?
按照上面给出的思路,你是否已经能够理解KMP算法了呢?如果不明白欢迎评论区留言。
在这里给大家留个疑问,夜深了,明天继续更新这个优化点。