KMP字符串匹配算法(看完必懂!!!)

KMP算法的原理

这个算法理解起来比较复杂,看了网上很多帖子,写的都很乱,不容易理解。现在结合看过的一些书和视频写一些好理解的笔记,希望能给大家带来帮助:

总的思想还是想要回退的时候能尽量偷懒,利用已知的信息,阮老师讲的很清楚:http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html

也就是说在每次不匹配发生回退的时候,尽量能让之前比较过的一致的字符能够不用再重复匹配。

 

KMP算法的实质是,有时候,字符串头部和尾部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。

这就引出了最长的真前缀和 真后缀匹配长度的问题,而基于模式串自身的自匹配性,这个长度在给出模式串的时候已经能算出来。

我们把模式串首先遍历一遍,将每个位置之前字串的最大真前缀和真后缀长度事先存在next数组中,这样每当发生一次失配时,在这个数组中查找这个最长的匹配长度,然后移动到这个最长匹配位置。举个例子:

当上面的情况发生空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应next数组中的"部分匹配值"为2,即“AB”,因此按照下面的公式算出向后移动的位数:

  移动位数 = 已匹配的字符数 - 对应的部分匹配值

因为 6 - 2 等于4,所以将搜索词向后移动4位。

怎么求next数组?

 先来考察这样的一个场景,在字符串P[i]和模式串P[j]处发生了失配,KMP会去查表,取出next[j],即用P[t]去取代P[j],让P[t]继续与之前的P[i]相对齐。那么为什么会选定这样的t呢?或者说这样的t又具备哪些必要条件呢?

 经过分析我们不难发现,选择这个t的必要条件是:在P[j]之前已经适配的子串中,必须有一个长度为t的前缀和一个长度为t的后缀完全匹配,也就是说这个字串的首部和尾部具有一定的相似性。选择这个t必要条件也就可以表示为:

P[0,t) == P[j-t,j)

 将满足下述条件的所有t筛选出来,也就可以得到一个候选集合:

 也就是在P[j]的前缀P[0,j)中,所有匹配真前缀和真后缀的长度

在发生一次失配时,也只有来自这个集合中的t,才有资格来作为下一轮的对齐位置。

而next表,其实就是:

那么我们怎么具体怎么求解next数组呢?这里不妨采用递推策略:

 分析不难得出如下结论:

 当且仅当P[j]和它的替代者P[ next[j] ]相等时等号成立。

 也就是说当模式串的某一字符和它的继任者在这一位置相等时,如上图,那么next[j+1] = next[j] +1;(最长匹配长度变长了一位)

那么如果二者不相等呢?怎么去递推?

我们还是按照原有思路,当一次失配发生,我们调用next数组中的对应值,往最大匹配的位置上滑动。那么如果还是不匹配,我们就在新的模式串位置再次调用next中的值,即next[ next[ j ] ]。这个过程可能持续多步,直到匹配为止。见下图:

因为next[ j ]是表示真前缀和真后缀中的最长匹配长度,故next[j]<j(严格小于) ,故上面这个候选序列只会严格递减,直到下图中的最后面的情况:

在这种情况下,通常会出现问题。因为接下来和P[j]比对的那个字符根本就无从谈起。这时候就是哨兵大显身手的时候了,KMP巧妙的借助了"哨兵"。即让next[0]=-1;在模式串的最前面加上-1的通配符,它和任意字符都可以匹配。因此每当第一个字符不能匹配时,我们就用哨兵来匹配。

分析上述过程,我们发现这其实就是模式串不断自匹配的过程。

代码实现

分析完可以开始写代码了,事实上,求next数组的代码和KMP代码几乎一模一样。差别在于要设置个哨兵,以及只传一个模式串参数。我们只需要记住其中一个就够了。

根据《数据结构(C++版)》KMP算法的伪代码可以用如下伪代码表述:

1. 在串S和串T中分别设置比较的起始下标i和j;
2. 重复下述操作,直到S或T的所有字符均比较完毕;
    2.1 如果S[i]等于T[j],继续比较S和T的下一对字符;
    2.2 否则将下标j回溯到next[j]的位置,即j = next[j];
    2.3 如果j等于-1,则将下标i和j分别加1,准备下一趟比较;
3. 如果T中所有字符均比较完毕,则返回匹配的i-j;
    否则返回-1;

至此,我们搞清楚了算法思路,以下是代码:

//求Next数组
void getNext(char * p, int * next)//只需要传入模式串,模式串不断自匹配,既作为母串,又作为匹配串
{
	next[0] = -1;//初始化哨兵
	int i = 0;
        int j = -1;//j代表下方模式串中的最右匹配位置,初始化为哨兵位置
 
	while (i < strlen(p))
	{
		if (j == -1 || p[i] == p[j])
		{
			++i;
			++j;
			next[i] = j;//当前位置匹配,next[j+1]=next[j]+1
		}	
		else
			j = next[j];//将当前位置更新为next[j],再次比对
	}
}

//KMP算法
int KMP(char * t, char * p) 
{
	int i = 0; 
	int j = 0;
 
	while (i < strlen(t) && j < strlen(p))
	{
		if (j == -1 || t[i] == p[j]) 
		{
			i++;
           		j++;
		}
	 	else 
           		j = next[j];
    	}
 
    if (j == strlen(p))
       return i - j;
    else 
       return -1;
}

也可以参考下这篇文章:https://blog.csdn.net/x__1998/article/details/79951598

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值