(c2)KMP算法:查询表

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

KMP 查询表

看完你能亲手推导next数组,并说清失配时模式串为何能跳着滑

按 空格/→ 演示下一步

问一句
1 / 17 页

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

字符串匹配next数组失配跳转算法图解

KMP 查询表

看完你能亲手推导next数组,并说清失配时模式串为何能跳着滑

1第 1 页 · KMP 查询表

暴力匹配的痛点

上一页我们说 KMP 有张查询表能避免重复比较。但在看这张表之前,得先明白它到底替我们省掉了什么——先回到最朴素的暴力匹配,看看它是怎么在失配时白白浪费工作的。

失配即回退
暴力匹配遇到字符不等,主串指针 i 退回到本次起点 +1 重新比
模式串也重置
模式串指针 j 直接回到开头,前面已匹配的信息全部丢弃
最坏 O(nm)
主串长 n、模式串长 m,每个起点都要比 m 次,如主串 AAAA… 匹配 AAB
逐字抄课文抄错了对应 →暴力匹配回退

抄错一整行就撕掉重抄,明明只错最后一个字

2第 2 页 · 暴力匹配的痛点

核心思路:模式串自匹配

上一页看到暴力匹配一失败就把主串指针往回挪,太浪费。其实想想:已匹配的那段字符完全来自模式串本身,模式串对它了如指掌——那能不能让模式串自己照照镜子,提前算好失败后从哪儿继续?

已匹配段=模式串前缀
主串中已匹配的部分,必然是模式串的某个前缀
前缀自 matching
在模式串内部找:最长的相等前后缀,决定失配后跳多远
主串指针不动
失配时只回退模式串指针,主串指针永不后退
预处理换效率
提前花 O(m) 算出查询表,匹配阶段省掉全部回溯
背课文接龙对应 →模式串自匹配

卡壳时不用重背全文,接上背过的段落末尾继续

3第 3 页 · 核心思路:模式串自匹配

next 表的定义

上一页我们发现:失配后其实能从模式串自己身上找回进度。那到底回退到哪?答案可以提前算好存进一张表——next 表。先弄清楚这张表每个位置到底存的是什么。

考察对象
只看已匹配部分 P[0..j-1],与文本串无关
真前缀/真后缀
真前缀不能是整个子串本身,真后缀同理
next[j] 的值
最长的那对相等前缀与后缀的长度
接力赛交接棒对应 →失配回退

接棒点提前定好,失配时直接去 next[j] 接着跑

next[j]=max⁡{k∣P[0..k−1]=P[j−k..j−1],  0≤k<j}\text{next}[j]=\max\{k \mid P[0..k-1]=P[j-k..j-1],\; 0\le k<j\}
4第 4 页 · next 表的定义

前缀、真前缀与真后缀

先看前缀/后缀如何派生出「真」版本,再用 ABABC 举例,最后汇合到 next 表的取值来源。

图解渲染中…
f1:真前缀与真后缀相等的部分,长度记入 next 表
5第 5 页 · 前缀、真前缀与真后缀

next 表递推构造

python

看 k 如何借已有 next 值回退,O(m) 建出整张表。

代码高亮加载中…

失败时不从头比,k 沿 next 链跳到次长候选;成功则 k 加一并写入 nxt[j],每步都由之前的项推出。

6第 6 页 · next 表递推构造

手工推一例 next 表

用模式串 abcabd 亲手递推一遍 next 表,验证构造规则。

1
初始化
next[0]=0,真前缀长度不能超过自身;指针 i=1、j=0 就绪
2
失配回退
i=1,2 时 b、c 都与 a 不等,j 退到 0,next=0
3
相等延伸
i=3 处 a=a,j 增到 1,next[3]=1
4
继续延伸
i=4 处 b=b,j 增到 2,next[4]=2
5
跳跃回退
i=5 的 d 与 c 失配,j=next[4]=2 跳,仍不等,最终 next[5]=0
7第 7 页 · 手工推一例 next 表

小测:next 值

点击作答

模式串 "abab" 中,next[3](第 4 个字符 'b' 处)的真前缀-真后缀最长公共长度是多少?

8第 8 页 · 小测:next 值

KMP 匹配框架

前面我们花大力气构造了 next 表,但它在匹配时到底怎么用?回忆暴力匹配:失配后文本串指针要往回退,前功尽弃。KMP 的妙处就在失配那一刻只动一个指针。

双指针框架
i 扫文本串、j 扫模式串,从 0 开始同步推进
匹配则双进
字符相等时 i 和 j 同时加一,继续比对下一位
失配只退 j
失配时令 j = next[j],i 永不回退
复杂度线性
i 只前进,均摊下来总比较次数 O(n+m)
读长文划重点对应 →KMP 双指针

读到的位置 i 不重读,只调整脑中已记住的进度 j

