KMP算法——(手把手算next数组)

KMP算法

  • 该算法核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数,从而达到快速匹配的目的。
  • KMP算法与BF算法(暴力算法)区别在于,主串的i不会回退,并且模式串的j不会每次都回到0位置。

第一个问题:为什么主串的i不需要回退?

看如下两个字符串:
a b c d a b g h
a b g
假设此时 i 指向 cj 指向 g,匹配失败。
就算 i 退回到 b 的位置,此时和模式串的 0 位置 a 也不一样。

第二个问题:模式串的j回退的位置?

看如下两个字符串:
a b c a b g h a b c a b c
a b c a b c
假设此时 i 指向 gj 指向 c,匹配失败。
此时 i 不进行回退,因为在这个地方前,两个字符串是有一部分相同的。
因此,我们观察,当我们保持 i 不动, j 退回到第三个位置也就是 c 时,不难发现两个字符串前 a b 是一样的。
因此,我们需要借助 next 数组,帮助我们找到 j 需要回退的位置。


next数组

  • KMP算法的精髓就是next数组,next[j]=k,表示不同的 j 对应一个 k 值;k 表示模式串下标为 j 的元素,匹配失败时,要退回的位置。
  • 求k值的规则:
  • 在模式串中,以下标为 0 开始,以下标为 j-1 结束分别找到两个相同的字符串
    得到的字符串长度就是 k
  • 不管什么数据 next[0]=-1; next[1]=0;

例如求模式串 a b c a b c a b 的next数组(数组下标从0开始)

  • 首先next[0]=-1 next[1]=0
  • 对于数组下标为 j=2c ,寻找以 a 开头,以 b 结尾的两个相同字符串,找不到,则字符串长度为 0, 即 k=0 —— next[2]=0
  • 对于数组下标为 j=3a ,寻找以 a 开头,以 c 结尾的两个相同字符串,找不到,则字符串长度为 0, 即 k=0 —— next[3]=0
  • 对于数组下标为 j=4b ,寻找以 a 开头,以 a 结尾的两个相同字符串,找到 a 字符串,则字符串长度为 1, 即 k=1 —— next[4]=1
  • 对于数组下标为 j=5c ,寻找以 a 开头,以 b 结尾的两个相同字符串,找到 ab 字符串,则字符串长度为 2, 即 k=2 —— next[5]=2
  • 对于数组下标为 j=6a ,寻找以 a 开头,以 c 结尾的两个相同字符串,找到 abc 字符串,则字符串长度为 3, 即 k=3 —— next[6]=3
  • 对于数组下标为 j=7b ,寻找以 a 开头,以 a 结尾的两个相同字符串,找到 abca 字符串,则字符串长度为 4, 即 k=4 —— next[7]=4
  • 因此该字符串的next数组-1 0 0 0 1 2 3 4
  • 在上面next数组计算时,不难发现,我们可以通过前一项的k,计算当前项的k,有两种情况:

情况一:arr[k] == arr[j-1]
例如我们在计算数组a b c a b c a b 中下标为 j=6ak 值时。
此时 j-1=5k=2 指向下标为2的 c,说明存在相同字符串 abarr[0]…arr[k-1=1] == arr[(j-1)-k=3]…arr[(j-1)-1=4]。又此时 arr[k=2] == arr[j-1=5] (== c),那么连接起来便存在相同字符串 abc,即 arr[0]…arr[k=2] == arr[(j-1)-k=3]…arr[j-1=5] 因此在上一项的基础上增加一项(从 ababc),即 next[j] = k+1 = 2+1 = 3
在这里插入图片描述

情况二:arr[k] != arr[j-1]
例如我们在计算数组 a b c a b e a b 中下标为 j=6ak 值时。
此时 j-1=5k=2 指向 c,说明存在相同字符串 abarr[0]…arr[k-1=1] == arr[(j-1)-k=3]…arr[(j-1)-1=4]。又此时 arr[k=2] != arr[j-1=5] ,那么借助 k 下标的 next 值继续回退,回退到了 0 下标,此时 arr[k=0] != arr[j-1=5] 仍然不相等,继续回退 k=-1 ,当 k=-1 数组越界,也就意味着不存在相匹配的字符串,因此 next[j] = k+1 = 0

总结,如果是情况一或者k=-1数组越界,则 next[j] = k+1。 否则为情况二,则 k=next[k] 一直回退,直到出现情况一或者k=-1数组越界。

  • C代码如下:
void GetNext(char* sub, int* next, int lenSub) {
	next[0] = -1;
	next[1] = 0;
	int j = 2;	// 当前下标
	int k = 0;	// 前一项的k,即前一项回退的下标
	// 此时 j=2 的前一项next数组值为0
	while(j < lenSub) {
		if (k == -1 || sub[j - 1] == sub[k]) {	// 情况一和数组越界情况:next[j] = k+1
			next[j] = k+1;	
			j++;	// 求下一个位置
			k++;
		}
		else {
			k = next[k];	// 情况二:不相等则回退
		}
	}
}

KMP主体

我们得到 next数组 后,便可开始从基础的暴力算法,升级为KMP算法。
借助 next数组,完成 主串 的i不会回退,模式串 的j回退到 next数组 中存放的相应位置。
str 表示主串,sub 表示模式串, pos 表示开始比较的位置。
C代码如下:

