(c1)KMP算法:从记忆力到预知力

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

KMP:从记忆力到预知力

看完你能亲手推导next数组,并说清主串指针为何永不回退

按 空格/→ 演示下一步

问一句
1 / 15 页

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

字符串匹配前缀函数算法图解无回溯

KMP:从记忆力到预知力

看完你能亲手推导next数组,并说清主串指针为何永不回退

1第 1 页 · KMP:从记忆力到预知力

暴力匹配怎么做

暴力匹配是KMP的起点:逐位对齐,失配就右移一位重来。

1
逐位对齐比较
模式串与主串左端对齐,一位一位往后比对,记录匹配进度
2
遇到失配
某一位不相等时,本次对齐宣告失败,无法继续往右比
3
右移一位重来
模式串整体右移一格,再从模式串第一位重新逐位比较
4
重复直到成功
反复对齐—失配—右移,直到匹配成功或主串剩余部分不够长
2第 2 页 · 暴力匹配怎么做

暴力匹配浪费在哪

绿色是已匹配成功、灰色是被丢弃的部分。跟着红色指针走一遍,看它怎样反复回退、反复重比。

图解渲染中…
b1:失配后主串指针i退回起点+1,而不是停在原地f1:模式串每前进一位就重跑一轮,代价 O(n×m)
3第 3 页 · 暴力匹配浪费在哪

已匹配串里藏着什么

上一页看到暴力匹配失配后把主串指针往回挪,白白重扫一遍。其实失配那一刻,主串和模式串刚比对过的那一段是完全已知的——已知的信息里,就藏着少走回头路的线索。

已匹配部分已知
失配前主串那段字符,与模式串前缀完全一样,不必回看主串
前缀=后缀重复
已知段的开头和结尾若有相同子串,失配处可无缝续接
模式串自比
重复结构只看模式串本身,可失配前预先算好
背课文接龙对应 →前缀后缀复用

课文后半段与前半段相同,读到断点直接从熟悉处续背,不用重头

4第 4 页 · 已匹配串里藏着什么

重复匹配的前缀

图示失配瞬间:已匹配部分的头尾有相同前缀,模式串直接滑到前缀对齐处,跳过重复比较。

图解渲染中…
d1:已匹配串开头出现的重复段e1:已匹配串结尾的相同段,失配点前方h1:暴力法需回退文本指针,KMP不用
5第 5 页 · 重复匹配的前缀

next 数组:把重复记下来

python

用 Python 实算模式串 "ababc" 的 next 数组,看清每个位置的最长相等前后缀。

代码高亮加载中…

nxt[i] 记录「前 i+1 个字符」里,开头与结尾能重合的最长长度——正是失配时可以跳过的匹配量。

6第 6 页 · next 数组:把重复记下来

不变性:主串指针不回退

两法都逐位比较,最易混淆的是:失败时谁在动?主串指针 i 的行为是本质区别。

暴力匹配
  • i 是位置标记,随 j 重启
  • 失配后 i 退回起点旁一位
  • 已匹配字符被反复重扫
KMP 匹配
  • i 是进度承诺,永不后退
  • 失配后仅 j 跳到 next[j]
  • 每个主串字符至多比较两次
正确的理解是:next 数组保证 j 的调整不遗漏任何可能匹配,所以 i 无需回退——这是 KMP 线性复杂度的根基。
7第 7 页 · 不变性:主串指针不回退

记忆力:过去比较不白费

上一页我们定下规矩:主串指针绝不回退。那模式串失配后靠什么对齐?靠的是刚才已经匹配成功的那段——比较过的每个结果都记下来,失配时直接查记忆,而不是重新比一遍。

记录比较结果
每次匹配成功都写入记忆,不白费
失配查记忆
不再回退重比,直接从记录中取值
跳转有依据
取出的值就是 next 数组给出的对齐位置
登山时做路标对应 →KMP 的记忆

走过的路提前标记,回头不用重新探路

8第 8 页 · 记忆力:过去比较不白费

失配跳转一步步走

