一、引言
KMP算法是昨天才学习的算法,因为最初听别人讲的next数组很复杂,所以自己觉得比较繁琐。
但是在学习中,它的效率比暴力匹配快了许多,所以学习它也是很重要的。
下面是我对KMP算法的所有理解,由浅入深、逐步讲解。
二、暴力匹配
字符串匹配问题:给定一个n个字符组成的串[称为文本(text)],一个m(m<=n)个字符的串[称为模式(pattern)],从文本中寻找匹配模式的子串。
- 如果匹配成功,即a[i + j] == b[j],则j++,继续匹配下一个字符。
- 如果匹配失败,即没有进入while循环,则i++,j=0。每次匹配都是从模式的第一个位置开始匹配,所以j每次都要记为0。
- 注意最后一轮子串匹配的起始位置是n-m(文本位置是从0到n-1)。在这个位置以后,再也没有足够的字符可以匹配整个模式了,因此,该算法也就没有必要再做比较了。
#include <iostream>
#include <string>
using namespace std;
int BruteFroceStringMatch(string a, string b) {
int la = a.length();
int lb = b.length();
int i,j ;
for (i = 0; i <=la-lb; i++) {
j = 0;
while (j < lb&&a[i + j] == b[j]) {
j++;
if (j == lb) return i;
}
}
return -1;
}
int main() {
string a, b;
getline(cin, a); //a为文本串
getline(cin, b); //b为模式串
int result = BruteFroceStringMatch(a, b);
cout << "文本匹配到字符串的起始位置为:" << result << endl;
return 0;
}
例子:
文本a“abxabcabcaby”,模式b“abcaby”。过程如下:
1.a[0]为a,b[0]为a,匹配。执行while语句,j++。直到a[i + j] != b[j],即a[0+2]!=b[2]
2.a[1]和b[0]不匹配,继续执行i++。
3.直到a[3]和b[0]相匹配,又一次进行while循环。
4.一直进行for循环,直到i=n-m。此时,a[6]==b[0]。进行while循环,发现字符串匹配,输出匹配时的位置。
该算法的时间复杂度为O(mn),在此基础上学习优化的查找算法,KMP。
三、KMP
#include <iostream>
#include <string>
using namespace std;
int kmp(string a, string b) {
int next[100];
int i, j , k;
j = 0;
int lla = a.length(); int llb = b.length();
next[0] = 0;
i = 1;
while (i < llb) {
if (b[i] == b[j]) { //j是第一个数的指针
next[i] = j + 1;
j++;
i++;
}
else {
if (j != 0) {
j = next[j - 1];
}
else {
next[i] = 0;
i++;
}
}
}
i = 0; k = 0;
while (i < lla && k < llb) {
if (a[i] == b[k]) {
i++; k++;
}
else {
if (k != 0) {
k = next[k - 1];
}
else {
i++;
}
}
}
if (k == llb) {
return i - k;
}
else {
return 0;
}
}
int main() {
string a, b;
getline(cin, a);
getline(cin, b);
int result = kmp(a, b);
cout << "文本匹配到字符串的起始位置为:"<<result<<endl;
return 0;
}
3.1 前缀后缀
如果给定的模式串是:“ABCDABD”,从左至右遍历整个模式串,其各个子串的前缀后缀分别如下表格所示:
3.2 最大公共字符长度表
例子:
模式为a“aabaabaaa”,求最大公共字符长度的数组next。
1.设两个指针,j指向a[0],i指向a[1],当只有一个字符时,没有前缀和后缀,所以令next[0]=0。此时a[0]==a[1],所以next[1]=j+1=1,i++,j++。
2.由图可知,a[j]=a[1],a[i]=a[2],a[j]!=a[i]。所以回溯j=j-1,因j-1=0,所以next[i]=0,i++。
3.a[j]==a[i],next[i]==j+1。i++,j++。
4.后面逐步进行匹配,得下图结果。此时a[j]!=a[i],j=next[j-1],j=2,即在字符a前(包括字符a)最大公共字符串长度为2,转到j处。
此时a[j]!=a[i],j=next[j-1],j=1。
a[j]=a[i],next[i]=j+1;
3.3 基于最大公共字符长度表匹配
结合之前的最大公共字符长度表,进行字符串的匹配。如果给定文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,现在要拿模式串去跟文本串匹配,如下图所示:
“ABCDABD”的next数组值为“0000120”
- 因为模式串中的字符A跟文本串中的字符B、B、C、空格一开始就不匹配,所以不必考虑结论,直接将模式串不断的右移一位即可,直到模式串中的字符A跟文本串的第5个字符A匹配成功:
2.继续往后匹配,当模式串最后一个字符D跟文本串匹配时失配,显而易见,模式串需要向右移动。但向右移动多少位呢?因为此时已经匹配的字符数为6个(ABCDAB),然后根据最大公共字符长度表可得失配字符D的上一位字符B对应的长度值为2,所以此时k=2,即从C开始匹配。
3.移动后,发现C处再度失配,因为此时已经匹配了2个字符(AB),且上一位字符B对应的最大长度值为0,k=next[k-1]。
4.A与空格失配,向右移动1 位。
5.继续比较,发现D与C 失配,故k为k=next[k-1],即k=2。
- 经历第5步后,发现匹配成功,过程结束。
通过上述匹配过程可以看出,问题的关键就是寻找模式串中最大长度的相同前缀和后缀,找到了模式串中每个字符之前的前缀和后缀公共部分的最大长度后,便可基于此匹配。而这个最大长度便正是next 数组要表达的含义。
四、学习视频
慢慢看完、认真理解就能掌握KMP
https://www.bilibili.com/video/av3246487?t=590