简介:
Knuth-Morris-Pratt(KMP算法or字符串查找算法)可在一较长串S内查找一子串P出现的位置,KMP算法利用最长公共前后缀的特性以避免重新检索先前配对的字符串,提高算法效率。
——KMP算法最终由Knuth-Morris-Pratt三人于1977年联合发表
朴素算法(Brute Force Algorithm):
代码示例:
#include<iostream>
#include<cstring>
//i和j下标从0开始的情况:
using namespace std;
const int Max = 1e3 + 10;
int main() {
char s[Max]={}, p[Max]={};
cout << "请输入文本串S" << endl;
cin >> s;
cout << "请输入模式串P" << endl;
getchar();
cin >> p;
int i = 0, j = 0;
int sLen = strlen(s), pLen = strlen(p);
while (i < sLen && j < pLen) {
if(s[i] == p[j]){//如果匹配成功,i和j同步后移
i++;
j++;
}
else {//如果失配,i回溯,j归零
i = i - (j - 1);
//注意,i回溯到了最初始匹配成功的i的下一位;
//最初始匹配成功:上一次匹配失败的下一位的i;
j = 0;
}
}
//如果匹配成功,则j等于pLen;
if (j == pLen) cout << "匹配成功,物理位置为:" << i - j
<<" " << "逻辑位置为:" << i - j + 1 << endl;
else cout << "抱歉,匹配失败!" << endl;
return 0;
}
例如:查找串S为:ABCABCDAB 模式串P为:ABCD
注意:这里规定二者下标均从0开始
查找过程中:下标i从0开始,P串下标j从0开始;(下文会详解)
要想查找P在S中的位置,该如何查找呢?
假设此时:S和P都查找到了下标为2(逻辑地址,i = 1)的位置:因为S[i] == P[j],故i++,j++;
假设此时:S和P都查找到了下标为6的位置:但S[i] != P[j]; 此时:i = 5,j = 5
按照朴素算法,应该这样操作:令 i = i - j + 1 ,j = 0;(i的回溯过程)
此时 i = 5 - 5 + 1 = 1,i = 1,j = 0 ,相当于模式串P右移:S[i] != P[j];
此时 i = 1 - 0 + 1 = 2,i = 2,j = 0 ,相当于模式串P右移:S[i] != P[j];
此时 i = 2 - 0 + 1 = 3,i = 3,j = 0 ,由于:S[i] == P[j]; 则:i++,j++;
直到:i = 6 , j = 3;由于S[i] != P[j]; 此时 i = 6 - 3 + 1 = 4 , j = 0 ;相当于模式串P右移:
以此类推,直到 i = 9 ,由于 j = 5 即 N - 1 ; 所以查找到模式串;
KMP算法(KMP Algorithm):
代码示例:
#include<iostream>
#include<cstring>
using namespace std;
//求next数组:
void GetNext(char* p, int next[]) {
int pLen = strlen(p);
next[0] = -1;
int k = -1, j = 0;//p[k]表示前缀,p[j]表示后缀
while (j < pLen - 1) {
if (k == -1 || p[j] == p[k]) {
j++;
k++;
next[j] = k;
}
else {
k = next[k];//模式串的自我匹配过程
}
}
}
const int Max = 1e3 + 10;
int main() {
char s[Max] = {}, p[Max] = {};
int next[Max] = {};
GetNext(p, next);
cout << "请输入文本串S" << endl;
cin >> s;
cout << "请输入模式串P" << endl;
getchar();
cin >> p;
int i = 0, j = 0;
int sLen = strlen(s), pLen = strlen(p);
while (i < sLen && j < pLen) {
if (s[i] == p[j] || j == -1) {//如果匹配成功 or j == -1,i和j同步后移
i++;
j++;
}
else {//如果失配,i不变,j = next[j];
j = next[j];//相当于模式串p右移了j-next[j]位;
}
}
//如果匹配成功,则j等于pLen;
if (j == pLen) cout << "匹配成功,物理位置为:" << i - j
<< " " << "逻辑位置为:" << i - j + 1 << endl;
else cout << "抱歉,匹配失败!" << endl;
return 0;
}
引言:
因此我们发现,朴素算法的效率比较低,尤其是第一步:
我们发现在ABCAB(下标1-5)中:
AB(下标12)作为前缀与作为后缀的AB(下标45)共同构成了最长(长度为2)的公共前后缀;
因此,我们只需要:令 j = 2 ,相当于向右移动模式串3位(j = 5 ,j - 2 = 3);
这样以来,大大节省了匹配速度,面对失配情况,我们可以通过模式串P对应位的最长公共前后缀的长度,进行快速移位。不再需要像朴素算法那样,进行i的回溯和j的归零。
KMP算法的难点在于next数组的理解和构建
引入next数组:
由引言我们可知:模式串P的第5位:对应的最长公共前后缀为2;
因此我们可以先求得该位置的最长公共前后缀:
模式串P[N] | A | B | C | A | B | D |
下标值 | 1 | 2 | 3 | 4 | 5 | 6 |
最大公共前后缀 | 0 | 0 | 0 | 1 | 2 | 0 |
next数组 | -1 | 0 | 0 | 0 | 1 | 2 |
前缀:例如:ABCDA :不包括最后一个字符A的所有字符组合:A,AB,ABC,ABCD
后缀:例如:ABCDA :不包括最前一个字符A的所有字符组合:A,AD,ADC,ADCB
则,ABCDA的公共前后缀为:A,其中最长的为A,故ABCDA的最长公共前后缀为A;
显而易见,next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为 -1。
KMP查找模式串:
由引言和引入next数组可知:遇到s[i] != p[j]的情况,我们可以令j = next[j];
令j = next[j] ,相当于保持查找串S不动,模式串P右移了(j - next[j])位;
相当于前缀为1,后缀为2,将1的位置平移至3的位置;
next[5] = 2,令 j = next[5] = 2,则模式串P右移了(5-2)位;
KMP查找代码如下:
while (i < sLen && j < pLen) {
if (s[i] == p[j] || j == -1) {//如果匹配成功 or j == -1,i和j同步后移
i++;
j++;
}
else {//如果失配,i不变,j = next[j];
j = next[j];//相当于模式串p右移了j-next[j]位;
}
}
求解next数组的值:
求解next数组和KMP查找差不多,只不过KMP查找是S[i]在P[j+1]之间查找;
求解next数组是模式串的自我匹配过程
求解next数组代码如下:
void GetNext(char* p, int next[]) {
int pLen = strlen(p);
next[0] = -1;
int k = -1, j = 0;//p[k]表示前缀,p[j]表示后缀
while (j < pLen - 1) {
if (k == -1 || p[j] == p[k]) {
j++;
k++;
next[j] = k;
}
else {
k = next[k];//模式串的自我匹配过程
}
}
}