讲解KMP算法的核心【套娃】思想

引用一段别人的答案

KMP 算法(Knuth–Morris–Pratt algorithm)的基本思想 阅读本文之前,您最好能够了解 KMP
算法解决的是什么问题,最好能用暴力方式(Brute Force)解决一下该问题。 KMP 算法主要想解决的是文本搜索的问题:
给定一个模式字符串 p 和一个子串 t, 找出 p 串出现在 t 串中的位置。

术语定义

“abc”(引号中的字符串): 代表字符串字面值 a…z(单个斜体小写字母): 代表字符串。 A…Z(单个大写字母):代表单个字符。
prefix(x, n): 字符串 x 的前 n 个字符构成的子串(前缀)。 suffix(x, n): 字符串 x 的后 n
个字符构成的子串(后缀)。 |a|: 字符串 a 的长度。 如: 字符串 x = “abcdef”, 则 prefix(x, 3)
=“abc”, suffix(x, 3) = “def”,|x| = 6。

KMP 算法的基本思想 假设字符串 x = prefix(p, n),且存在 i > 0 使得字符串 y := prefix(x, i)
:= suffix(x, i), 则p, x 和 y 之间的关系如下图:

讲解KMP算法的核心【套娃】思想_第1张图片

若 t 串匹配到 p 串的前缀x,并且在 x 串的下一个串匹配失败,如下图:

讲解KMP算法的核心【套娃】思想_第2张图片

仔细观察上图可以发现,此次匹配失败后,我们不用按照暴力算法直接将 p 串移动一位,从头开始比较。 而是将 prefix(x, i) 移动到
suffix(x, i) 的位置,继续比较第 |y|+1 位。 这是因为此时已经匹配成功的 p 串和 x 串(即, prefix(t,n))
相等。 结合下图(移动后的情况),仔细理解上一句话:

讲解KMP算法的核心【套娃】思想_第3张图片

以上,就是 KMP 算法的最核心思想。我们不难发现,i 越大,移动之后匹配成功的字符就越多, 并且只有 i 取得最大值时,
才不会移动过多的位。 因此,KMP 算法找的是使得 prefix(p, i) == suffix(p, i) 最大的 i, 记作
i_max, 此时的 y 串记作 y _max。

容易求得,每次移动的位数是 |x| - | y _max|。 将 prefix(p, 1…|p|) (即 p 串的所有前缀 ) 的
i_max 打成一个表格,就是 KMP 算法所谓的 next 数组。

———————————————— 版权声明:本文为CSDN博主「杨领well」的原创文章,遵循CC 4.0
BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yanglingwell/article/details/79829549


讲解KMP的套娃思想。

每一个前缀,就象征一个下标,每个下标就是一个字符串,每个字符都会有一个最长相等前后缀。

计算next数组的目的就是找出每个元素最长相等前后缀的长度。

有n个下标,就要计算n次,每次都用暴力算法找最长相等前后缀i_max,那么每次就得花费n*n 步,每一次的具体过程就是,假设i_max为1,检查前后缀字符(需要比较n次),然后再假设i_max为2…直到找到为止。

我们kmp的过程就是不停的移动长串下标,需要执行m步,m为长串长度。

所以kmp的算法取决计算next数组需要花费的复杂度,其中性能瓶颈是需要花费多少步找到每一个i_max,如果花费常量步就是O(n * 1),如果花费i步就是o(n*n).

总复杂度,最优的是找出next规律,直接在常量步判定出来即o(m + n * 1)

大部分是用动态规划,实现o(m + n * n)

暴力算法是 o(m + n * n * n)
上面说了一大堆,那kmp的核心思想究竟是什么呢?答案是“套娃”。

满足两个条件就可以开始套话了,1.有两个相等的字符串,2.两个字符串均满足字符串前缀等于后缀这一特征。

只要这两个条件成立我们就可以开始套娃了。

1.从一个字符串AD中截取一段相等的前缀AB和后缀CD(可能相等前缀和后缀长度是0,也就是不存在相等的前后缀 |AB| = |CD|=0)
2.然后如果要找如果要我们在AB和CD上各找一个点m1和m2,使得|Am1| = |m2D|要怎么找呢?

答案是套娃,我们知道|AB|=|CD|,如果|AB|的存在相等的前后缀Aa和bB(且|Aa|=|bB|>0).

那么一定满足Aa = bB = Cc = dD (Cc,dD是CD的前后缀)
, a、d就是我们要找的m1,m2,即m1 = a, m2 = d.

同理,我们可以继续套娃。

既然Aa = dD,如果Aa存在相等的前后缀 Ai = ja,那么dD也一定存在相等前后缀dI = JD

即Ai = ja = dI = JD.

即Ai = JD.

继续套娃,假如Ai存在相等前后缀 Ax = yi,那JD也一定…

这样无休止的套娃,知道某一个字符串不存在前后缀为止。

结果你会发现,上面所有的以A、D为端点的线段都是原字符串AD的相等前后缀,只不过越来越短而已。

知道了这个套娃规律(KMP的核心思想)。
我们就可以开始写next数组生成算法中基于动态规划的状态转移方程了。

dp[n]表示长度为n的短串他的前缀next值next[n-1](next[i]表示长度为i的前缀)。

比如dp [5],即长度为5的字符串abcae,他的前缀abca的next值next[4].

其中next[4]=1则表示,最长相等前后缀长度为1(abca,最长相等前后缀是a,a长度是1)。

然后dp方程就是

dp[n] =
src[next[n-1] + 1] == src[n] ?
next[n-1] + 1 : 套娃找src[n-1]的下一个子串。

写出函数就是

定义函数:

// 找出AD字符串的第i长前后缀长度度(肯定在next数组里)
int findLen(int i, String AD) {
int len = strLen(AD);
while(i == 0) {
len = next[len];
i–;
}
return len;
}

dp[n] = findNext(1, n);

// 找出长度为n的子串的第k个相等前后缀的长度。
int findNext(int k, int n) {
if(src[findLen(k, substring(0, n)) + 1] == src[n] ) {
return len + 1;
} else {
return findNext(k+1, findLen(k+1, substring(0, n)));
}
}

看起来花里花俏的,是因为故意为了套娃而套娃,实际上我们老老实实的自底向上写动态规划算法就简单多了。


int[] generateNext(string substr)
{
	int[] next = new int[substr.size()];
	next[0] = 0;
	int j = 0;
	int m = substr.size();
	for (int i = 1; i < m; i++)
	{
/*可以尝试例子: a b c a d a b c a b 最后一个b是2.这就是回溯到最前面的a,然后比较第二个b得到的数值。	注意:这个是当子串很长的时候才有的情况,当前比较的字符的前一个字符跳到前面相同字符的位子,回溯查找。
思想就是:相同字符往前跳。一定要会举例子,才能明白的!!!
*/
		while (j>0 && substr[j] != substr[i])
		{
			j = next[j-1];
		}
		if (substr[j] == substr[i])
 		{
			j++;
		}
		next[i] = j;
	}
	return next;


你可能感兴趣的:(用算法来学计算机,算法与数据结构)