当前位置: 首页 > news >正文

KMP算法_next与nextval计算详解_图解案例版

KMP 算法中nextnextval的计算详解

本文统一采用0 下标:模式串P的下标范围为0 ~ m-1,并规定next[0] = -1。不同教材可能采用 1 下标或 LPS/前缀函数,因此数组数值可能不同,但本质相同。计算数组时,必须与对应的匹配代码保持同一种约定。

1. 学习目标

完成本文后,你应该能够:

  1. 理解 KMP 算法为什么不需要回退主串指针;
  2. 准确说明next[j]的含义;
  3. 用手算和代码两种方式求出next数组;
  4. 理解nextval如何消除无效比较;
  5. 独立实现基于nextnextval的 KMP 字符串匹配。

2. KMP 算法解决了什么问题

设:

  • 主串为S,长度为n
  • 模式串为P,长度为m
  • 当前已经成功匹配了P[0..j-1]
  • 下一次比较S[i]P[j]时发生失配。

朴素匹配会把主串起点向后移动一位,再从模式串开头重新比较。KMP 利用已经匹配成功的模式串前缀,直接计算模式串应回退到的位置:

主串: ... [已经匹配成功的部分] X ... 模式串: P[0 ........ j-1] P[j] ↑ 失配 KMP:主串下标 i 不回退,只令 j = next[j]

因此,KMP 的关键不是“跳过字符”,而是利用模式串自身的重复结构,避免重新比较已经确定的信息。

2.1 图解:朴素匹配与 KMP 的根本区别

第一次失配时,普通匹配会移动主串起点,而 KMP 保持i不变,只令j = next[j]。在图示案例中,S[5]=BP[5]=C失配后,j跳到next[5]=3,随后直接比较S[5]=BP[3]=B

核心结论:“主串不回退”指的是i不向左移动;j可以沿next链多次跳转。


3. 前缀、后缀与最长相等前后缀

3.1 前缀

字符串的前缀是从第一个字符开始、但不包含整个字符串本身的子串。

例如,字符串ababa的真前缀为:

a ab aba abab

3.2 后缀

字符串的后缀是以最后一个字符结束、但不包含整个字符串本身的子串。

ababa的真后缀为:

a ba aba baba

3.3 最长相等前后缀(最长 border)

ababa的前缀和后缀中,最长的相等字符串是aba,长度为3

这个长度就是 KMP 计算next的核心依据。


4. 本文中next[j]的准确含义

当模式串位置j发生失配时,令:

j = next[j]

本文采用的定义是:

对于j > 0next[j]等于子串P[0..j-1]的最长相等真前缀与真后缀的长度。

形式化表示为:

next[0] = -1 next[j] = max{k | 0 <= k < j,且 P[0..k-1] = P[j-k..j-1]}

其中:

  • k = 0表示不存在非空的相等前后缀;
  • next[0] = -1是哨兵值,用于统一匹配代码;
  • next[j]也是失配后模式串下一次参与比较的位置。

为什么考察的是P[0..j-1]

因为P[j]已经失配,真正已经匹配成功的是它前面的j个字符:

P[0], P[1], ..., P[j-1]

只有这部分信息可以被复用。


5. 手工计算next的通用方法

对模式串中的每个位置j

  1. 取出位置j之前的子串P[0..j-1]
  2. 写出它的所有真前缀和真后缀;
  3. 找到最长的相等前缀和后缀;
  4. 将其长度记为next[j]
  5. j = 0,直接规定next[0] = -1

图解示例:P = ABABAC

对于j = 5,考察的是P[0..4] = ABABA,最长相等真前后缀是ABA,长度为 3,所以next[5] = 3。字符P[5] = C不参与next[5]的计算。

示例模式串

P = ababaca

字符和下标:

下标j0123456
字符P[j]ababaca

逐项计算

j计算对象P[0..j-1]最长相等真前后缀长度next[j]
0--1
1a空串00
2ab空串00
3abaa11
4ababab22
5ababaaba33
6ababac空串00

最终结果:

下标: 0 1 2 3 4 5 6 字符: a b a b a c a next: -1 0 0 1 2 3 0

补充案例:P = abcabca

下标j0123456
P[j]abcabca
next[j]-1000123
nextval[j]-100-100-1

例如j = 6时,失配前已匹配子串为abcabc,最长相等真前后缀为abc,所以next[6] = 3。又因为P[6] = P[3] = a,所以nextval[6] = nextval[3] = -1


6. 使用递推过程计算next

手工枚举前后缀容易理解,但代码不能对每个位置都重新枚举。KMP 使用两个指针递推:

  • i:当前准备计算next[i+1]的位置;
  • j:当前候选的最长相等前后缀长度;
  • 已知next[0..i],继续求后续值。

核心规则:

