KMP算法next数组求解基础

个人学习笔记:

对初学有一点难理解的KMP算法,仅提供个人理解过程,我不相信有人能完全看完这四千字的解释,所以仅作为个人学习记录的存档,大佬们看个笑话就好

KMP本体并不难理解,就是用一个数组存放模式串应该回溯的位置,实现被查字符串的线性时间增长,从而降低时间复杂度。已经有很多大佬讲过KMP,网上到处都是

关键在于next数组的求解,本篇主要从next讲解入手,集中讲解next求解过程和改进过程

这里采用的并不是暴力算法计算next,而是通过前后关系递推,先说普通的next:

一:next数组的一般求解

我们知道在求解next数组的时候要对模式串从一个字符开始依次增加字符个数,对于每次增加可以借用上一次next的值:比如最简单的模式串abab,当我们计算到aba时最长相同单位是a,现在next是1,接下来加入最后面这个b,也就是变成abab的时候,(前一次的next的1指向现在abab里面第一个a,即abab)我们可以在原来next的那个1的位置+1的字符(也就是abab里面的第一个b,即abab),跟新加进来的字符(b)比较,明显b==b,所以下一个的next就是前面的next+1,也就是2

用颜色表示,在aba的情况下,加入b变成abab,然后比较abab,相同,变成abab

看不懂再看一例,abcabc在abca的时候我们知道abca现在next是1,接下来放入b变成abcab,比较abcab,相同,next+1,就是2,abcab,再放入c变成abcabc,比较abcabc,相同,所以next+1,就是3,现在是abcabc

问题来了,如果不相同怎么办,比如abacabab,在倒数第二次取到的的字符串abacaba中,我们知道:abacaba现在next是3,现在加入最后的b,变成比较abacabab,显然c!=b,怎么办?

重新回到前一个next值的位置,在那个位置再取一次next的值!

现在next=3,回到next值的位置,也就是回到abacabab的三号位置,也就是abacabab,再取一次就是:这个a对应的next值是1(前面我们对aba求过,这时候的next的值是1,也就是aba),我们比较的时候从这个1号字符后面比较,也就是abacabab,发现b==b,所以next在原来的1上面+1,next就是2,即abcaabab

为什么可以这么做呢,其实细心就会发现这里偷偷改了颜色的标注,我在第七个字符‘a’(倒数第二个)上面加了红,原因就是当我们发现加了最后那个b之后变成abacabab,先观察一下,next=4已经走不通了,那next会不会等于3?

其实不会,但是我们先假设成立,那这里的前三个就等于最后三个,又由前面已知这前三个会等于倒数第四个,倒数第三个和倒数第二个,所以现在一定会有(倒数第四个等于倒数第三个,倒数第三个会等于倒数第二个,倒数第二个会等于倒数第一个),也就是其实最后四个字符必须全部相等才能实现,换句话说,只有类似aaacaaaa这种理想情况才有可能出现。怎么知道他会不会出现,其实一切都记录在前面那个next身上,如果是现在说的这种极为理想的情况,前三个是aaa,明显next是2,我们就会从2的下一位,也就是第三个字符开始判断和最后一位相不相等,也就是aaacaaaa,然后得到aaacaaaa的结论这个时候才是3

再看看我们刚才的例子:abacabab回到刚才说的,next是2,其实也是因为next数组的记录,

这个时候前面aba的next是1,我们就会从1的下一位,就是第二个字符开始判断,由此可知上一位的next就会告诉你该从哪里开始判断,比如一开始我们在next=3时开始判断会不会等于4,不会,就去看前面的next=3的3号位置,他会告诉你试试2行不行,而不是让你去试试next能不能等于3

总结来说:

如果下一位也相等就在上一次的next+1,不相等就去找上一位的next所指的位置,对那个位置的next重新进行判断!这就是代码的实现,原理就是next是百事通,知道让你从哪开始

二:next数组的改进

其实原来的next已经很好用了,他将时间复杂度从传统暴力BF算法的O(n*m)变为O(n+m),还能怎么改进?其实还能进行快速跳转

考虑这种情况:在aaabaaaa字符串中找到模式串aaaa第一次出现的下标,查无返回0

首先我们对aaaa得到一般的next数组为[0,1,2,3]

显然当搜索到aaabaaaa第三个a时一切顺利(aaaa/aaabaaaa),在第四个b这里会发生卡顿,然后模式串的指针开始回退。现在是第四个,查next的第三个是2,我们从2的下一位开始比较,现在是(aaaa/aaabaaaa)下一位a!=b,再次查next的第二个是1,退到1,现在是(aaaa/aaabaaaa)下一位又是a!=b,现在next是0了第一位还是a!=b,只能动主串了,从aaabaaaa第五个a开始算