失配时:  j=next[j],  i 不变\text{失配时:}\; j = next[j],\; i \text{ 不变}
9第 9 页 · KMP 匹配框架

失配时的动作分解

匹配中每一对字符比较,只有三种结局,逐一拆开看。

1
j=-1
模式串滑过头,i 前移、j 归到 -1 起点重新对齐
2
字符相等
s[i]==p[j],双双前进:i++、j++,继续比对下一位
3
失配跳转
s[i]!=p[j] 且 j 未到头,令 j=next[j],i 不回退
10第 10 页 · 失配时的动作分解

KMP 主算法代码

python

把 next 表和匹配循环拼起来:一段真正能跑的完整 KMP。

代码高亮加载中…

build_next 与 kmp_search 共享同一套「失配沿 next 回退、匹配指针前移」逻辑,i 永不回退是线性复杂度的关键。

11第 11 页 · KMP 主算法代码

复杂度:为何是线性

从指针 j 的循环看:它只增不减,n 步内必走完,加上回退的模式串指针,总计 O(n+m)。

图解渲染中…
b2:回退量由 next 表决定,模式串指针每步至少减 1,总回退 ≤ 总前进 ≤ nc1:回退发生在模式串一侧,主串 j 不动
12第 12 页 · 复杂度:为何是线性

暴力 vs KMP

同样是匹配,核心区别只有一个:失配后文本指针动不动。看清这一点,复杂度差距自然浮现。

暴力匹配
  • 失配后文本指针回退
  • 模式串仅移一位重比
  • 最坏 O(n·m),如 AAA 匹配
KMP
  • 文本指针永不回退
  • 按 next 表跳模式串指针
  • 最坏 O(n+m),均摊线性
正确的理解是:KMP 用 O(m) 预处理 next 表,换取失配时文本指针不回退,从而避免重复比较。
13第 13 页 · 暴力 vs KMP

实例:完整匹配过程

以主串 ABABCABABABD、模式串 ABABABD 为例,完整走一遍失配跳转。

1
起点对齐
i=0,j=0 对齐比较,ABAB 四位全匹配
2
首处失配
i=4 处主串 C≠模式 D,j 跳回 next[4]=2
3
跳后重比
模式串不回退,i=4 与 j=2 比较,C≠A 仍失配
4
继续回缩
j=next[2]=0,再比较 C≠A,j=next[0]=-1
5
整体右移
j=-1 表示模式串起点前无前缀可利用,i++、j=0
6
匹配成功
从 i=5 起逐位吻合,j 走到 7=模式长度,返回 i-j=5
14第 14 页 · 实例:完整匹配过程

实例中的指针轨迹

跟着 i、j 一步步走:注意每次失配后,i 始终不回退,只有 j 沿 next 跳。

图解渲染中…
a4:i=2 时 j 到末位 A 失配,触发跳转a5:j=next[2]=0,从头比较模式串a8:第二次失配,j 退到 1 而非 0a9:j=next[2]…此处退到 1,跳过重复比较
15第 15 页 · 实例中的指针轨迹

要点收束

  • ✓失配时主串指针不回退,是线性的根源
  • ✓next[j]=真前缀与真后缀的最长相等长度
  • ✓构造 next 表本身就是模式串自匹配
  • ✓next 表让模式串按已知信息滑动,而非从头再来
  • ✓均摊分析保证双指针总移动不超过 2n
延伸主题:next 表优化版 nextvalZ 函数与 next 表的关系多模式串匹配:AC 自动机
16第 16 页 · 要点收束

课后思考

先合上讲义自己推一遍,再回头看参考答案。

1next[0] 为什么必须定义为 -1 或哨兵值?如果定义为 0,递推和匹配会在哪一步出问题?

参考答案思考路径:失配后 j 不断变小,递推和匹配都依赖 j=-1 时的特殊分支「前进、归零」。若 next[0]=0,j=0 失配会跳回 0,陷入死循环,必须有终止条件兜底。

2如果允许「自我重叠」的匹配(如模式串 AAA 在文本 AAAA 中),KMP 还能正确找出所有匹配位置吗?

参考答案能。KMP 每次匹配成功后按 next 值回退 j 再继续,而不是把 i 拉回去,重叠的起始位置会在后续推进中自然被逐一命中。可以拿 AAA/AAAA 手工走一遍验证。

3把失配跳转从「跳到 next[j]」改成「跳得更远」(如 next 的优化版 nextval),为什么不会漏掉匹配?

参考答案思考路径:nextval 剔除的是「跳过去后字符仍相等、必然再次失配」的跳转,被跳过的位置本来就不可能成功,因此剪掉冗余不损失解。回顾 next 递推中 s[i] 与 s[j] 相等的分支即可理解。

17第 17 页 · 课后思考
(c2)KMP算法:查询表 · 知识图解