字符串匹配之KMP算法

1.暴力匹配方法

假设现在我们面临这样一个问题:有一个文本串S,和一个模式串P,现在要查找P在S中的位置,怎么查找呢?

如果用暴力匹配的思路,并假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置,则有:

  • 如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符;
  • 如果失配(即S[i] ! = P[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。

代码如下:

int ViolentMatch(char* s, char* p)
{
	int sLen = strlen(s);
	int pLen = strlen(p);
 
	int i = 0;
	int j = 0;
	while (i < sLen && j < pLen)
	{
		if (s[i] == p[j])
		{
			//①如果当前字符匹配成功(即S[i] == P[j]),则i++,j++    
			i++;
			j++;
		}
		else
		{
			//②如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0    
			i = i - j + 1;
			j = 0;
		}
	}
	//匹配成功,返回模式串p在文本串s中的位置,否则返回-1
	if (j == pLen)
		return i - j;
	else
		return -1;
}

Q:为什么是 i - j -1?

A:因为主串的i和模式串的j此时是对齐匹配的,发现不相同,那么此时模式串和主串匹配的起点是i-j,往前递增一位,自然是i-j+1


2.KMP算法

KMP算法相比暴力法而言,有效利用了模式串自身的特性,从而提高了效率。

下面先直接给出KMP的算法流程:

假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置

  • 如果 j  = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符;

  • 如果 j ! = -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了 j - next [j] 位。

换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值,即移动的实际位数为:j - next[j],且此值大于等于1。

很快,你会意识到next 数组各值的含义:代表当前字符之前的字符串中,有多大长度的相同前缀后缀。例如如果next [j] = k,代表 j 之前的字符串中有最大长度为k 的相同前缀后缀。

此也意味着在某个字符失配时,该字符对应的next 值会告诉你下一步匹配中,模式串应该跳到哪个位置(跳到next [j] 的位置)。

  • 如果next [j] 等于0或-1,则跳到模式串的开头字符

  • 若next [j] = k 且 k > 0,代表下次匹配跳到j 之前的某个字符,而不是跳到开头,且具体跳过了k 个字符(跳过的都是模式串本身重复的)。

牢牢记住:KMP算法保证主串的i值不回溯,next[j]代表当发生匹配失败时,j下次应该指向哪里(指向next[j]),来再次和主串i(不回溯)进行匹配。

 

2.1 什么叫前缀后缀最长公共元素长度?

前缀:是指除了字符最后一个元素外,其余全部的子集

后缀:是指除了字符第一个元素外,其余全部的子集

做成表格形式为:

这就是模式串对应的最大长度表,j=0时,最大长度为0,j=1时,最大长度为0.....j=5时,最大长度为2,j=6时,最大长度为0;

每个j对应有一个:从第一个字符到j字符这段字符前后缀最大公共元素长度

 

2.2 什么是j对应的next数组?

当匹配到一个字符失配时,其实没必要考虑当前失配的字符(第j个),而是看失配字符的上一位字符(第j-1个)对应的公共元素最大长度值。如此,便引出了next 数组。

next[j]的含义:j对应的next数组就是其上一个元素即j-1个对应的最大公共元素长度

在KMP算法中的含义:next[j]代表当发生匹配失败时,j下次应该指向哪里(指向next[j]),来再次和主串i(不回溯)进行匹配。

next数组就干了两件事:

  • 最大公共长度表右移一位
  • 初始值为-1

如下图便是j对应的next数组

 

2.3 如何用代码求next数组?

我们已经知道的是:next[0] =-1;next[1] = 0;

我们要用代码求出next数组,其核心问题是:

怎么由next[j]  -> next [j+1]?

假定给定模式串ABCDABCE,且已知next [j] = k,现要求next [j + 1]等于多少?

因为我们已经知道next[j] = k,就是说j前面的字符前缀和后缀有k个重复的,即相当于前缀->p0... pk-1(k个) = 后缀 -> pj-k... pj-1(k个)

这个只要知道 next[j] = k,上面就一定成立,这个条件我们要牢记。

已知next[j] =k ,即已知的是两个值:k和j,那我们先判断下pk 和 pj的值,

①pk = pj

若pk =pj,如图例, j = 6,此时pk = pj = C,所以:

next [j + 1 ] = next[ j ] + 1 = k + 1(可以看出next[j + 1] = 3)

代表字符E前的模式串中,有长度k+1 的相同前缀后缀。

其实很好理解:

因为我们知道了next[j],又next[k]和next[j]是对称的(从0~k-1(下标k前面的)有k个,j-k到j-1(下标j前面的)有k个,这k个是两者重复的元素)

若此时再:next[k] == next[j],即本来又对称,现在两边又各自多了一个相同的元素,当然也对称了

所以其前缀和后缀相同有k+1个,即next[j+1] =next[j] +1 = k + 1;

其实这也代表着元素E前面有多少个相同的前缀和后缀(next[j+1]的含义)

若pj=pk,则此时变为k+1

 

②pk  != pj

若pk != pj,如图例,那么就说明:

存在:   p0... pk-1 = pj-k... pj-1 (因为next[j]=k

不存在:p0... pk-1 pk  =  pj-k... pj-1 pj(因为pk != pj

很明显,因为C不同于D(pk != pj),所以ABC 跟 ABD不相同,即字符E前的模式串没有长度为k+1的相同前缀后缀,也就不能再简单的令:next[j + 1] = next[j] + 1 。所以,咱们只能去寻找长度更短一点的相同前缀后缀。

因为我们要求的是E前面有多少个同前缀和后缀,我们以后缀为中心,已经知道后缀pj=D,既然是找前缀==后缀最大长度,那就是找一个前缀有D:AXXX...D == 后缀AXXX...D

(根据定义就能知道,前缀必包含第一个字母,后缀必包含最后一个字母)

既然已经知道pk!=pj了,那当务之急是我们找到一个:等于pj的新的pk'(如果有,它必然是D

结合上图来讲,若能在前缀“ p0 pk-1 pk ” 中不断的递归前缀索引k = next [k],找到一个字符pk’ 也为D,代表pk’ = pj,且满足p0 pk'-1 pk' = pj-k' pj-1 pj,则最大相同的前缀后缀长度为k' + 1,

从而:

next [j + 1] = k’ + 1 = next [k' ] + 1

否则:

前缀中没有D,则代表没有相同的前缀后缀

即:next [j + 1] = 0

如上图中青黄色就是找到的新的k',即k' = next[k]

Q:那为何k = next [k]呢?

理解:

注意看这个红黄蓝分区图,我们要求的是next[j+1],我们已经知道的是:

  • next[j] = k     //它代表在Pj前面,有k个同前缀和后缀
  • Pk!=Pj

next[j+1] 即代表着j+1前面j个字符最大同前后缀长度,我们先假设:存在一块前缀区域从P0~Px,等于后缀区域Pj-x~Pj-1,即Px = Pj

要知道P0~Pk-1(k个前缀) 和Pj-k~Pj-1(k个后缀)可是完全相等的,他俩大范围都相等,那对应子区肯定相等!那么根据对称原理,已知下图区域1和4相同,可以发出三个灵魂质问

区域1和区域3是不是完全相等?   当然是!

区域2和区域4是不是完全相等?   当然是!

那区域1和区域2是不是完全相等?当然是!

所以:如果我们再能找到存在这么一个Px,即我们要找到一块1区域和区域4相等,就是在k前面的字符找到一块1区域使2区域相等?

这不就是对下标Pk进行求最大前缀和后缀吗?

也就是求next[k]啊!

故:找到这么一个新的k',使得k' =next[k]

写成代码如下:

void GetNext(char* p,int next[])
{
	int pLen = strlen(p);
	next[0] = -1;
	int k = -1;
	int j = 0;
	while (j < pLen - 1)
	{
		//p[k]表示前缀,p[j]表示后缀
		if (k == -1 || p[j] == p[k]) 
		{
			++k;
			++j;
			next[j] = k;                    //即next[j+1]=next[j]+1=k+1;
		}
		else 
		{
			k = next[k];
		}
	}
}

2.4 为什么i和j不匹配时,j要指向next[j]?

这其实是问next[j]有什么作用,为什么用于KMP算法能提高效率?

其实意味着在某个字符失配(Pj != Pi)时,j字符对应的next[j] 值会告诉你下一步匹配中,模式串下一步应该跳到哪个位置(跳到next [j] 的位置)。如果next [j] 等于0或-1,则跳到模式串的开头字符,若next [j] = k 且 k > 0,代表下次匹配跳到j 之前的某个字符,而不是跳到开头,且具体跳过了j-k 个字符。

如图:

此时 i = 9,j = 6时,发生不匹配的状况,由此我们知道:在j==6时,i和j才不匹配,那么0~j-1对应长度肯定是匹配的。

j-1有最长后缀和前缀串AB,既然已经知道i的ABCDAB和j的i的ABCDAB是匹配的,那么模式串的后缀串AB肯定也是匹配的,模式串的后缀串AB又等于模式串的前缀串AB,那肯定模式串的前缀AB也和主串后面的AB是匹配的!

直接从模式串前缀串AB后面的开始比较不就行了?这就是我们的next[j]的作用。

next[j] == 2那么从模式串[2]的元素开始和 i 匹配即可,即第三个元素开始和 i 匹配,也就是让模式串的第三个元素C和主串的空格对齐,此时 j = 2,i = 9 不变,也就是j指向了next[j],继续开始匹配。

 

2.5 模式串移动了多少?

让 j = next[j],看起来是让主串的后缀串和模式串的前缀串对齐了,但因为模式串的后缀串已经和主串的后缀串匹配了(因为在j才发生匹配失效,因此j前面的都已经匹配到了),所以本质上是让模式串的重复后缀串和前缀串对齐了,假设模式串的长度是Length,那么,

原先对其的元素是j,j匹配失效后,才让j指向next[j],因此模式串是从:j和i对齐 -> k和i对齐

其实移动了:j-k 的距离,而不是k的距离

 

 

 

2.6 KMP算法完整代码

int KmpSearch(char* s, char* p)
{
	int i = 0;
	int j = 0;
	int sLen = strlen(s);
	int pLen = strlen(p);
	while (i < sLen && j < pLen)
	{
		//①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++    
		if (j == -1 || s[i] == p[j])
		{
			i++;
			j++;
		}
		else
		{
			//②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]    
			//next[j]即为j所对应的next值      
			j = next[j];
		}
	}
	if (j == pLen)
		return i - j;
	else
		return -1;
}

 

 

Refre:https://blog.csdn.net/v_july_v/article/details/7041827

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值