当前位置: 首页 > news >正文

一文读懂:通俗易懂的KMP算法详解

我们先提出一个问题:

给你两个字符串 s 和 p(p 的长度不超过 s 的长度,且 s 和 p 都不是空的),问 s 中是否包含 p?

例如:

  • s=“hello, java”, p = “java”,那么 s 包含 p
  • s=“github”, p=“ppt”, s 不包含 p

能否写出一个程序高效地解决这个问题。

我们能容易想到这样的方法:

设置两个指针,i 和 j,都初始化为 0,我们对比 s 在 i 位置,p 在 j 位置的字符。如果 s[i] == p[j],那么 i 和 j 都移到下一个位置。否则 j 回退到 0,i 回退到 1,继续上述过程,如果在下一次比较中,还是出现了不匹配的字符,那么 j 回退到 0,i 回退到 2,继续……,周而复始。直到某一次匹配中,如果 j 到达越界位置,那么 s 包含 p,否则 s 不包含 p。

代码如下:

public class StrContains { public static boolean contains(String s, String p) { int ls = s.length(), lp = p.length(), i = 0, j = 0; while(i <= ls - lp) { int x = i; j = 0; while(j < lp && s.charAt(x) == p.charAt(j)) { ++x; ++j; } if(j == lp) return true; ++i; } return false; } }

这样的查找方法,在遇到 s = “aaaaaaaaaaaaab”,p = “aab” 这样的情况的时候,会使得 p 只有在最后一次匹配的时候,才可以得到匹配。假设 s 的长度是 N,p 的长度是 M,那么显然的最坏情况下时间复杂度就是 O(N∗M)。而 KMP 算法能做到最坏情况下 O(N+M) 的时间复杂度。它是怎么做的呢?我们一起来看看吧。

KMP 算法的计算过程启发的过程:上面的暴力方法是基于这样的一个尝试的思路,如果 s 中有一个子串和 p 是匹配的,因为任何一个子串都有一个开头位置,那么这个和 p 匹配的子串当然也有一个开头位置,又因为我们不知道哪个开头位置的子串和 p 是匹配的,因此我们尝试所有可能的开头。如果我们尝试完所有的开头位置,都没有发现一个子串可以和 p 匹配,那么 s 中就没有一个子串和匹配,即 s 不包含 p,反之 s 包含 p。那么这个过程它为什么低效呢?我们来看一下 s = “aaaaaaaaab” 和 p = “aaab” 的匹配过程。

当我们发现某一个开头的尝试已经宣告失败的时候,此时只能选择下一个开头,继续从头开始匹配。那么此时指向 s 的指针会回退,之前已经匹配的部分结果完全抛弃,从新开始,因此这个方法是低效的。如果某一次尝试失败了,那么之前已经匹配的部分(之前做过的努力)能否给我们提供一些帮助,加速我们的匹配过程,甚至能使得字符串 s 上的指针不回退呢?我们调整的时候,需要遵循什么原则呢?

为了便于说明 j 的调整,下面我们举一个明显的例子。请看字符串 s = “acacab”,和字符串 p = “acab” 的匹配过程。

那么如果已经匹配的部分有多个前缀和后缀是匹配的呢?我们怎么选择?请看 s = “aaaab” 和 p = “aaab” 的匹配过程。

总结一下:此时我们似乎找到了,保证 s 指针不回退的时候,p 的指针的调整方案,即当我们发现某一次匹配失败的时候,我们需要找出前面已经匹配部分的 前后缀最大匹配长度,假设为 next,那么我们只需要把 j 调整为 next,继续进行匹配操作即可。next 数组

我们在进行真正的匹配之前,我们要先计算好,每一个元素的 next 值(next 值的含义就是当前元素失去匹配的时候,它前面部分字符串的前后缀最大匹配长度,这个前后缀不包含自己),看下面对字符串 “caccacb” 的 next 值的定义过程:

http://www.jsqmd.com/news/1176801/

相关文章:

  • 2026年百色右江区黄金回收哪家靠谱?实测3大30年老牌机构,报价稳居榜首 - 福金阁黄金回收
  • 2026年7月最新泉州宝玑官方售后联系电话与客户服务中心网点地址 - 亨得利钟表维修中心
  • Rust在Cursor中调试崩溃的3大致命陷阱:从内存泄漏到宏展开失败,附可立即复用的配置模板
  • MediaCrawler终极指南:5分钟掌握多平台数据采集技术
  • 【Atlas】治理工作流(如标签审批)是否原生支持?
  • 天津河西实力私教哪里找?这3家口碑爆棚! - GrowthUME
  • FastAsyncWorldEdit 核心功能详解:200+命令与异步编辑的完美结合
  • Chisel-Tutorial实战指南:从问题到解决方案的完整流程
  • 为什么选择React Router Hash Link?解决React Router锚点滚动问题的最佳方案
  • 2026年7月最新宁波宇舶官方售后客户服务热线与维修网点地址汇总 - 亨得利官方服务中心
  • 如何用futures-await简化Rust异步代码:10个实用技巧
  • Chopper多环境配置终极指南:开发、测试和生产环境的无缝切换
  • WebAssembly线程提案未来展望:路线图与社区发展
  • 【Bug已解决】openclaw config schema mismatch / Unsupported config key — OpenClaw 配置模式不匹配解决方案
  • Koodo Reader:打造您的跨平台数字图书馆,一次阅读处处同步
  • 如何用LibMTL在30分钟内构建高效多任务学习模型?[特殊字符]
  • tufw源码探秘:用Go语言构建终端UI的实现原理
  • Hyperid安全吗?深入探讨唯一ID生成的安全性考量
  • 盐城施工劳务资质办理全攻略 申办要求及注意事项一次讲透彻 - GrowthUME
  • Hermes 教程 01:安装与快速入门
  • Introspy-iOS核心功能解析:监控iOS应用安全敏感API调用的实战技巧
  • Site Taxonomy
  • 2026 安徽宣城市全区域彩钢瓦修缮公司 TOP4 权威推荐|彩钢瓦翻新 / 防水补漏 / 除锈喷漆优选指南 + 避坑攻略 - 本地便民网
  • 为什么选择CCManager?10大理由让你的AI编码效率提升300%
  • 深度解析:Cloudflare Tunnel Ingress Controller 架构设计与实战部署
  • Seed-VC语音克隆与歌声转换完整实践终极指南
  • pz 与其他命令行工具对比:如何用 Python 简化日常 CLI 操作
  • 5分钟快速上手:使用Binance Connector Node构建专业加密货币交易系统
  • p5.js-svg vs Canvas2D:终极矢量绘图工具对比与选择指南 [特殊字符]
  • Swift开发者必学:ComplimentaryGradientView的单元测试与调试技巧