若 j == -1 或 P[i] == P[j]: i++, j++ next[i] = j 否则: j = next[j]

注意:发生回退时,只改变j,不改变i。这正是 KMP 利用已有信息的体现。

ababaca的关键递推过程

步骤ij判断或操作新状态/结果
10-1哨兵推进next[1] = 0
210b != a,回退j = next[0] = -1
31-1哨兵推进next[2] = 0
420a == anext[3] = 1
531b == bnext[4] = 2
642a == anext[5] = 3
753c != b,回退j = next[3] = 1
851c != b,回退j = next[1] = 0
950c != a,回退j = next[0] = -1
105-1哨兵推进next[6] = 0

C 语言实现

voidbuild_next(constchar*p,intm,intnext[]){if(m<=0){return;}inti=0;intj=-1;next[0]=-1;while(i<m-1){if(j==-1||p[i]==p[j]){++i;++j;next[i]=j;}else{j=next[j];}}}

Python 实现

defbuild_next(pattern:str)->list[int]:ifnotpattern:return[]next_array=[-1]*len(pattern)i,j=0,-1whilei<len(pattern)-1:ifj==-1orpattern[i]==pattern[j]:i+=1j+=1next_array[i]=jelse:j=next_array[j]returnnext_array

7. 为什么还需要nextval

next数组已经能够保证 KMP 正确运行,但某些情况下仍会产生必然失败的重复比较。

假设在位置j失配,并且:

P[j] == P[next[j]]

按照普通next,下一步会令:

j = next[j]

但主串当前字符刚刚与P[j]比较失败,而P[next[j]]又与P[j]相同,因此下一次比较也一定失败。

nextval的作用就是继续跳过这个无效位置。


8.nextval的定义与计算公式

先计算普通next,再按以下规则优化:

nextval[0] = -1 对于 j > 0,令 k = next[j]: 若 P[j] != P[k]: nextval[j] = k 否则: nextval[j] = nextval[k]

换句话说:

  • 若回退后比较的字符不同,保留普通回退位置;
  • 若回退后还是相同字符,继续沿nextval向前跳。

ababacanextval计算

已知:

next = [-1, 0, 0, 1, 2, 3, 0]

逐项计算:

jP[j]k = next[j]比较nextval[j]
0a-1哨兵-1
1b0b != a0
2a0a == anextval[0] = -1
3b1b == bnextval[1] = 0
4a2a == anextval[2] = -1
5c3c != b3
6a0a == anextval[0] = -1

最终结果:

下标: 0 1 2 3 4 5 6 字符: a b a b a c a next: -1 0 0 1 2 3 0 nextval: -1 0 -1 0 -1 3 -1

根据next生成nextval的 C 语言代码

voidbuild_nextval(constchar*p,intm,constintnext[],intnextval[]){if(m<=0){return;}nextval[0]=-1;for(intj=1;j<m;++j){intk=next[j];if(k>=0&&p[j]==p[k]){nextval[j]=nextval[k];}else{nextval[j]=k;}}}

Python 实现

defbuild_nextval(pattern:str,next_array:list[int])->list[int]:ifnotpattern:return[]nextval=[-1]*len(pattern)forjinrange(1,len(pattern)):k=next_array[j]ifk>=0andpattern[j]==pattern[k]:nextval[j]=nextval[k]else:nextval[j]=kreturnnextval

9.nextval优化效果最明显的例子

考虑模式串:

P = aaaaab

普通next

下标: 0 1 2 3 4 5 字符: a a a a a b next: -1 0 1 2 3 4

优化后的nextval

nextval: -1 -1 -1 -1 -1 4

当主串当前字符与某个a失配时,普通next可能依次回退到多个仍然是a的位置,产生重复失败;nextval可以直接跳过这些位置。

若某个a与主串当前字符失配,普通next可能按4 → 3 → 2 → 1 → 0 → -1逐级回退;这些位置仍然都是anextval可以直接跳到-1,省去多次必然失败的比较。

nextval只减少冗余比较,不改变匹配结果。使用next和使用nextval都是正确的 KMP。

9.1 案例一:包含多次回退的匹配过程

主串S = ABABABCABABABCAB,模式串P = ABABAC。这个主串最终不包含模式串,但非常适合观察j沿next链多次回退,而i始终不向左移动。

步骤ij比较或操作结果
155S[5]=BP[5]=C失配,j=next[5]=3
253S[5]=BP[3]=B匹配,i=6,j=4
364S[6]=CP[4]=A失配,j=2
462S[6]=CP[2]=A失配,j=0
560S[6]=CP[0]=A失配,j=-1
66-1触发哨兵规则i=7,j=0
770S[7]=AP[0]=A匹配,继续扫描

每次跳转都对应一个仍可能成为匹配开头的前后缀。被跳过的位置已经由模式串结构证明不可能成功,因此不会漏掉匹配。

