想象你正在攀爬一面巨大的岩壁。路线图(模式串)已经攥在手里,而整面山壁(主串)就是你要征服的道路。普通登山者的做法是:从山脚每一个可疑的位置开始,一步步对照路线图往上爬;一旦发现哪一步对不上,立刻滑落回山脚,从旁边一格重新开始。面对一面上百米高的岩壁,他会反复上下,耗尽体力。 而聪明的登山者随身带着一块“岩钉记录表”——每爬一步,就在脚下钉一颗岩钉,把“这段路和开头哪段路是重复的”记下来。万一失手,他只会滑落到最近的那颗岩钉,而不是掉回山脚重新开始。这正是 KMP 字符串匹配算法的灵魂:next 数组就是那张岩钉记录表。它能让字符串匹配在最坏情况下也只需一次主扫,绝不倒退重来。
理解 next 数组,就是把“每步从头再来的蛮力匹配”升级为“一次扫描、安然回退”的线性算法。这是文本编辑器搜索、入侵检测、基因比对等工具的底层引擎,也是算法面试中考察思维深度的经典关卡。
目录
可视化演示
KMP算法通过next数组实现O(n+m)线性时间字符串匹配,避免了朴素算法的回溯。本文讲解KMP算法的暴力匹... 切换模式查看不同维度。
本节配有交互动效(进入视口后自动加载,约 3KB)。
一、字符串匹:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现
朴素匹配之痛:没有岩钉的登山者
一个没有岩钉、不做任何记录的登山者,会如何寻找与路线图吻合的岩壁?他会从山脚第一个位置开始,照图往上爬;一旦某一步对不上,就滑回山脚,从第二个位置重新攀爬。这种“暴力攀登”在直觉上很简单,却隐藏着巨大的体力浪费——每次滑落都丢弃了之前所有验证过的安全高度。
这个场景对应字符串匹配中的朴素算法:主串从第 0 位到第 n-m 位逐一作为起点,把模式串 m 个字符从头逐个比较。每次失配后,主串指针回到“当前起点 + 1”,模式串指针归零。表面上每个起点最多比较 m 次,但最坏情况下,比如主串为 AAAAAAB、模式串为 AAAAB,每个起点都要辛辛苦苦爬到第 4 步才惊觉失配,再跌落回山脚。
登山者很快意识到:每次回到山脚重新爬,意味着同一个位置反复被踩踏,体力的浪费全在“重复”。既然已经爬到过某个高度,为什么不能在那个高度挂上一颗岩钉,下次失手时只退到那里?这正是公共前后缀要解决的第一个问题。
- 参数含义n 和 m 分别表示主串与模式串的字符个数,× 是乘法符号。
- 最坏情形文本
AAAAAAB与模式AAAAB中,每个起点都要匹配 4 次才失败,起点总数接近 n。 - 量级结论忽略常数与低阶项,最终记作 O(n×m)。
公共前后缀:岩壁上的重复纹路
聪明的登山者在攀爬中注意到一个现象:岩壁的纹路常常有规律。沿着已爬过的这一段,某些后缀的形状恰好与这条路线图开头的某个前缀一模一样。如果能记住“这一段的前后端有多长是重复的”,那么失手时就可以退到那个位置继续,而不是退到山脚。
对模式串 P 而言,前 i 个字符(下标 0 到 i-1)的最长公共前后缀,记作 π[i]:它既等于这 i 个字符开头的一段前缀,又等于同样长度、以第 i-1 个字符结尾的一段后缀,且长度尽量大。例如模式串 ABABCAB 中,前 7 个字符自己的前缀 AB 和后缀 AB 相同,所以 π[7]=2。
对 ABABCAB 手工演算:前 1 个字符 A 的 π=0;前 2 个 AB 的 π=0;前 3 个 ABA 的 π=1(前缀 A=后缀 A);前 4 个 ABAB 的 π=2(AB=AB);前 5 个 ABABC 的 π=0;前 6 个 ABABCA 的 π=1;前 7 个 ABABCAB 的 π=2。这张“纹路重复表”正是岩钉表的前身——现在要解决的是:怎样边爬边把岩钉钉进去。
- 前缀含义P[0..k-1] 表示从下标 0 开始的 k 个字符,也就是“开头的 k 个”。
- 后缀含义P[i-k..i-1] 表示前 i 个字符中最后那 k 个字符,也就是“结尾的 k 个”。
- 相等条件这两段字符串必须逐字符完全相等,才称得上“公共前后缀”。
- 取最大值在所有满足条件的 k 中选出最大的那个,记为 π[i]。
next数组构建:边攀爬边钉岩钉
登山者不希望在出发前花大量时间研究整条路线,他更想边爬边钉:每走一步,只需要回顾“刚刚走完的这一段和开头哪一段相同”,就能确定新的岩钉位置。这正是 next 数组的递推构建思想——不需要回头反复扫描,一个指针 i 负责爬山,一个指针 j 负责比对“当前已匹配前缀有多长”。
初始时约定 next[0] = -1,这是“山脚哨兵”:当模式串第一个字符就失配时,表示无路可退,登山者必须把主线(主串)向前推一步。随后,让模式串自己和自己比较:若 P[i] == P[j],说明公共前后缀延长了一格,于是 next[i+1] = j + 1,两个指针一起前进;若不相等,j 则沿着已有的 next 链回退,直到 j=-1 或找到相等字符。
以 ABABCABAB 为例推演,最终得到 next = [-1, 0, 0, 1, 2, 0, 1, 2, 3]。登山者手里的岩钉表就这样在攀爬中完成了。值得注意的是,即使卡在某一步需要沿 next 链回退多次,回退的总次数也极为有限——这正是下一节匹配心法和第六节复杂度分析的基石。
- 哨兵起点next[0] = -1,代表第一个字符失配时模式串无路可退,让主串前进。
- 相等递推若 P[i] 与 P[j] 相等,公共前后缀延长一格,于是 next[i+1]=j+1。
- 失配回退若不相等,把 j 沿 next 链往回跳,缩小前缀长度再比。
- 终止归零当 j 回退到 -1 时,说明没有公共前后缀,next[i+1]=0。
匹配过程心法:失手只滑到最近岩钉
带着岩钉表的登山者终于正式上场了。他的铁律只有一条:主线路(主串指针 i)绝不后退,失手时只利用岩钉表把模式串指针 j 拉到安全位置。第 j 位失配时,前 j 个字符已经和主串完全吻合,而这些字符的最长公共前后缀长度正好是 next[j]——那意味着主串的当前位置之前,有一段已知安全的路可以复用。
于是匹配过程变得极简:字符相等,i++、j++;字符不等,i 原地不动,j = next[j];当 next[j] = -1 时,登山者知道脚下根本没有岩钉可借力,只能让 i++ 且 j=0。这个过程中,主串的每个字符最多被“看一眼”就永远不会回头看,模式串则反复借用岩钉缩短回退距离。
对照可视化演示可以清楚地看到:当主串第 6 位 D 与模式串第 4 位 C 失配时,i 停在原处,j 从 4 跳到 next[4]=2,继而比对 A。这次跳转让登山者保留了已确认的 AB 两个高度,避免了从山脚重爬。可是,新的问题出现了:如果岩钉的位置上是一块光滑的“重复石板”,跳过去是否还是会滑落?
- 失配处理主串第 i 个字符与模式串第 j 个字符不同时,主串指针原地待命。
- 模式跳转j 重新定位到 next[j],即最近安全岩钉的位置,继续比对。
- 匹配推进两字符相等时,双指针同步前移。
- 成功判定j 走到模式串长度 m,说明整条路线完全吻合,记录起点后继续找下一处。
nextval优化:拔出打滑的岩钉
登山者在使用岩钉表时发现了一个尴尬情况:有些岩钉钉上去根本没有意义。例如模式串 AAAA,若第 4 个 A 失配,next[4]=3 会让他退到第 3 个 A 继续比——但主串那个字符既然和第 4 个 A 不等,就一定和第 3 个 A 也不等。跳过去等于注定再失败一次,白费一段手脚。
nextval 就是“拔出打滑岩钉”的手术:在构建 next 之后,再检查每一位。若 P[j] == P[next[j]],说明跳转目标是同一个字符,跳了也白跳,干脆直接继承 nextval[next[j]];只有 P[j] != P[next[j]] 时,next[j] 本身才值得保留。这样就把“必然再次失配”的跳转压缩掉了。
以 AAAA 为例:next = [-1, 0, 1, 2],修正后 nextval = [-1, -1, -1, -1]。登山者发现整面墙都是同一种石板,最安全的做法是失配时直接滑落回山脚哨兵,连中间那几步重复的试探都省去。nextval 并不改变算法框架,它只是让每一颗岩钉都真正“挂得住”。
- 判定条件先看第 j 位的字符与 next[j] 位置的字符是否相同。
- 不相等保留两字符不同,跳过去有意义,nextval 沿用 next[j]。
- 相等继承两字符相同,跳过去必然再失配,改为继承 nextval[next[j]]。
复杂度分析:线性收益背后的账本
现在来算一笔登山者的体力账。整条主线路(主串)从第 0 米延伸到第 n-1 米,主串指针 i 在匹配过程中只增不减,最多走 n 步。模式串指针 j 的每次增长,都来源于一次成功的字符匹配,而成功的匹配总数不可能超过主串长度 n。失配时 j 回退到 next[j],每跳一次至少变小一点,可回退的“总高度”被 j 的增长总量所限制。
换句话说,即便最坏情况下回退频繁发生,总回退量也在 O(n+m) 量级——因为 j 不可能无中生有地长高,每次回退的“高度”都来自之前匹配积攒的高度。加上构建 next 数组只需要扫描一遍模式串 O(m),整个算法的复杂度便达到了优美的线性。
对比朴素算法 O(n×m) 的账本,KMP 等于把“反复上下山”变成“一次性爬完主线路”。对登山者而言,最坏情况下也不会因为某段岩壁设计得特别刁钻(全是重复字符)而体力崩盘——这就是线性保证的工程价值。
- 主串扫描指针 i 从 0 走到 n-1,不回退,因此贡献 O(n)。
- 回退总量j 每次失配回退至少缩小 1,而 j 的总增量被成功匹配次数限制,均摊 O(n+m)。
- 空间开销只需要 next 数组,长度为模式串长度 m。
实战应用:三位登山者的工地
第一位登山者是文本编辑器的开发者。用户在几十万行代码的工程里按下 Ctrl+Shift+F 搜索某个函数名,编辑器底层需要快速定位所有出现位置。如果朴素匹配在最坏情况下卡顿,用户会觉得“这编辑器怎么这么慢”;KMP 的单遍扫描与线性保证让全局搜索即便面对恶意构造的重复文本也能稳定输出结果。
第二位登山者是生物信息工程师。高通量测序仪吐出海量的短序列片段(reads),每条只有几十到几百个碱基,却要定位到长达几十亿碱基的参考基因组上。这相当于在超长的“ACGT 岩壁”上寻找成千上万条短小路线。KMP 的线性特性让每条 reads 的定位既快又可预测,是序列比对工具中关键的精确匹配组件。
第三位登山者守护在网络安全一线。入侵检测系统(IDS)需要在每个网络包中快速扫描恶意特征串,攻击者经常故意构造大量重复字符来拖慢检测系统(正则拒绝服务攻击)。KMP 的最坏线性保证意味着:无论流量数据多么“滑腻”,特征匹配的时间都不会被恶意构造击穿,始终在可控范围内。此外,反垃圾邮件与敏感词过滤也依赖同样的精确匹配引擎。
代码实战:把岩钉表写进程序
最后,把登山者的整套策略翻译成 Python 代码。核心只有两个函数:build_next 负责边爬边钉岩钉,kmp_search 负责带着岩钉表完成一次扫描匹配。代码虽短,却完整复现了 next 构建、失配跳转、成功记录与继续搜索的全过程。
nxt = [-1]— 初始化山脚哨兵,next[0] 固定为 -1。
if j == -1 or pattern[i] == pattern[j]:— 无路可退或字符相等时,进入“延长公共前后缀”分支。
nxt.append(j)— 记录下一位失配时的新跳转位置,相当于钉下一颗岩钉。
j = nxt[j]— 字符不相等时沿 next 链回退,寻找更短的可复用前缀。
while i < len(text):— 主串指针只前进不回退,体现“一次扫描”的核心。
positions.append(i - j)— 当 j 走到模式串末尾时,计算并记录当前匹配起点。
j = nxt[j]— 即使匹配成功,也要借助 next 继续寻找后续可能的匹配。
def build_next(pattern): """构建 next 数组,next[j] 表示第 j 位失配时模式串应跳转的位置""" nxt = [-1] i, j = 0, -1 while i < len(pattern) - 1: if j == -1 or pattern[i] == pattern[j]: i += 1 j += 1 nxt.append(j) else: j = nxt[j] return nxt def kmp_search(text, pattern): """在 text 中查找 pattern 的所有出现位置,返回起点下标列表""" if not pattern: return [0] nxt = build_next(pattern) i = j = 0 positions = [] while i < len(text): if j == -1 or text[i] == pattern[j]: i += 1 j += 1 if j == len(pattern): positions.append(i - j) j = nxt[j] else: j = nxt[j] return positions if __name__ == "__main__": text = "ABABDABACDABABCABAB" pattern = "ABABCABAB" print("next:", build_next(pattern)) print("matches:", kmp_search(text, pattern))
AI 应用场景
IDE 与文本编辑器全局搜索
阶段:工程基础组件
解决的问题:现代 IDE 在百万行代码中搜索标识符时,朴素匹配法在重复性高的代码片段上可能产生明显卡顿。KMP 保证最坏情况下仍为线性时间,让全局搜索、查找替换、代码跳转都稳定流畅,AI 编程助手的代码检索底层同样依赖这类精确匹配。
具体案例:VSCode 的全局搜索在索引大仓库时,对每个文件执行精确匹配过滤;当用户搜索 userInfo 这类在模板与测试中高频出现的名字时,KMP 的 next 数组让每次失配都快速跳转,避免重复比较同一段文本。配合 AI 语义理解前,精确匹配先把候选集快速缩小。
生物序列的快速定位
阶段:数据预处理
解决的问题:测序数据预处理中,需要把数百万条短 reads 精确比对到参考基因组上。朴素匹配对每条 reads 都可能扫描整个基因组,而 KMP 的单遍扫描和线性保证让每条短序列的定位耗时稳定可控,是构建后续 AI 变异检测模型前的重要清洗环节。
具体案例:一个变异检测流程需要先剔除比对到参考基因组上多个位置的 reads(多映射 reads)。工程师用 KMP 精确匹配快速找出 reads 在染色体上的所有候选起始位点,再交由统计模型判定是否属于重复区域,从而过滤噪声。相比每次从头匹配,整个预处理管线提速数十倍。
网络入侵检测与敏感词过滤
阶段:系统安全监控与内容安全
解决的问题:入侵检测系统需要在每个网络包、每段实时流量中匹配数百条恶意特征串。攻击者常构造大量重复字符试图拖垮匹配引擎(ReDoS 思想)。KMP 最坏线性时间保证不受输入分布影响,是 IDS、WAF、反垃圾系统的第一道快速过滤器,AI 安全模型可作为后续的语义研判层。
具体案例:Snort 规则库中某条特征串 "Shellcode|00|" 需要在高速网卡上逐包匹配。匹配模块先用 KMP 做精确串匹配筛掉绝大多数无关包,只有命中的包才进入深度检测与 AI 异常评分模型,从而在 10Gbps 流量下保持线速处理。
Python 演示
def build_next(pattern): """构建 next 数组,next[j] 表示第 j 位失配时模式串应跳转的位置""" nxt = [-1] i, j = 0, -1 while i < len(pattern) - 1: if j == -1 or pattern[i] == pattern[j]: i += 1 j += 1 nxt.append(j) else: j = nxt[j] return nxt def kmp_search(text, pattern): """在 text 中查找 pattern 的所有出现位置,返回起点下标列表""" if not pattern: return [0] nxt = build_next(pattern) i = j = 0 positions = [] while i < len(text): if j == -1 or text[i] == pattern[j]: i += 1 j += 1 if j == len(pattern): positions.append(i - j) j = nxt[j] else: j = nxt[j] return positions if __name__ == "__main__": text = "ABABDABACDABABCABAB" pattern = "ABABCABAB" print("next:", build_next(pattern)) print("matches:", kmp_search(text, pattern))
nxt = [-1]初始化山脚哨兵,next[0] 固定为 -1。if j == -1 or pattern[i] == pattern[j]:无路可退或字符相等时,进入“延长公共前后缀”分支。nxt.append(j)记录下一位失配时的新跳转位置,相当于钉下一颗岩钉。j = nxt[j]字符不相等时沿 next 链回退,寻找更短的可复用前缀。while i < len(text):主串指针只前进不回退,体现“一次扫描”的核心。positions.append(i - j)当 j 走到模式串末尾时,计算并记录当前匹配起点。j = nxt[j]即使匹配成功,也要借助 next 继续寻找后续可能的匹配。
常见误区
误区我看了几个教程,有的说 next[0]=-1,有的说 next[0]=0,还有人说 next 就是“最长公共前后缀表”,看得我晕头转向,随便记一个就好了。
正解next 是“失配跳转表”,前缀表 PMT(即 π 数组)是“每个前子串的最长公共前后缀长度”。本文采用 next[0]=-1、next[i]=π[i] 的经典约定;另一些教材定义 next[i]=π[i-1](右移一位),甚至有的直接让 next 从 0 开始。不同资料只是偏移量不同,语义都是“失配时该退到哪”。登山比喻里,PMT 是岩壁纹路的统计报告,next 是直接可用的岩钉编号,两者相关但不是一个东西。读代码前务必先确认作者的约定。
为何常见:网上教程对 next 的初始化与偏移约定五花八门,初学者比对多篇文章时看到表面矛盾,便误以为“怎么定义都行”,忽略了内在语义的一致性。
误区KMP 的时间复杂度是 O(n+m),朴素是 O(n×m),所以任何输入下 KMP 都比朴素快,以后写字符串匹配一律用 KMP。
正解KMP 的优势集中在“最坏情况的线性保证”和“主串指针不回退”。对于完全随机的短文本,朴素匹配通常第一个字符就失配,平均接近 O(n) 而且无需预处理与额外空间,实际可能更快。登山比喻里,在路况极好的平缓岩壁上,不带岩钉表直接爬反而更轻快;只有在重复性高、陷阱多(如 AAAAAAB 对 AAAAB)的复杂岩壁上,岩钉表的威力才真正显现。工程上应根据文本特征选用,或利用内建算法(如 Python 的 in、find 已经过 C 级优化)。
为何常见:人们容易把“最坏时间复杂度的对比”误当成“平均性能的对比”,忽略了常数因子、预处理开销与 CPU 层优化。
误区失配的时候,让 i 退回刚才匹配起点的下一个位置,同时用 next 数组优化 j,这样既有 next 的智能又有暴力的稳妥,应该更快。
正解这正是很多“伪 KMP”实现的错误。KMP 的核心保证是主串指针 i 永不自回归——失配时只移动模式串指针 j,因为前 j 个字符已经验证过匹配,next[j] 告诉我们其中有多少前缀可以复用。一旦 i 回退,最坏情况仍可能退化到 O(n×m),岩钉表的意义就消失了。可以理解为登山者只有一根主绳,主绳只会向前延伸,从不放掉已占领的高度。检查一个实现是不是 KMP,就看失配分支里 i 是否被赋值回退。
为何常见:很多人理解了“利用已匹配信息”的直觉,但没有抓住“哪个指针不动”这一关键细节,凭感觉把 next 当成普通加速技巧叠加到暴力框架上。
误区nextval 把 next 的重复跳转问题修好了,那以后构建数组一律用 nextval,next 是过时产物。
正解当模式串中重复字符很少时(如 ABCDE),next 与 nextval 几乎相同,构建 nextval 反而多一次逐位比较的开销。此外,nextval 会丢弃部分“同字符上下文”信息,在一些需要模式串位置语义的场景(如结合后缀自动机做多模式匹配)中未必合适。正确做法是:追求极致匹配速度且模式串重复度高时用 nextval;需要和传统教材、面试常规答案对齐或模式串较短时用 next 足够。登山比喻里,nextval 是“拔掉打滑岩钉”的精修,但岩钉本来就少且牢固的岩壁,精修意义有限。
为何常见:不了解 nextval 的适用边界,把“针对特定缺陷的优化”理解成“全面超越”,忽略了构建开销与信息损失。
面试题
Q1. KMP 算法相对于朴素字符串匹配的核心改进是什么?
- A.主串指针失配时不回退,只移动模式串指针← 正确答案
- B.模式串从不回退,只移动主串指针
- C.使用哈希函数加速比较
- D.完全不需要预处理模式串
答案:A。KMP 的核心是主串指针 i 永不回溯,失配时借助 next 数组让模式串指针 j 跳到安全位置。选项 B 说反了——模式串指针 j 恰恰是需要回退的一方;C 是 RK 等哈希算法的思路;D 与事实相反,KMP 必须先构建 next 数组。登山比喻中,主线路绝不放掉,只有模式串这只手退回岩钉处。
Q2. 模式串 "ABABCAB" 的 next 数组(按 next[0]=-1 的经典约定)是下列哪一项?
- A.[-1, 0, 0, 1, 2, 0, 1]← 正确答案
- B.[0, 0, 1, 2, 3, 0, 0]
- C.[-1, 0, 1, 2, 3, 0, 1]
- D.[0, 1, 2, 1, 2, 0, 1]
答案:A。逐位推演:next[0]=-1;前 1 个字符无公共前后缀得 next[1]=0;前 2 个 “AB“ 得 0;前 3 个 ”ABA“ 得 1;前 4 个 ”ABAB“ 得 2;前 5 个 ”ABABC“ 得 0;前 6 个 ”ABABCA” 得 1。因此为 [-1,0,0,1,2,0,1]。B 把哨兵位置搞错且值不对;C/D 混淆了部分匹配值与 next 的对应关系。
Q3. KMP 匹配过程中第 j 位失配时,正确的指针操作是?
- A.i 回退到本次匹配起点的下一个位置,j 归零
- B.i 保持不动,j = next[j]← 正确答案
- C.i 和 j 都前进一位
- D.i 和 j 都回退到起点
答案:B。KMP 的铁律是失配时主串指针 i 原地不动,模式串指针 j 跳到 next[j],利用前 j 个字符的最长公共前后缀跳过无效比较。A 是朴素算法的做法;C 是字符相等时的操作;D 等于完全放弃已匹配信息,退化为暴力。登山者失手时,主线位置不变,只是把模式串这只手缩回到岩钉处。
Q4. nextval 优化主要解决什么问题?
- A.next 数组构建速度太慢的问题
- B.当 P[j] == P[next[j]] 时,跳转后必然再次失配,造成无效比较← 正确答案
- C.主串无法匹配数字等特殊字符的问题
- D.模式串过长导致空间溢出问题
答案:B。若第 j 位失配,而 P[j] 与 P[next[j]] 是同一个字符,那么跳到 next[j] 后还是与主串中那个已失配字符不同,必然再次失配。nextval 让这种跳转直接继承更深一层的 nextval[next[j]]。A、D 是构建效率与空间问题,C 与字符类型无关。
Q5. 模式串 "AAAA" 的 next 数组为 [-1, 0, 1, 2],它的 nextval 数组是?
- A.[-1, 0, 1, 2]
- B.[-1, -1, -1, -1]← 正确答案
- C.[0, 0, 0, 0]
- D.[-1, 0, 0, 0]
答案:B。nextval[1] 处 P[1]=A 且 P[next[1]]=P[0]=A 相同,所以继承 nextval[0]=-1;同理第 2、3 位也继承 -1,最终全部为 -1。也就是说,对全是同一字符的模式串,无论在哪一位失配,主串那个不等字符与任何位置的 A 都不等,直接退到哨兵最省事。A 是原始 next,C/D 都违背了“相等则继承”的修正规则。
Q6. KMP 预处理(构建 next 数组)的时间复杂度是?
- A.O(n)
- B.O(m)← 正确答案
- C.O(n + m)
- D.O(n × m)
答案:B。构建 next 数组只扫描模式串一次,每个位置最多沿 next 链回退若干次,但总回退量受模式串长度 m 限制,因此整体 O(m)。n 是主串长度,预处理根本不需要碰主串,所以 A、C、D 都不对。登山者只在出发前研究路线图,不需要先爬一遍整座山。
Q7. 关于“最长公共前后缀”在 KMP 中的角色,下列说法正确的是?
- A.指模式串整体首尾相等的最长子串长度,用于匹配成功后的处理
- B.模式串每个前缀子串自身的首尾相等最长长度,被 next 借用于失配跳转← 正确答案
- C.主串中所有子串的最长重复段长度
- D.模式串与主串的最长公共部分
答案:B。next[j] 利用的是“模式串前 j 个字符”这一前缀子串的最长公共前后缀。匹配到第 j 位失配时,前 j 位已与主串一致,其公共前后缀长度直接告诉我们能安全复用多少前缀。A 只看整体不看每个前缀,C 关注主串不相关,D 是子序列问题而非前缀子串的性质。
Q8. 以下哪些是 KMP 算法的正确特点?
- A.主串指针 i 失配时不回退← 正确答案
- B.需要 O(m) 的额外空间存储 next 数组← 正确答案
- C.失配时模式串指针 j 跳到 next[j]← 正确答案
- D.最坏时间复杂度为 O(n × m)
答案:ABC。KMP 的主串指针只前进不回退(A 对),预处理需要存储长度为 m 的 next 数组(B 对),失配时 j=next[j] 是核心操作(C 对)。D 是朴素算法的复杂度,KMP 的最坏时间复杂度是 O(n+m)。登山比喻里,主线路单程、岩钉表占背包空间、失手缩手到岩钉,这三件事缺一不可。
Q9. 模式串 "ABABAB" 的 next 数组(next[0]=-1 约定),以下描述正确的有?
- A.next[5] = 3← 正确答案
- B.next[3] = 1← 正确答案
- C.next[2] = 1
- D.next[0] = -1← 正确答案
答案:ABD。推演可得 next = [-1, 0, 0, 1, 2, 3]。因此 next[5]=3(前 5 个字符 “ABABA“ 的最长公共前后缀 ”ABA“ 长为 3),next[3]=1(”ABA“ 的公共前后缀 ”A“ 长为 1),next[0]=-1。而 next[2] 对应 ”AB“,无公共前后缀应为 0,C 错误。注意 next 的索引对应的是”前 i 个字符”,容易与字符下标混淆。
Q10. KMP 在现代文本搜索工程中最重要的价值是?
- A.平均比较次数一定比朴素匹配少
- B.保证最坏情况线性时间,输入恶意构造也不怕性能被击穿← 正确答案
- C.完全不需要预处理,实现简单
- D.天然支持多个模式串同时匹配
答案:B。KMP 的 O(n+m) 是严格的最坏情况保证,即使攻击者构造铺满重复字符的文本(如 “AAAAA……“ 配 ”AAAAB”),匹配时间依然线性,这对网络入侵检测、搜索引擎防攻击至关重要。A 错误,随机短文本下朴素匹配平均更快;C 与事实相反;D 是多模式匹配(如 AC 自动机)的能力,不属于单模式 KMP 的天然特性。