在这个过程中,由于遵循着next,导致我们一直在对a与b进行比较,实际上我们知道如果模式串中一个字符和当前主串的另一个字符不相同,那模式串里面所有的跟这个字符相同的其他字符都和主串的那另一字符不相同。可能有些绕,放在这个例子中,我们知道aaaa最后的a跟aaab最后的b不相同,那整个aaaa前面三个a都和这个b不同。可是人类能一眼看出来计算机却不行,有什么办法能让计算机知道这件事情?

next是百事通,对next进行改进,他会告诉计算机去跳过前面三个一样的a

打个比方,如果在(aaaa/aaabaaaa)时next就告诉计算机从下一位开始,问题就得到解决了

现在需要在原来的next进行改进:

计算机不会去傻傻的回溯四个a,需要我们对他提前回溯好,然后将next设为0,计算机就只用回退到第一个a去和b比(aaaa/aaabaaaa),失败他就会自己向b之后进位

怎么对next提前回溯?先给结论:

在模式串中,对于一个字符x,去这个字符原来对应的next值的位置,对那个位置的字符y和当前字符x进行比较:如果x!=y,新旧next值就相同,并可以进行下一个字符的next的更新;如果x==y,则将y的原来的next值对应的字符z与x进行比较,并重复上述过程。

再相同就再取next再比,直到不同位置或者已经推到第一个字符为止。(最终不同就保留本次的next作为新的next,最终仍然相同就是0了)

像我们之前那个(aaaa/aaabaaaa)的例子,明显对于aaaa我们在确定最后一个a时,他的next是3,我们就去和第三个a比较,相同,那就拿第三个a的next即2进行比较,还是a,又相同,同理取next再和第一个a比较,还是相同,所以就是0了,新的next在第四个a对应值就是0,这样计算机在发现(aaaa/aaabaaaa)不同时就能舍弃前面已经相同的部分了,这就是我称作快速跳转的地方。

他的优点就在于每次计算机发现第四个a不同时都能舍弃前面三个a,而我们仅仅只做了一次运算就让计算机一直避免——舍不得前面的a,舍不得大量回溯——这个问题。

下面是代码实现,如果你试过就会直到,新的next其实会比原来的next少一个值,其实少的是最后一个值,而且不需要用到,因为即使是比较模式串的最后一个字符,不相同时我们也是查找到next的倒数第二个值(因为每次都是去找当前这个字符前一个的next值),最后一个值实际上不用到,所以就不把他放入新的next了。

本蒟蒻对next数组的进阶用法还不清楚,如有错误还请大佬指正。

#include<bits/stdc++.h>
using namespace std;
//算法本体 
int KMP(string a,string b,vector<int>next){
	int i=0;
	int j=0;
	while(i<a.length()){
		if(a[i]==b[j]){
			i++;
			j++;
		}else if(j==0){
			i++;
		}else{
			j=next[j-1];
		}
		if(j>=b.length()){
			return i-j;
		}
	}
	return -1;
}
//next数组求解 
vector<int> NEXT(string b){
	vector<int>next(1,0);
	int next_value=0;
	int i=1;
	while(i<b.length()){
		if(b[next_value]==b[i]){
			next_value++;
			next.push_back(next_value);
			i++;
		}else if(next_value==0){
			next.push_back(next_value);
			i++;
		}else{
			next_value=next[next_value-1];
		}
	}
	return next;
}
//next数组修正,提高效率 
vector<int>NEXT_improve(string b,vector<int>next){
	vector<int>next_new;
	int i=1;
	int j=1;
	while(i<b.length()){
		if(j==0){
			next_new.push_back(0);
			i++;
			j=i;
		}
		else if(b[i]!=b[next[j-1]]){
			next_new.push_back(next[j-1]);
			i++;
			j=i;
		}
		else{
			j=next[j-1];
		}
	}
	return next_new;
}
//主函数 
int main(){
	cin>>string b;
	cin>>string c;
	vector<int>v;
	cout<<b<<endl<<c<<endl;
	v=NEXT(c);
	for(auto p=v.begin();p!=v.end();p++){
		cout<<*p<<" ";
	}//试试普通的next
	cout<<endl;
	v=NEXT_improve(c,v);
	for(auto p=v.begin();p!=v.end();p++){
		cout<<*p<<" ";
	}//试试改进的next
	cout<<endl<<KMP(b,c,v)<<endl;
	return 0;
}

  • 21
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值