9.2 案例二:最终匹配成功

S = ABABABCABABACABP = ABABAC。扫描到主串下标 7 后,出现完整匹配:

主串下标789101112
S[i]ABABAC
模式下标012345
P[j]ABABAC

匹配结束时i=13j=6=m,所以匹配起点为i-j=7

应用案例:编辑器查找、日志关键词扫描、DNA/蛋白质序列片段定位、网络数据流中的固定模式检测。


10. 完整 KMP 匹配代码

10.1 使用任意回退表进行匹配

intkmp_search(constchar*text,intn,constchar*pattern,intm,constinttable[]){if(m==0){return0;}inti=0;intj=0;while(i<n&&j<m){if(j==-1||text[i]==pattern[j]){++i;++j;}else{j=table[j];}}return(j==m)?(i-j):-1;}

调用时:

intnext[m];intnextval[m];build_next(pattern,m,next);build_nextval(pattern,m,next,nextval);intpos1=kmp_search(text,n,pattern,m,next);intpos2=kmp_search(text,n,pattern,m,nextval);

pos1pos2的结果应完全相同,只是比较次数可能不同。

10.2 完整可运行示例

#include<stdio.h>#include<string.h>voidbuild_next(constchar*p,intm,intnext[]){if(m<=0)return;inti=0;intj=-1;next[0]=-1;while(i<m-1){if(j==-1||p[i]==p[j]){++i;++j;next[i]=j;}else{j=next[j];}}}voidbuild_nextval(constchar*p,intm,constintnext[],intnextval[]){if(m<=0)return;nextval[0]=-1;for(intj=1;j<m;++j){intk=next[j];if(k>=0&&p[j]==p[k]){nextval[j]=nextval[k];}else{nextval[j]=k;}}}intkmp_search(constchar*text,intn,constchar*pattern,intm,constinttable[]){if(m==0)return0;inti=0;intj=0;while(i<n&&j<m){if(j==-1||text[i]==pattern[j]){++i;++j;}else{j=table[j];}}return(j==m)?(i-j):-1;}intmain(void){constchar*text="bacbababadababacambabacaddababacasdsd";constchar*pattern="ababaca";intn=(int)strlen(text);intm=(int)strlen(pattern);intnext[64];intnextval[64];build_next(pattern,m,next);build_nextval(pattern,m,next,nextval);printf("next: ");for(inti=0;i<m;++i)printf("%d ",next[i]);printf("\\nnextval: ");for(inti=0;i<m;++i)printf("%d ",nextval[i]);intpos=kmp_search(text,n,pattern,m,nextval);printf("\\nmatch position: %d\\n",pos);return0;}

预期输出:

next: -1 0 0 1 2 3 0 nextval: -1 0 -1 0 -1 3 -1 match position: 10

11. 时间复杂度与空间复杂度

11.1 构造数组

  • 构造nextO(m)
  • 构造nextvalO(m)
  • 额外数组空间:O(m)

11.2 字符串匹配

KMP 匹配过程中:

  • 主串指针i不回退;
  • 模式串指针j虽然可能多次回退,但总操作次数仍为线性级别。

因此匹配时间复杂度为:

O(n + m)

其中O(m)用于预处理模式串,O(n)用于扫描主串。


12. 与其他教材记法的对应关系

12.1 1 下标版本

有些教材令模式串下标为1..m,并规定:

next[1] = 0

与本文 0 下标版本的对应关系通常为:

next_1based[j + 1] = next_0based[j] + 1

例如ababaca

本文 0 下标:-1 0 0 1 2 3 0 常见 1 下标: 0 1 1 2 3 4 1

12.2 LPS 或前缀函数pi

很多算法题使用 LPS(Longest Prefix Suffix)或前缀函数pi

pi[k] = P[0..k] 的最长相等真前后缀长度

本文nextpi的关系为:

对于 j >= 1:next[j] = pi[j - 1]

因此,看到不同数组时,先检查:

  1. 下标从 0 还是 1 开始;
  2. 首元素是-10还是1
  3. 数组表示“失配后的比较位置”还是“当前前缀的最长 border 长度”;
  4. 匹配代码是否与数组定义配套。

13. 常见错误

错误 1:把P[j]也纳入next[j]的计算

next[j]考察的是已经匹配成功的部分P[0..j-1],不是P[0..j]

错误 2:把最长公共子串当成最长前后缀

候选字符串必须同时满足:

  • 从原串第一个字符开始;
  • 在原串最后一个字符结束。

出现在中间的相同子串不能使用。

错误 3:真前缀或真后缀包含整个字符串

“真”前缀和“真”后缀不能等于字符串本身,否则每个字符串都会与自身完全相等,失去意义。

错误 4:混用不同版本的next

