前端进阶之旅前端进阶之旅
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
首页程序员面试题库KMP 时间复杂度 文本指针不回退
算法算法字符串算法

为什么 KMP 字符串匹配的整体时间复杂度是 O(n+m),文本指针为什么从不回退?

KMP通过前缀函数使文本指针i单调递增,失配时仅模式指针j回退,整体时间复杂度为O(n+m)。

前端进阶之旅 · 一题精讲更新于 2026.09.05
算法#字符串算法
先看核心答案读代码示例
理解线索

KMP线性匹配的双柱

  1. 前缀函数π记录每个位置最长相等前后缀长度
  2. 匹配时j回退失配时按π跳转,跳过无效比较
  3. i永不后退每轮循环i加1,文本仅扫描一遍

模式退化为全同字符时,π值线性上升,回退链浅,复杂度仍为O(n+m)。

核心回答

先记住这个答案

预处理模式串得到π数组,π[i]表示s[0..i]的最长相等真前后缀长度。匹配时若text[i]≠pattern[j],则j回退到π[j-1](j>0时),i不动;若相等则i和j同时加1。i从不回退,因此至多移动n次;j每次回退都源于至少一次成功匹配,所有回退次数总和≤匹配成功次数≤n,加上预处理O(m),总时间复杂度为O(n+m)。

  • 文本指针i单调递增是复杂度关键
  • 模式指针j回退次数受限于i前进次数
  • 总比较次数为O(n+m)

前缀函数如何实现线性匹配

前缀函数π[i]是模式串在索引i处的最长真前后缀长度。例如模式'abcabcd'的π为[0,0,0,1,2,3,0],π[5]=3表示前缀'abc'也是后缀'abc'。匹配时,若text[i]与pattern[j]失配且j>0,则j=π[j-1],将模式向右滑动,同时保留已匹配的前缀部分,避免文本字符被重复比较。

复杂度证明:设文本长n,模式长m。主循环中i每次增加1,共进行n轮。模式指针j在匹配成功时增加1,最多增加n次;失配时j回退,每次回退都因先前至少一次匹配成功积累了“信用”,所以j的总减少次数不超过总增加次数。因此循环内所有操作次数为O(n),加上预处理π的O(m),总复杂度O(n+m)。

KMP 匹配(返回所有出现下标)JavaScript
function kmpSearch(text, pattern) {
  const m = pattern.length;
  const pi = new Array(m).fill(0);
  for (let i = 1; i < m; i++) {
    let j = pi[i - 1];
    while (j > 0 && pattern[i] !== pattern[j]) j = pi[j - 1];
    if (pattern[i] === pattern[j]) j++;
    pi[i] = j;
  }
  const matches = [];
  let j = 0;
  for (let i = 0; i < text.length; i++) {
    while (j > 0 && text[i] !== pattern[j]) j = pi[j - 1];
    if (text[i] === pattern[j]) j++;
    if (j === m) {
      matches.push(i - m + 1);
      j = pi[j - 1];
    }
  }
  return { matches };
}
console.log(JSON.stringify(kmpSearch('abcabcabcabc', 'abcabc')));
console.log(JSON.stringify(kmpSearch('aaaaab', 'aaaa')));
console.log(JSON.stringify(kmpSearch('abababc', 'ababc')));
查看输出与解释
{"matches":[0,3,6]}
{"matches":[0,1]}
{"matches":[2]}

代码严格实现KMP,经过三个不同输入的测试:模式在文本中多次间隔出现、模式在文本开头重叠出现、模式在文本中部出现且失配多次。输出JSON数组可验证正确性。

长日志中搜寻重复模式

假设系统日志文件有10^6个ASCII字符,需要找出模式“ababac”出现的位置。暴力匹配在每次失配后文本指针回退,最坏比较次数接近6×10^6。改用KMP:预处理模式串的π数组,然后一遍扫描文本,i从0递增到末尾,每次失配j回退但i不动,总比较次数为O(n+m)。

此场景的关键在于日志中可能出现“ababab”这样的长重复前缀,失配时能一次移动多个位置。KMP利用π数组跳过那些已知不可能匹配的位置,使得文本中每个字符只被比较常数次,因此总时间与日志长度线性相关,满足实时处理需求。

线性正确性的边界条件

KMP线性性不依赖具体数据,但存在常数差异。当模式为全同字符如'aaaaa'时,π数组为[0,1,2,3,4],失配时j每次只会回退1,但匹配时j增加快,回退次数占比仍不超过50%,总比较次数约2n,依然是O(n)。真正的风险在于模式中存在大量不同border,导致π链变长,但链长受模式长度m限制,总回退次数依然≤n。

若模式或文本长度极短(如n≤1),KMP与暴力并无区别,此时不必要引入预处理开销。另外,若模式中包含通配符或需要动态更新,KMP框架不再适用,需改用Aho-Corasick或扩展算法,因为π依赖于静态模式。工程中应评估数据规模与模式变更频率,避免为短串重复计算π。

回答前,多想一步

容易答错的地方

误认为文本指针从不回退意味着从不重复比较
文本指针i确实不回退,但同一文本字符可能被多次比较(例如失配后j回退再比较同一位)。不过这种重复次数受π跳转限制,摊还后总次数仍为O(n),这正是线性复杂度的来源。
认为模式指针回退会造成O(nm)
模式指针j回退到π[j-1]跳过了很多无效比对,并非每次只退一格。即使退多步,总回退次数≤成功匹配次数≤n,所以均摊后线性。关键是把回退视为“利用已比较信息的跳跃”,而不是重置。
试着用自己的话回答

面试官还会怎么问?

KMP中失配时j一定回退到π[j-1]吗?为什么不是更远的某个值?

要保证跳转后已匹配前缀与当前后缀相等,π[j-1]是使s[0..k-1]等于后缀的长度k中最大的,因此能保留最长的已匹配信息且不丢失任何可能匹配位置。若跳得更远可能错过真实匹配。

文本指针i在匹配成功后需要回退吗?

不需要。匹配成功时j到m,记录起点后j回退到π[m-1],i继续前进。由于π[m-1]可能大于0,这意味着模式自身有重叠,下一次匹配可能用之前匹配的后缀部分,但文本指针仍然保持单调。

如果模式串中所有字符都相同,KMP是不是退化了?

不会退化。模式如'aaa'的π=[0,1,2],匹配时每个文本字符可能导致j连续回退,但每次回退都对应一次成功的匹配,总回退次数约等于匹配成功次数,所以仍是O(n)。常数因子略高,但不改变线性阶。

从一道题,走向一组知识

把知识连起来

字符串算法

KMP 算法中的前缀函数(失败函数)是什么含义,如何在线性时间内求出?

同属「字符串算法」专题,接着看 KMP 前缀函数 prefix function 计算 在具体场景中的处理方式。

动态规划

动态规划和贪心算法的决策方式有什么本质区别,为什么贪心在有些最优化问题上失效?

继续了解 动态规划 贪心算法 区别,补充本题涉及的 算法 相关知识。

参考资料

  • Prefix function. Knuth–Morris–Pratt algorithm¶

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 前缀函数如何实现线性匹配
  3. 长日志中搜寻重复模式
  4. 线性正确性的边界条件
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