int KMP(char* str, char* sub, int pos) {
	assert(str && sub);
	int lenStr = strlen(str);
	int lenSub = strlen(sub);
	if (lenStr == 0 || lenSub == 0) {
		return -1;
	}
	if (pos < 0 || pos >= lenStr) {
		return -1;
	}

	// 对模式串sub创建next数组
	int* next = (int*)malloc(sizeof(int) * lenSub);
	assert(next);
	GetNext(sub, next, lenSub);

	// 进行遍历比较
	int i = pos;	// 遍历主串
	int j = 0;	// 遍历子串
	while (i < lenStr && j < lenSub) {
		if (j == -1 || str[i] == sub[j]) {	// j == -1 时,回退越界,一样进行++处理
			i++; j++;
		}
		else {
			j = next[j];	// 根据next数组进行回退
		}
	}

	// 模式串匹配主串,则会在模式串末尾结束
	if (j >= lenSub) {
		return i-j;
	}

	// 模式串不匹配主串
	return -1;
}
<think>好的,我现在需要帮助用户理解KMP算法next数组的求解方法。用户已经看过一些资料,但可能还是觉得抽象,所以我要用更清晰的方式解释。首先,我得回顾一下KMP的基本原理,确认next数组的作用是避免回溯,提高匹配效率。接下来,要分步骤讲解next数组的定义和求法,可能需要结合例子。 用户提供的引用中有几个关键点:next数组的核心是找最长公共前后缀,有的资料用不同方法,比如暴力循环或递归。用户提到参考了多个资料,包括next数组的逐位计过程。我需要把这些整合起来,用中文一步步解释,确保例子清楚。 首先,得明确next数组的定义,每个位置j对应的值是最长公共前后缀的长度。比如,引用[4]中的例子,模式串ababaaababaa的next数组是如何一步步求出的。我需要选择一个简单的模式串,比如"ababc",然后逐步计每个位置的next值。 接下来,考虑用户可能的困惑点,比如如何比较前后缀,如何处理不匹配时回退的情况。可能需要用图示或分步说明,比如对于每个字符,比较前缀和后缀,如果匹配则长度加1,否则回退到之前的位置继续比较。 还要注意,不同资料可能有不同的起始索引,有的从0开始,有的从1开始。需要明确说明这里使用的是哪种方式,比如引用[4]中的下标从0开始,而有些资料可能从1开始,这里可能需要统一。 然后,根据引用[3]提到的暴力for循环方法,虽然效率不高,但有助于理解。可以分步骤展示如何用循环来求解,比如外层循环遍历每个位置,内层循环寻找最长公共前后缀。 最后,总结注意事项,比如next数组的优化版本(nextval),以及实际应用中的常见错误,比如索引越界或处理边界条件。确保用户理解每一步的逻辑,并能自己动手计next数组。</think>### KMP算法next数组求解方法详解 #### 一、next数组核心作用 next数组KMP算法的核心数据结构,用于记录模式串**最长公共前后缀长度**,实现匹配失败时智能跳转指针,将时间复杂度从暴力匹配的$O(mn)$优化到$O(m+n)$[^1]。 #### 二、定义与计规则 给定模式串$P$,定义$next[j]$表示: > 子串$P[0..j]$中,**最长的相等真前缀与真后缀的长度** **计步骤**(下标从0开始): 1. **初始化**:$next[0] = -1$(首位无前缀) 2. **递推公式**: $$next[j] = \begin{cases} -1 & j=0 \\ k+1 & P[j] = P[next[j-1]+1] \\ 0 & \text{其他情况} \end{cases}$$ #### 三、手演示(模式串"ababc") | j | P[j] | 分析过程 | next[j] | |---|------|----------|---------| | 0 | a | 无前缀 | -1 | | 1 | b | 比较P[0]与P[1] → a≠b | 0 | | 2 | a | P[next[1]+1]=P[0]=a → 匹配成功 | next[1]+1=0+1=1 | | 3 | b | P[next[2]+1]=P[1]=b → 匹配成功 | next[2]+1=1+1=2 | | 4 | c | P[next[3]+1]=P[2]=a≠c → 回退比较P[next[2]+1]=P[1]=b≠c → 最终为0 | 0 | 最终next数组:$[-1,0,1,2,0]$ #### 四、代码实现(C语言) ```c void getNext(char* p, int next[]) { int len = strlen(p); next[0] = -1; int k = -1; // 当前最长公共前后缀长度 int j = 0; // 模式串指针 while (j < len - 1) { if (k == -1 || p[j] == p[k]) { next[++j] = ++k; } else { k = next[k]; // 关键回退操作 } } } ``` #### 五、关键原理图示 当$P[j] ≠ T[i]$时,模式串右移位数为: $$移动位数 = j - next[j]$$ 通过预处理的next数组,主串指针$i$无需回溯[^2]。 #### 六、注意事项 1. **优化nextval数组**:当$P[j] = P[next[j]]$时,可进一步优化跳转步长[^4] 2. **边界处理**:注意字符串下标起始位置(0或1) 3. **复杂度**:构建next数组的时间复杂度为$O(m)$,$m$为模式串长度
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值