(c3)KMP算法:理解next[]表

官官方信息技术老师·17 页·深入(追求细节与边界)·0 次浏览·38 天前
字符串匹配next数组前缀后缀图解算法

KMP算法:理解next[]表

看完你能亲手推导任意模式串的next[]表,并讲清匹配为何不回退

按 空格/→ 演示下一步

问一句
1 / 17 页

全部页面点击任意一页,跳回舞台从这页播放

字符串匹配next数组前缀后缀图解算法

KMP算法:理解next[]表

看完你能亲手推导任意模式串的next[]表,并讲清匹配为何不回退

1第 1 页 · KMP算法:理解next[]表

暴力匹配怎么走

KMP之前先看暴力匹配:逐位比较,失配就右移一格重来。

1
对齐起点
主串与模式串左端对齐,从第0位开始逐位比较
2
逐位比较
两串指针同步右移,字符相同就继续往后比
3
失配
遇到不同字符,本轮匹配宣告失败
4
右移一格
模式串整体右移1位,指针回到模式串开头
5
重头再来
从对齐后的新起点重新逐位比较,直到匹配或主串耗尽
2第 2 页 · 暴力匹配怎么走

暴力匹配的两大浪费

上一页我们看到暴力匹配能跑通,但主串指针 i 匹配失败后要退回去重新数。就像读一本书读到一半忘了页码,只能翻回第一页重读——问题到底出在哪两处?

主串指针回溯
i 失配后跳回 i-j+1,已比较的字符全部重比一遍
模式串步进太保守
失配后只右移一格,从零开始逐位再试
浪费的信息
前面已匹配的串是已知的,其内部结构完全可以提前利用
读错行重读整页对应 →暴力匹配失配

明明记得读到哪句话,却翻回页首重新逐字找

3第 3 页 · 暴力匹配的两大浪费

失配后必须从头比吗

点击作答

模式串在位置 j 失配时,已匹配的前缀中藏着哪条可直接利用的信息?

4第 4 页 · 失配后必须从头比吗

已匹配部分的最长公共前后缀

上页我们说了失配后不必从头比。那到底能跳多远?答案藏在失配点之前的那段已匹配文本里——看它的开头和结尾有多少是「重复」的。

真前缀与真后缀
不取整个串的前提下,从头部切的前段、从尾部切的后段
公共前后缀
同一段内容既出现在开头、又出现在结尾
取最长那个
最长公共前后缀的长度,就是失配后模式串要保留的已比长度
边界情形
没有公共前后缀时长度为 0,只能整体右移一格重来
下棋复盘对应 →最长公共前后缀

复盘时棋局后段和开局重复的部分,正是可以保留不再推演的

5第 5 页 · 已匹配部分的最长公共前后缀

next[]表:跳跃的路标

沿箭头走一遍失配流程:j 处失配后,查表、跳跃、原地继续比,主串指针永不回头。

图解渲染中…
a3:j 赋值为 next[j]:跳到已匹配前后缀的边界处a4:KMP核心:i 永不回退,只动模式串指针 j
6第 6 页 · next[]表:跳跃的路标

按next[]表匹配主串

python

用 next[] 表重跑同一组匹配:主串指针 i 永不回退,失配时只有模式串指针 j 跳到 next[j]。

代码高亮加载中…

失配时 j=next[j-1],利用已匹配前缀的公共前后缀直接对齐,主串指针 i 从不回头——这正是相对暴力匹配省时的全部来源。

7第 7 页 · 按next[]表匹配主串

暴力 vs KMP的指针行为

两者失配时都动模式串指针,但主串指针是否回退,是初学最容易混淆的关键差异。

暴力匹配
  • 失配后主串指针 i 回退到本轮起点的下一位置
  • 模式串指针 j 直接归零,从头再比
  • 已匹配的比对结果全部丢弃,重复劳动
KMP匹配
  • 失配后主串指针 i 永不回退,只增不减
  • 模式串指针 j 跳到 next[j],利用历史信息
  • 已匹配部分的最长公共前后缀被保留复用
正确的理解是:主串指针永不回退是KMP的本质,回退主串指针的必是暴力。
8第 8 页 · 暴力 vs KMP的指针行为

不回溯的直觉

上一页我们看到:KMP 的主串指针从不后退。凭什么敢不回头?因为失配那一刻,已匹配的前缀本身携带了结构信息——next[] 表已经把这段信息「吸收」干净,主串不需要再被看第二遍。

信息在模式串里
已匹配部分是模式串的前缀,它的结构早已写在 next[] 中
主串指针永不动
i 只增不减:主串每个字符最多被读一次
跳转 = 复用前缀
j 沿 next 回退,是把已比对结果换成更短的重叠前缀
总代价线性
i 不回退、j 摊还回退,整体 O(n+m)
读完的书记页码对应 →KMP 不回溯

读到第几页记住了页码,续读直接翻回,无需重翻全书找位置

9第 9 页 · 不回溯的直觉

走一个完整例子

用主串 ABABCABABD 匹配模式 ABABD,逐步验证主串指针从不回头。