例如使用next[0] = -1的数组,却配套使用next[0] = 0的匹配代码,可能导致死循环、越界或错误结果。

错误 5:构造next时失配后同时移动i

失配时只应执行:

j = next[j]

不能移动i,因为当前P[i]还需要与更短候选前缀继续比较。

错误 6:认为nextval会改变匹配结果

nextval只是进一步跳过必然失败的位置,匹配结果与普通next完全一致。


14. 练习题

练习 1

计算模式串abcabcanextnextval

练习 2

计算模式串aaaaabnextnextval,并说明为什么nextval的优化明显。

练习 3

模式串ababaca在位置j = 5失配时:

  1. 普通nextj回退到哪里?
  2. 为什么不能直接回退到j = 2

参考答案

练习 1: P = a b c a b c a next = -1 0 0 0 1 2 3 nextval = -1 0 0 -1 0 0 -1 练习 2: P = a a a a a b next = -1 0 1 2 3 4 nextval = -1 -1 -1 -1 -1 4 练习 3: next[5] = 3,因此先回退到 j = 3。 P[0..4] = ababa 的最长相等真前后缀为 aba,长度是 3; 长度为 2 的前缀 ab 并不是 ababa 的后缀,因此不能直接取 2。

15. 速记总结

1. next[j] 看的是 P[0..j-1]。 2. next[j] 等于这段子串的最长相等真前后缀长度。 3. 本文约定 next[0] = -1。 4. 失配时主串指针不回退,只执行 j = next[j]。 5. 若 P[j] == P[next[j]],普通回退会再次必然失配。 6. nextval 用 nextval[next[j]] 跳过这种无效比较。 7. next 与 nextval 都正确;nextval 通常比较次数更少。 8. 不同教材数组不同,首先核对下标和哨兵约定。
http://www.jsqmd.com/news/1246379/

相关文章:

  • 2026实测可用的免费方法:微信里能直接压缩图片的小程序怎么选 - 图片处理研究员
  • 从矿卡到世界模型:年入7.8亿的博雷顿,真正价值在“AI”里
  • 2026年7月最新万国东莞东城万达广场维修保养服务电话 - 万国中国官方服务中心
  • 劳力士济南售后热线与网点地址更新通知(2026年7月最新) - 劳力士服务中心
  • BeeWorks底座:私有化IM、丰富模块与强集成能力的企业协作平台
  • 影刀RPA 网页改版后的容错处理:选择器失效时的自适配策略
  • Skyfall AI 花 100 万美元买公司让 AI 当 CEO,这场实验能否成功?
  • TSDB产品经理实战:如何定义工业场景的时序数据模型?
  • 2026年7月叮咚变声器真实测评来咯!
  • 蔡明小柯泪洒舞台 小柯音乐剧《想把我唱给您听》首发宣传片曝光
  • 2026年7月亲身到店体验大连亨得利名表服务中心|热线及门店地址 - 亨得利官方博客
  • 深入解析C/C++ union:从内存布局到实战应用与陷阱规避
  • 陕西悬挂式除铁器品牌商综合实力榜,{年份}避坑攻略与真实用户反馈 - myqiye
  • 2026年7月最新劳力士贵阳售后网点地址及客户服务热线 - 劳力士官方服务中心
  • 2026年7月最新爱彼无锡万达广场(无锡梁溪店)维修保养服务电话 - 爱彼中国官方服务中心
  • C++实现深度优先搜索(DFS)迷宫生成算法:从原理到完整代码
  • 劳力士中国售后服务中心|完整地址与客服电话权威信息通知(2026年7月更新) - 劳力士服务中心
  • 在 Nacos 点了下线,为什么流量还是打到了停机的机器上?
  • 独立游戏美术全流程:AI辅助从概念到3D资产的实战指南
  • NVIDIA Vera CPU技术详解:88核Olympus、176线程与1.2TB/s内存
  • 2026龙岩新罗黄金回收全维度深度测评 六大片区正规门店综合对比解析 - 不晚生活号
  • AI产品A/B测试的实验设计:分流策略、指标选择与统计显著性的工程化实现
  • 2026年7月最新劳力士长沙开福天街维修保养服务电话 - 劳力士官方服务中心
  • BOM管理实战:制造业成本控制与效率提升指南
  • 亲身到店体验泉州亨得利名表服务中心|最新电话和维修地址(2026年7月更新) - 亨得利官方
  • AMD提出Agent Computer概念,PC与AC未来能否共存?
  • 深夜卡文破防?2026年全网最全10款写小说ai工具(内含避坑经验)
  • Lua与C/C++交互实战:从动态库编译到性能优化全解析
  • 石墨乳环保型厂家实力测评,零套路不踩坑,选购避坑指南 - myqiye
  • C++银行账户管理系统:从控制台项目掌握面向对象与文件操作