失配后四步走:查表、滑动、跳过、续比,主串指针始终不回退。

1
定位失配点
比较失败时记下模式串位置 j,先别动主串指针 i
2
查 next 表
查 next[j]:失配前的串里,最长相等前后缀有多长
3
滑动模式串
把 j 更新为 next[j],模式串整体滑到新对齐位置
4
继续比较
从主串 i 和新 j 接着比,跳过的部分已保证相等
9第 9 页 · 失配跳转一步步走

预知力:先看再比

上一页走跳转时,每失配一次才查一次 next 表。能不能更快?把主串拿来之前,所有失配位置的去向就先算好——模式串自己跟自己比,答案提前备好,比较时直接翻答案。

预计算
匹配开始前,模式串自比对,算出每个位置的跳转去向
next 表
一张查询表:位置 i 失配,下一步从哪继续比
先看再比
失配时不临时推导,直接查表跳转,比较前已知结果
代价前置
多花的计算在匹配前一次付清,匹配中只剩查表
考前背公式表对应 →预计算 next 表

考场临时推导对应边比边想,提前背好对应先算好表

10第 10 页 · 预知力:先看再比

next 数组怎么算

next 数组不用暴力求:用模式串自己匹配自己,拿旧值推新值。

1
起点定零
next[0]=0,next[1]=0:长度为1时没有真前后缀
2
带值前行
求 next[i] 时,i 前面的 next 值全部已知,可当查询表用
3
自匹配比较
拿位置 i 的字符和位置 j(候选前缀长度)的字符比较
4
相等就延长
字符相等,公共前后缀长 度加一:next[i]=j+1,双双右移
5
失配就跳
不等时 j=next[j-1] 回退,同主串失配跳转一模一样
6
跳到头归零
j 退到 0 仍不等,next[i]=0,从零重新找前缀
11第 11 页 · next 数组怎么算

KMP 完整代码

python

把 next 预处理和主循环合在一起,逐行看记忆力和预知力怎么落到代码上。

代码高亮加载中…

两处 k = nxt[...-1] 是同一个动作:失配时不回头,而是查 next 表跳到下一个可能对齐的位置——这就是记忆变预知。

12第 12 页 · KMP 完整代码

自测:失配跳到哪

点击作答

KMP 匹配中主串指针 i 失配后,next 数组决定的是谁的动作?

13第 13 页 · 自测:失配跳到哪

要点收束

  • ✓暴力匹配的浪费:失败后主串指针回退重比
  • ✓已匹配部分的前后缀重叠,决定能跳过多少比较
  • ✓next 数组把重叠信息预先算好,查表即跳
  • ✓主串指针永不回退,比较次数 O(n+m)
  • ✓next 的自匹配构造:KMP 用自己算自己
延伸主题:next 优化版 nextval 怎么推导Boyer-Moore 与 Sunday 算法对比AC 自动机:多模式串的 KMP
14第 14 页 · 要点收束

课后思考

先自己想五分钟,再看参考答案——答案只是思考的起点。

1为什么主串指针 j 可以一路不回退?如果回退一次,KMP 的复杂度保证会坏在哪里?

参考答案回顾核心不变性:已匹配前缀内部的信息已经由 next 数组记录,任何失配都能在「已匹配串内部」重新对齐,无需重读主串。若回退,最坏每趟都要重比已匹配部分,退回 O(n·m)。

2在文本编辑器的「全部替换」里,KMP 比暴力匹配的优势什么时候最明显?什么时候几乎没差别?

参考答案模式串自重叠多(如 aaaab)、文本中频繁出现「部分匹配又失配」时优势最大——暴力每次都回退重比。若模式串无公共前后缀且很快失配,两者行为接近,差别不大。

3next 数组的思想能否推广到「两个字符串比对」的场景,比如编辑距离?边界在哪里?

参考答案能推广:对模式串预处理、失配时利用已比对信息的思路,催生了 Aho-Corasick(多模式)等。但编辑距离允许增删改、需要任意子串间的最优解,next 这种单链跳转不够,需 DP。

15第 15 页 · 课后思考