赞
踩
Knuth-Morris-Pratt(KMP算法or字符串查找算法)可在一较长串S内查找一子串P出现的位置,KMP算法利用最长公共前后缀的特性以避免重新检索先前配对的字符串,提高算法效率。
——KMP算法最终由Knuth-Morris-Pratt三人于1977年联合发表
- #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 ; 所以查找到模式串;
代码示例:
- #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数组的理解和构建
由引言我们可知:模式串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。
由引言和引入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数组和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];//模式串的自我匹配过程
- }
- }
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。