(c4)KMP算法:构造next[]表

官官方信息技术老师·13 页·深入(追求细节与边界)·0 次浏览·38 天前
字符串匹配KMP递推构造边界细节

KMP 算法:构造 next[] 表

看完你能亲手推演next[]表的每一步递推,并写出正确处理边界的匹配代码

按 空格/→ 演示下一步

问一句
1 / 13 页

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

字符串匹配KMP递推构造边界细节

KMP 算法:构造 next[] 表

看完你能亲手推演next[]表的每一步递推,并写出正确处理边界的匹配代码

1第 1 页 · KMP 算法:构造 next[] 表

暴力匹配的痛点

从左往右看:主串指针 i 一路前进,一旦失配就大幅回退,前面比对过的字符全部白比。

图解渲染中…
a2:i:主串指针,j:模式串指针,双双前进b2:i 退回本次起点下一格,已走距离全部重来c1:浪费根源:主串里已知的信息没被利用
2第 2 页 · 暴力匹配的痛点

next[] 的定义

上一页看到暴力匹配会反复回退主串。KMP 的解法是让模式串自己记住:失配时该从哪里继续。这份备忘录就是 next[] 表,先给它一个严格定义。

核心定义
next[j] = P[0..j-1] 中最长相等前后缀的长度
真前后缀
前后缀不能是整个子串本身,否则恒等于 j,无意义
边界约定
约定 next[0] = -1,表示无备选位置,j 需整体右移
下标辨析
j 指「失配位置」时,看的是前 j 个字符;另一派定义为 P[0..j] 自身,注意版本差异
书的页边索引对应 →next[] 表

读到读不懂处先查索引,索引告诉你从哪页接着读,而不是从头翻

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

两种情形的分界

从左往右看:求 next[j] 时先查一条边是否相等,相等走短路,不等走回退长路。

图解渲染中…
b1:比较 P[j-1] 与 P[next[j-1]] 是否为同一字符c1:相等则延续前一条边,长度加一d1:不等则沿失配链继续缩小候选前缀
4第 4 页 · 两种情形的分界

失配时的递归回退

失配时沿 next 表逐级回退 k,每一步都在找更短的相等前后缀。

1
在 k 处失配
P[k+1] 与主串字符不等,不能继续前进
2
查 next[k]
取 P[0..k] 的最长相等前后缀长度 j=next[k]
3
对齐前后缀
前缀 P[0..j-1] 恰好等于已匹配尾段,免重比
4
仍失配再退
新位置又对不上,令 k=next[k] 继续缩短
5
退到 -1 才停
无更短相等前后缀,从模式串头重新开始
5第 5 页 · 失配时的递归回退

手工推演示例

以模式串 ABABC 为例,从左到右逐位递推 next[],每步只看前一位的比较结果。

图解渲染中…
c3:Pj 为模式串第 j 个字符,模式串是 ABABCc4:比较失败则回退 k=next[k],退到头只能取 1c5:比较 P[j-1] 与 P[k],k 取上一步的 next 值
6第 6 页 · 手工推演示例

构造 next[] 的代码

python

把前面两页的递推与回退浓缩成一个可运行的构造函数。

代码高亮加载中…

while 是失配回退分支(k=nxt[k-1]),if 是匹配延长分支(k+=1),与手工推演的两情形一一对应。

7第 7 页 · 构造 next[] 的代码

代码逐步走查

用模式串 ababa 单步执行代码,逐项核对 next[] 的每个值。

1
初始化
next[0]=-1,i 指向 1,k 指向 -1,是递推的起点
2
i=1,k=-1
k=-1 时走 k++、next[1]=0 分支,记录边界值
3
i=2,k=0
比较 a 与 a 失配,k 递归回退到 next[0]=-1
4
i=3,k=1
比较 b 与 b 匹配,k++ 后写 next[3]=1
5
i=4,k=1
比较 a 与 a 匹配,next[4]=2;与手工结果逐项一致
8第 8 页 · 代码逐步走查

双指针的摊还分析

追踪 i 与 k 的总移动次数:i 只增不减,k 虽会回退但每次回退都由 i 的前进「付费」,总代价 O(m)。

图解渲染中…
b1:势能预算:k每前进消耗预算,回退支取预算d1:k回退后仍≥0,回退总量≤k总前进量f1:i前进m次+k前进至多m次
9第 9 页 · 双指针的摊还分析

边界情形辨析

next[] 最易错的是两个极端:无公共前后缀时取什么、全同字符时为何一路递增。边界值最能检验你是否真懂定义。

无公共前后缀
  • 例:"abcad",比较 b 与 c 处失配
  • 前后缀无交集,最长相等前后缀长为 0
  • next[j] 只能取 0,j 回到开头重比
全同字符
  • 例:"aaaaa",任何位置失配
  • 每个前缀都等于后缀,长度取满 j
  • next[j] = j-1,一路线性递增
正确的理解是:next[j] 恒等于「前 j 个字符的最长相等前后缀长」,从 0(无交集)到 j-1(全相等),两极只是同一定义的端点。
10第 10 页 · 边界情形辨析

自测:算一张 next 表

点击作答

模式串 "ababab",next[1]=0(下标从 1 开始),正确的 next[] 表是?

11第 11 页 · 自测:算一张 next 表

要点收束

  • ✓next[j] 的本质:真前缀与真后缀的最长相等长度
  • ✓递推只用两种情形:相等就延伸,失配就回退
  • ✓回退是递归的,但被 next[] 链条天然有序化
  • ✓摊还分析保证:双指针均摊各走 O(n) 步
  • ✓边界优先想 j=0 与 j=1:递推的锚点
延伸主题:KMP 主匹配的完整实现扩展:BM 与 Sunday 算法next[] 的应用:周期与循环节
12第 12 页 · 要点收束

课后思考

先合上笔记自己想十分钟,再对照参考答案,看思路是否对得上。

1失配时 next[j] 回退,为什么一定能保证前缀仍与已匹配部分对齐,不会漏掉可行解?

参考答案顺着定义想:next[j] 记录的就是「最长的相等真前缀后缀」。回退到 next[j] 等于换一个更短的候选前缀,且所有比它长的都已排除,故不漏。

2把 next 的构造换成统计每个位置的相等前后缀个数而非最长,代码要怎么改?

参考答案思路:最长的那个也是个数之一,可在求最长时顺带累加链上所有匹配长度。想想能否 O(1) 递推,还是必须沿回退链逐个数?

3如果模式串里有「通配符」能匹配任意字符,next 表的定义还成立吗?该怎么推广?

参考答案相等前后缀的定义依赖字符逐一相等,通配符打破了这一点。可把「相等」改成「可匹配」再重推构造;想想传递性还在不在?

13第 13 页 · 课后思考