1
对齐起点
主串 i=0 对模式 j=0,ABAB 连续匹配 4 位
2
第5位失配
模式第5位 D ≠ 主串 C,暴力法要回退 i=1
3
查next跳跃
已匹配 ABAB 的最长公共前后缀为 AB,j 跳到 next[4]=2
4
i不回退
主串指针停在 i=4,模式滑到 AB 继续对齐
5
续配成功
C=A 失配再跳 j=1?此处 C 对 B 失配,j=next[2]=1 仍失配退到 0,i 前进
6
完成匹配
从 i=5 起 ABABD 全配,i=10 结束,全程 i 未回退
10第 10 页 · 走一个完整例子

为什么恰好是线性复杂度

走完整个例子后你会发现:KMP 从没让主串指针回头。线性复杂度不是算出来的巧合,而是「主串指针只前进」这一条纪律的直接推论。

主串指针单调前进
每步比较后 i 要么加一,要么不动,永不回退
j 的移动有界
j 每次至多沿 next[] 跳小,总增幅受 i 限制
均摊记账
j 的每次上跳都预付了未来若干次下跳的开销
爬楼梯时的膝盖对应 →j 指针

上楼消耗的每级台阶,都在预付下楼要走的路

i每步+1,共n步;j的总下降≤总上升<n,比较总次数≤2n=O(n)i 每步 +1,共 n 步;j 的总下降 \le 总上升 < n,比较总次数 \le 2n = O(n)
11第 11 页 · 为什么恰好是线性复杂度

通配哨兵:next[0]=-1

按 next[] 跳跃时有个尴尬时刻:模式串开头就失配,j 已经是 0,没有更靠前的位置可跳了。难道要写一段特殊代码处理这种情况?更聪明的做法是——放一个万能哨兵统一收编。

哨兵值 -1
next[0]=-1,表示「跳无可跳」,比 0 更靠前的虚拟位置
统一判定
失配循环只需判断 j==-1,无需为表头单独写分支
触发动作
j 变 -1 后 i++ 且 j++ 同步执行,主串前进、j 归零重比
边界自洽
与普通跳跃共用同一套代码,逻辑收敛为一条路径
飞机场的地勤车道对应 →next[0]=-1

-1 是跑道外的牵引区,车滑到头就从这里重新起飞,不算异常

12第 12 页 · 通配哨兵:next[0]=-1

递推构造next[]表

利用「模式串自己匹配自己」递推算出next[],每一步只依赖前面的结果。

1
放好哨兵
约定next[0]=-1作通配哨兵,让递推和跳跃在边界都自动成立
2
种下初值
i=1时前面无真前后缀,next[1]=0,作为递推的种子
3
自己匹配自己
指针i在主串角色、j在子串角色,模式串对模式串做一次KMP匹配
4
匹配则延伸
P[i]==P[j]时,next[i+1]=j+1:最长公共前后缀延长一位
5
失配则递归跳
不等时j=next[j]继续试;j到-1则next[i+1]=0,i前移
6
逐步填满
i从1扫到m-1,每个next值只依赖已算好的结果,全程线性
13第 13 页 · 递推构造next[]表

手推一张完整的next[]表

python

用 Python 逐位计算模式串 "ABABC" 的 next[],每步打印依据,验证递推构造。

代码高亮加载中…

高亮行就是递推构造:k 沿已算好的 next 链回退找可延长的公共前后缀,nxt[i]=k+1 写入答案,输出与手推结果一致。

14第 14 页 · 手推一张完整的next[]表

自测:算出next值

点击作答

采用 next[0]=-1 的约定,模式串 "ababaa" 中位置 5(字符 'a')的 next 值是多少?即前缀 "ababa" 的答案。

15第 15 页 · 自测:算出next值

要点收束

  • ✓主串指针只进不退,回溯全交给模式串
  • ✓next[j]=已匹配段最长公共前后缀长度
  • ✓哨兵-1把边界情况并入同一套移动逻辑
  • ✓构造next本身用递推,均摊O(1)一次比较
  • ✓失配跳跃总代价均摊后为O(n+m)
延伸主题:next函数与有限状态自动机KMP的失配树视角扩展:Sunday与Boyer-Moore
16第 16 页 · 要点收束

课后思考

先自己想,别急着翻答案——想不动了再看参考思路。

1next表还能再优化吗?比如"AAAAB",主串再失配时跳一次够吗?

参考答案可优化为nextval:若跳到的新字符与失配字符相同,跳过去必然再失配,可继续沿用其next值。想想递推时如何顺便比较这两处字符。

2匹配成功、主串走完之后,KMP还能顺手做什么?换个场景(多模式串)还成立吗?

参考答案主串走完时j值就是最长后缀匹配长度,可用于求周期、循环子串等。多模式串则需AC自动机——把next思想推广到Trie上的失配指针。

3边界再抠一下:哨兵next[0]=-1时,若模式串首字符也失配会怎样?j=-1这条路径你推演过吗?

参考答案j=-1时主串i与模式串j同时右移(i++、j=0),等价于从主串下一字符重新开始——这就是-1而非0的原因。试着用"模式串比主串长"等极端输入推一遍。

17第 17 页 · 课后思考
(c3)KMP算法:理解next[]表 · 知识图解