KMP算法图文详解

简介:

        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]ABCABD
下标值123456
最大公共前后缀000120
next数组-100012

前缀:例如: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];//模式串的自我匹配过程
		}
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云之君若雨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值