渣渣浅谈【KMP算法】

KMP算法

谈到KMP需要先说一下BF算法。
BF算法是一种暴力匹配的算法。
BF算法的核心步骤
1. 回溯
2. 校验

#include<iostream>
using namespace std;
int BF(string mainString, string modelString) 
{
    
    
	int index = -1;
	int lenMain = mainString.size();
	int lenModel = modelString.size();

	int pMain = 0, pModel = 0;
	while (pMain < lenMain) 
	{
    
    
		if (mainString[pMain] == modelString[pModel]) {
    
     pMain++; pModel++; }
		else {
    
    
			pMain = pMain - pModel + 2; 
			//回溯
			pModel = 0;
		}
		if (pModel == lenModel) {
    
    
			index = pMain - pModel + 1;
			break;
		}//表示循环终止的结束有两种,一种是匹配成功,另一种是当pMain==lenMain的时候,就可以退出了
	}
	
	return index;//如果index=-1,则表示没找到
	//否则,返回第一次匹配到模板串的时候的索引
	
}
int main() {
    
    
	string MainString = "AAABBCBD";
	string modelString = "BC";
	cout << BF(MainString, modelString);
}

回溯步骤中的

pMain = pMain - pModel + 2;//是因为当不匹配的时候,此时的主串的索引已经是匹配头+已经匹配的长度
//所以,要回溯后+1能从下一个匹配头开始匹配,需要把已经匹配的个数减去,再加1.由于后面还需要+1,所以直接+2

好,我们粗略的讲完了BF算法,不知道大家有没有注意到,其实,如果当AB为匹配串的时候,我们的模板串永远都是小于主串的,甚至在一些极端的情况下模板串的大小是远远小于主串的大小的。那么问题就来了,我们真的有必要在回溯的情况下从头开始重新匹配吗。

当然不是
在KMP算法中,我们主串的索引是不会像BF算法中的索引一样回溯的
如果说在BF算法中,我们通过主串逐一"试头" 来寻找返回的索引值,那么在KMP算法中,就是通过模板串的逐一右滑(可以理解成回溯的是模板串),但是模板串的右滑,是根据next数组来滑动的。
那么问题来了,什么是next数组?我们又为什么要知道他呢?

当我们在执行BF算法的时候,我们是不是通过两个索引逐一比较
在这里插入图片描述
我们要在回溯这个地方,做做“手脚”让他回溯得不那么前,这就是创建next数组得目的,也能通过这个方式,减少时间上的开销.

当我们匹配成功的时候,我们依旧是双索引++,关键在于匹配失败的时候引发的回溯操作此时被替换成滑动操作了,什么叫滑动,我们都学过大学物理,我不动,你动,也就是你相对于我发生了相对运动类似这样的原理:

我们令pMain不变,只改变pModel----令pModel=next[pModel]
也就是说,next数组指向了一个相对快捷的地方,以至于我们不用在比较前面的部分,我就能肯定,从next[pModel]这个索引前,我便不再需要校验,我便能肯定前面的值,一定相等。好,我们接下来求一下next数组
假如我们的模板串是这样的(注意,这里的next数组仅仅与模板串有关,跟主串一点关系都搭不上,next数组是模板串的一个属性,他不仅仅可以用于KMP算法,还能经过修改后用在很多算法上
在这里插入图片描述

我们依次 不改变次序地前提下,逐一加一 地取出他地真子串

在这里插入图片描述
并依次算出他的前后缀相等的长度
在这里插入图片描述
这里可能会有人有疑惑:“我求这个东西干嘛
为什么:刚刚说到的右滑,其实也是一种属于对于模板串的一种回溯(因为属于模板串的索引,在不匹配的时候要被next[pModel]赋值,因此,我们要保证赋值后,不必再比较前面的元素,必须是以当前匹配尾的位置为末截取一个子串(也就是上述的六个的一个)找到他们的前缀相等处,回溯到前缀,那么我们就不需要重新匹配以前的元素,因为以前的元素已经经过校验肯定是对的,而又因为前后缀相等,所以就不需要增加开销去校验了

因此我们求出了一个prefix_table->0 0 1 0 0 1
也就是说,这里代表着下标为 0 1 2 3 4 5的六个元素分别对应着前后缀相等的长度

当我们把该prefix_table整体右移一格,并将next[0]初值赋值为-1时候,我们就得到了我们的next数组,即为[-1,0,0,1,0,0,1]
好,我们来写一下这个算法。

void initialPrefixTable(int* prefix_table, string modelString) {
    
    
	int len = 0, pModel = 1;
	prefix_table[0] = 0;
	while (pModel < modelString.size())
	{
    
    
		if (modelString[pModel] == modelString[len])
		{
    
    
			//判断,如果当两个相等,那么我们只需要匹配最后一个和前一个前缀的后一个是否相等就行了
			len++;
			prefix_table[pModel] = len;
			pModel++;
		}
		else
		{
    
    
			
			if (len > 0)//分开>0和<=0
			{
    
    
				len = prefix_table [len - 1] ;
				//这里是建立在两者不相等的情况下,同时这行代码是整个KMP算法中最难的一环
				//如果不相等,那么,我此时的prefix_table的值必然是小于len+1的,
				//要计算此时的prefix_table,其实就是计算prefix_table[len-1]
			}
			//处理第一次匹配,如果没有,则会死循环
			else
			{
    
    
				prefix_table[pModel] = len;
				pModel++;
			}
		}
	}

}

接下来我们只需要向右挪一格就可以得到next数组了

void removePrefixTable(int* prefix_table,int len) {
    
    
	for (int count = len - 1; count > 0; count--) {
    
    
		prefix_table[count] = prefix_table[count - 1];
	}
	prefix_table[0] = -1;
}

此时的prefix_table就是我们要求的next数组,那么我们的KMP算法也能顺手写出来了

int KMP(string mainString, string modelString,int *prefix_table) 
{
    
    
	int index = -1;
	int lenMain = mainString.size();
	int lenModel = modelString.size();

	int pMain = 0, pModel = 0;
	while (pMain < lenMain) 
	{
    
    
		if (mainString[pMain] == modelString[pModel]) {
    
     pMain++; pModel++; }
		else {
    
    
			pModel = prefix_table[pModel];//只有回溯发生了改变,当不相等时,直接让pModel回溯到在模板串上的前缀末即可
		}
		if (pModel == lenModel) {
    
    
			index = pMain - pModel + 1;
			break;
		}//表示循环终止的结束有两种,一种是匹配成功,另一种是当pMain==lenMain的时候,就可以退出了
	}
	
	return index;//如果index=-1,则表示没找到
	//否则,返回第一次匹配到模板串的时候的索引
	
}

到这里我们的KMP算法已经被写好了,KMP本身最大的核心便在他的next数组的求取,在写这篇文章之前,我还借鉴了很多带博主的代码,他们的更加简洁,他们直接在KMP算法中,直接赋值给了next数组,包括在严书上的代码也是如此,但是同时也让我更难懂,可能是我太蠢了吧,我觉得这中凭借前缀表的做法更能让我理解。
至于ArrayList嘛,周二再更了,乏了乏了

猜你喜欢

转载自blog.csdn.net/weixin_45864947/article/details/109131912