KMP算法:字符串匹配的智慧跳跃

文章目录

  • 起因:暴力法的致命缺陷
    • 暴力搜索的局限性
  • KMP核心思想:避免重复
    • 理解前缀表(PMT)
    • 不匹配时的回退机制
    • 代码:高效字符串匹配
    • 补充:next表和PMT表
  • 暴力法 vs KMP
  • 总结:KMP 是如何改变游戏规则的
  • 总结:KMP 是如何改变游戏规则的

起因:暴力法的致命缺陷

不知道你有没有曾经为编程中的慢速字符串搜索而烦恼吗?想象一下处理成千上万的字符,却发现你的解决方案运行时间过长。如果有一种方法可以极大地加快这个过程,会怎么样呢?

偶然一次刷题中也遇到了这么一个问题,看似一道很简单的题,背后却又大学问,题目的描述如下:

给你两个字符串 haystackneedle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1

leetcode链接挂这了,有兴趣的小伙伴可以去试试。[找出字符串中第一个匹配项的下标]

其实就是一个字符串匹配,刚读完题我就想到了一个方法,原思路是这样的:

1、以 needle 的第一个字符为基准,顺序遍历 haystack 字符串

2、如果第一个字符串相等,再以此开始,同时移动 needlehaystack 的下标

3、如果 needle 遍历完则表示可以匹配到,反之则表示没有匹配到需要继续遍历

4、没有匹配到则将 haystack 的下标回到上一次匹配的下一个,needle 则回到第一个

5、重复2、3、4,如果 haystack 遍历完都没有匹配到,则不存在

基于此思路,写下如下代码:

int strStr(string haystack, string needle) 
{int n = haystack.size(), m = needle.size();if (m == 0) return 0;if (n < m) return -1;for (int i = 0; i <= n - m; i++) // 优化循环终止条件{ if (haystack[i] != needle[0]) continue;int j;for (j = 0; j < m; j++) // 直接比较字符,无需创建临时字符串{ if (haystack[i + j] != needle[j]) break;}if (j == m) return i;}return -1;
}

暴力搜索的局限性

这种方法实现简单,但是性能却经不起推敲,用一组看似简单的测试案例演示:

// 主串(haystack): "AAAAAAAAB" (8个'A' + 1个'B',长度9)
// 模式串(needle): "AAAB"       (3个'A' + 1个'B',长度4)
int pos = strStr("AAAAAAAAB", "AAAB"); // 正确结果应为5

根据代码逻辑,实际匹配过程如下(👉 逐帧解析):

  1. i=0(主串起始位置)

    • 比较 haystack[0](A) == needle[0](A) → 成功

    • 逐字符检查

      • j=0: A vs A ✅
      • j=1: A vs A ✅
      • j=2: A vs A ✅
      • j=3: A vs B ❌
    • 总计比较4次 → 失败,i++

  2. i=1(主串第二个A)

    • 再次比较 haystack[1](A) == needle[0](A) → 成功

    • 逐字符检查

      • j=0: A vs A ✅
      • j=1: A vs A ✅
      • j=2: A vs A ✅
      • j=3: A vs B ❌
    • 总计比较4次 → 失败,i++
      (👉 问题浮现:主串的i=1~3位置已经被验证为A,却再次重复比较!)

  3. i=2、i=3、i=4、i=5

    • 每次i递增后,完全重复上述过程 → 每次比较4次,均失败
  4. i=6(主串第七个A)

    • 比较 haystack[6](A) == needle[0](A) → 成功

    • 逐字符检查

      • j=0: A vs A ✅
      • j=1: A vs A ✅
      • j=2: A vs A ✅
      • j=3: B vs B ✅
    • 总计比较4次 → 成功,返回i=5

用一张图片来演示,如下图:


最后的数字触目惊心,这就是暴力法的 “重复税”。

  • 总比较次数 = 4(i=0) + 4(i=1) + 4(i=2) + 4(i=3) + 4(i=4) + 4(i=5) + 4(i=5) = 28次
  • 实际有效比较:只需检查主串i=5~8位置的"AAB"是否匹配,理想情况仅需4次比较

🔥 核心问题:暴力法像陷入泥潭一样,每次失败后主串指针i仅前进1步,导致已确认匹配的字符被反复重验。当模式串有大量重复前缀时(如本例的"AAA"),这种冗余比较会被无限放大!

当我们再换一种思维实验:如果主串是10000个’A’加’B’?

假设 haystack = string(10000, 'A') + "B"needle = string(999, 'A') + "B",暴力法比较次数 ≈ (10000 - 1000) * 1000 = 9,000,000次


暴力方法看起来简单易懂,但效率极低。问题出现在我们找到不匹配时。我们不是跳过已经检查过的字符,而是反复回到它们那里进行检查,导致无数不必要的比较。怎么解决这个问题?这正是本文说要说的重点——KMP,今天,让我们深入了解 KMP(Knuth-Morris-Pratt),这是解决这个常见问题的优雅且高效的方法。

KMP核心思想:避免重复

想象你正在搜索一个巨大的文件,并且你已经在开头找到了一个模式匹配。使用暴力搜索,你会从非常开始的地方再次开始,反复检查相同的点。然而,KMP 就像一个聪明的助手,它会记住你已经看过的位置,并帮助你跳过。

KMP 通过避免我们在暴力方法中看到的冗余比较来解决此问题。关键思想是,当发生不匹配时,我们不是将 haystack 指针向前移动一个位置,而是利用我们已经收集到的关于匹配字符的信息,移动 needle 指针。

我们如何实现这一点?通过使用前缀表(PMT)

理解前缀表是理解KMP算法的关键,可以说这个前缀表就是KMP算法的核心,所以再次强调:前缀表记录的是每个位置的最长公共前后缀的长度!

理解前缀表(PMT)

前缀表,或部分匹配表,存储了 needle 的最长正确前缀同时也是后缀的长度,因此也叫最长公共前后缀。这有助于算法跳过已经匹配的部分 needle ,而不是从头开始。

让我们通过一个例子来分解它是如何工作的:

ABABAC示例:为 ABABAC 构建前缀表,以下是构建该字符串的前缀表(PMT)的方法:

步骤1:i=1(字符B)

  • 比较pattern[1](B)与pattern[j=0](A)
  • 不匹配j保持0,pmt[1]=0

步骤2:i=2(字符A)

  • 比较pattern[2](A)与pattern[j=0](A)
  • 匹配j++pmt[2]=j (j=1)

步骤3:i=3(字符B)

  • 比较pattern[3](B)与pattern[j=1](B)
  • 匹配j++pmt[3]=j (j=2)

步骤4:i=4(字符A)

  • 比较pattern[4](A)与pattern[j=2](A)
  • 匹配j++pmt[4]=j (j=3)

步骤5:i=5(字符C)

  • 比较pattern[5]©与pattern[j=3](B)
  • 不匹配j=pmt[j-1]=pmt[2]=1
  • 再次比较pattern[5]©与pattern[j=1](B) → 仍不匹配
  • 继续回退j=pmt[j-1]=pmt[0]=0
  • 最终pmt[5]=0

最终PMT[0, 0, 1, 2, 3, 0]

索引012345
字符ABABAC
PMT001230

在上面的动画中,你可以看到 KMP 如何通过利用之前匹配收集到的信息来避免不必要的检查。观察当发生不匹配时,模式指针如何跳到前面,从而加快过程。

不匹配时的回退机制

当模式串在位置j匹配失败时,利用PMT值跳转到pmt[j-1]继续匹配:

案例:主串ABABABAC vs 模式串ABABAC(PMT=[0,0,1,2,3,0]

 主串:A B A B A B A C  
模式串:A B A B A C  
匹配失败位置:j=5(字符C)

回退步骤

  1. pmt[j-1]=pmt[4]=3
  2. 模式串跳转到j=3(字符B)继续与主串i=5比较
  3. 跳过冗余比较A B A(已通过PMT确认匹配)

核心代码实现:

Cppvoid build_pmt(string pattern, vector<int>& pmt) 
{pmt[0] = 0;int j = 0;for (int i = 1; i < pattern.size(); i++) {// 关键回退:利用已计算的pmt值递归查找while (j > 0 && pattern[i] != pattern[j]) {j = pmt[j-1];}// 匹配成功则延长共同前后缀if (pattern[i] == pattern[j]) j++;pmt[i] = j;}
}

理解了回退机制,我们来看看如何用代码实现这一逻辑。

代码:高效字符串匹配

有了前缀表,KMP 算法可以智能地跳过之前匹配的部分。这里是 C++ 中的 KMP 实现:

void build_pmt(string pattern, vector<int>& pmt) 
{int j = 0;pmt[0] = 0;for (int i = 1; i < pattern.size(); i++) {while (j > 0 && pattern[i] != pattern[j]) {j = pmt[j - 1];}if (pattern[i] == pattern[j]) j++;pmt[i] = j;}
}int strStr(string haystack, string needle) 
{if (needle.empty()) return 0;if (needle.size() > haystack.size()) return -1;vector<int> pmt(needle.size(), 0);build_pmt(needle, pmt);int j = 0;for (int i = 0; i < haystack.size(); i++) {while (j > 0 && haystack[i] != needle[j]) {j = pmt[j - 1];}if (haystack[i] == needle[j]) j++;if (j == needle.size()) {return i - needle.size() + 1;}}return -1;
}

补充:next表和PMT表

可能有些人之前看到的代码很多人写的是next数组并不是pmt,并且很多都是将第一个初始化为-1。这时候可能有人会疑惑,next和pmt有关系吗,有什么区别?

其实这并不涉及到KMP的原理,而只是工程代码的具体实现,将第一位初始化为-1其实就是前缀表的统一右移一位后,第一位补-1。

  • PMT(部分匹配表)
    • 定义:记录模式串每个前缀子串的最长公共前后缀长度(不包含自身)。
    • 示例:模式串ABABAC的PMT为[0,0,1,2,3,0],表示各位置的最长公共前后缀长度
    • 核心作用:通过已匹配的信息,避免主串指针回溯。
  • next数组
    • 定义:由PMT右移一位首位补-1得到,用于直接指示失配时模式串指针的跳转位置。
    • 示例:PMT[0,0,1,2,3,0]右移后得到next数组[-1,0,0,1,2,3]
    • 核心作用:简化代码逻辑,避免手动计算偏移量。

在右移之后,就不需要在进行类似于 j = ptm[j - 1],而 next[j] = ptm[j - 1],因此就有 j = next[j]。硬要说区别的话就是两者所表达的意义变了:

  • PMT:回答“当前已匹配的子串中,前后缀有多少字符是重复的?”
  • next数组:回答“失配时,模式串指针应跳转到哪个位置继续匹配?”

总的来说两者:

  • 本质相同:PMT和next数组的核心数据一致,均基于最长公共前后缀的复用思想。
  • 工程优化:next数组通过右移和补-1操作,简化了代码实现中的指针跳转逻辑,是PMT的工程化变体。

下面是用next表实现的代码:

#include <vector>
using namespace std;void build_next(string pattern, vector<int>& next) 
{int n = pattern.size();next.resize(n);next[0] = -1;  // 传统 next 数组第一个位置为 -1int j = -1;    // 为了配合 next[0] = -1,j 初始化为 -1for (int i = 0; i < n; ) {if (j == -1 || pattern[i] == pattern[j]) {i++;j++;next[i] = j;  // next[i] 对应 pmt[i-1]} else {j = next[j];  // 利用已计算的 next 回溯}}
}int strStr(string haystack, string needle) 
{if (needle.empty()) return 0;if (needle.size() > haystack.size()) return -1;vector<int> next;build_next(needle, next);int i = 0, j = 0;int n = haystack.size(), m = needle.size();while (i < n && j < m) {if (j == -1 || haystack[i] == needle[j]) {i++;j++;} else {j = next[j];  // 直接根据 next 跳转}}return (j == m) ? i - m : -1;
}

暴力法 vs KMP

让我们重新审视我们之前的例子,其中包含 haystack = "AAAAAAAAB"needle = "AAAB"

  • 暴力破解:28 次比较
  • KMP:仅需 9 次比较(多亏了前缀表)

当处理大量字符串时,性能差异变得更加明显。KMP 通过利用它在匹配过程中收集的信息,有效地减少了不必要的比较次数。以下是暴力法和KMP的性能对比,随着字符长度的增加,性能差异越来越大。

总结:KMP 是如何改变游戏规则的

KMP 通过减少冗余检查的数量,革新了我们对字符串匹配的方法。借助前缀表,KMP 可以智能地跳过已匹配的部分,使其比暴力方法显著更高效。下次你需要搜索子字符串时,记得 KMP 算法。这不仅仅是一种避免不必要工作的聪明方法——它还是向编写更简洁、更高效的代码迈出的一大步。

尝试在不同字符串搜索问题中实现 KMP 算法,并使用更大的数据集进行实验。你将看到 KMP 算法的实时好处,尤其是在处理文本处理或生物信息学等应用中的大量字符串时。
得更加明显。KMP 通过利用它在匹配过程中收集的信息,有效地减少了不必要的比较次数。以下是暴力法和KMP的性能对比,随着字符长度的增加,性能差异越来越大。

[外链图片转存中…(img-czSr32l5-1740039479058)]

总结:KMP 是如何改变游戏规则的

KMP 通过减少冗余检查的数量,革新了我们对字符串匹配的方法。借助前缀表,KMP 可以智能地跳过已匹配的部分,使其比暴力方法显著更高效。下次你需要搜索子字符串时,记得 KMP 算法。这不仅仅是一种避免不必要工作的聪明方法——它还是向编写更简洁、更高效的代码迈出的一大步。

尝试在不同字符串搜索问题中实现 KMP 算法,并使用更大的数据集进行实验。你将看到 KMP 算法的实时好处,尤其是在处理文本处理或生物信息学等应用中的大量字符串时。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/70256.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

上位机知识篇---setuptools

文章目录 前言简介一、核心功能1.依赖管理自动安装依赖版本约束额外依赖组命令行工具插件系统 2.开发模式安装3.资源文件管理4.Egg 分发&#xff08;已逐渐被 Wheel 取代&#xff09;5.命名空间包 二、基础用法1. 项目结构示例2. 配置文件 setup.cfg3. setup.py 最小化示例&…

蓝桥杯学习大纲

&#xff08;致酷德与热爱算法、编程的小伙伴们&#xff09; 在查阅了相当多的资料后&#xff0c;发现没有那篇博客、文章很符合我们备战蓝桥杯的学习路径。所以&#xff0c;干脆自己整理一篇&#xff0c;欢迎大家补充&#xff01; 一、蓝桥必备高频考点 我们以此为重点学习…

Go 错误处理与调试:面向对象的入门教程

Go 错误处理与调试&#xff1a;面向对象的入门教程 Go 语言因其简洁、高效和易于并发编程的特性&#xff0c;逐渐成为后端开发的主流语言之一。错误处理是任何编程语言中非常重要的一部分&#xff0c;尤其是在 Go 语言中&#xff0c;Go 提供了一种不同于传统异常处理机制的错误…

Linux探秘坊-------4.进度条小程序

1.缓冲区 #include <stdio.h> int main() {printf("hello bite!");sleep(2);return 0; }执行此代码后&#xff0c;会 先停顿两秒&#xff0c;再打印出hello bite&#xff0c;但是明明打印在sleep前面&#xff0c;为什么会后打印呢&#xff1f; 因为&#xff…

基于Python的Diango旅游数据分析推荐系统设计与实现+毕业论文(15000字)

基于Python的Diango旅游数据分析推荐系系统设计与实现毕业论文指导搭建视频&#xff0c;带爬虫 配套论文1w5字 可定制到某个省份&#xff0c;加40 基于用户的协同过滤算法 有后台管理 2w多数据集 可配套指导搭建视频&#xff0c;加20 旅游数据分析推荐系统采用了Python语…

Scrapy:DownloaderAwarePriorityQueue队列设计详解

DownloaderAwarePriorityQueue 学习笔记 1. 简介 DownloaderAwarePriorityQueue 是 Scrapy 中一个高级的优先级队列实现&#xff0c;它不仅考虑请求的优先级&#xff0c;还会考虑下载器的负载情况。这个队列为每个域名&#xff08;slot&#xff09;维护独立的优先级队列&#…

dify-AI 私有部署可修改前端页面

dify文档 官方文档&#xff1a;欢迎使用 Dify | Dify 源码&#xff1a;https://github.com/langgenius/dify.git 安装docker 官网&#xff1a;https://www.docker.com/ 部署服务到docker cd dify cd docker cp .env.example .env docker compose up -d查看效果 http://localh…

PHP基础部分

但凡是和输入、写入相关的一定要预防别人植入恶意代码! HTML部分 语句格式 <br> <hr> 分割符 <p>插入一行 按住shift 输入! 然后按回车可快速输入html代码(VsCode需要先安装live server插件) html:<h1>标题 数字越大越往后</h1> <p…

【Elasticsearch】Retrieve inner hits获取嵌套查询的具体的嵌套文档来源,以及父子文档的来源

Retrieve inner hits 是 Elasticsearch 中的一个功能&#xff0c;用于在嵌套查询或父子查询中&#xff0c;返回导致主文档匹配的具体嵌套对象或子/父文档的详细信息&#xff0c;帮助用户更直观地理解查询结果的来源。 在 Elasticsearch 中&#xff0c;Retrieve inner hits是一…

SpringCloud面试题----eureka和zookeeper都可以提供服务注册与发现的功能,请说说两个的区别

dEureka 和 Zookeeper 都可以提供服务注册与发现的功能,它们的区别主要体现在以下几个方面: 设计理念 Eureka:是基于 RESTful 风格设计的,强调简单、轻量级,旨在为微服务架构提供一种易于使用的服务发现解决方案,注重服务的可用性和灵活性。Zookeeper:最初是为分布式协…

数据库提权总结

Mysql提权 UDF提权是利用MYSQL的自定义函数功能&#xff0c;将MYSQL账号转化为系统system权限 前提&#xff1a; 1.UDF提权条件 &#xff08;1&#xff09;Mysql版本大于5.1版本udf.dll文件必须放置于MYSQL安装目录下的lib\plugin文件夹下。 &#xff08;2&#xff09;Mysql…

“深入浅出”系列之QT:(10)Qt接入Deepseek

项目配置&#xff1a; 在.pro文件中添加网络模块&#xff1a; QT core network API配置&#xff1a; 将apiUrl替换为实际的DeepSeek API端点 将apiKey替换为你的有效API密钥 根据API文档调整请求参数&#xff08;模型名称、温度值等&#xff09; 功能说明&#xff1a; 使…

【Linux探索学习】第二十七弹——信号(上):Linux 信号基础详解

Linux学习笔记&#xff1a; https://blog.csdn.net/2301_80220607/category_12805278.html?spm1001.2014.3001.5482 前言&#xff1a; 前面我们已经将进程通信部分讲完了&#xff0c;现在我们来讲一个进程部分也非常重要的知识点——信号&#xff0c;信号也是进程间通信的一…

nginx负载均衡, 解决iphash不均衡的问题之consistent

原因分析 客户端IP分布不均&#xff1a;部分IP段请求集中&#xff0c;导致哈希到同一后端。 服务器数量变动&#xff1a;增删节点时&#xff0c;传统ip_hash未使用一致性哈希&#xff0c;导致分布重置。 哈希键范围过小&#xff1a;例如仅使用IPv4前24位&#xff0c;不同IP可…

[C++]多态详解

目录 一、多态的概念 二、静态的多态 三、动态的多态 3.1多态的定义 3.2虚函数 四、虚函数的重写&#xff08;覆盖&#xff09; 4.1虚函数 4.2三同 4.3两种特殊情况 &#xff08;1&#xff09;协变 &#xff08;2&#xff09;析构函数的重写 五、C11中的final和over…

WEB安全--SQL注入--PDO与绕过

一、PDO介绍&#xff1a; 1.1、原理&#xff1a; PDO支持使用预处理语句&#xff08;Prepared Statements&#xff09;&#xff0c;这可以有效防止SQL注入攻击。预处理语句将SQL语句与数据分开处理&#xff0c;使得用户输入的数据始终作为参数传递给数据库&#xff0c;而不会直…

ES12 weakRefs的用法和使用场景

ES12 (ECMAScript 2021) 特性总结&#xff1a;WeakRef 1. WeakRef 概述 描述 WeakRef 是 ES12 引入的一个新特性&#xff0c;用于创建对对象的弱引用。弱引用不会阻止垃圾回收器回收对象&#xff0c;即使该对象仍然被弱引用持有。WeakRef 通常与 FinalizationRegistry 结合使…

50页精品PPT | 某大数据资产平台建设项目启动会材料

该PPT主要介绍了某集团大数据资产平台建设项目的启动会材料&#xff0c;围绕数据作为数字经济时代核心生产要素的背景&#xff0c;结合国家战略和集团数字化转型需求&#xff0c;分析了当前数据资源整合不足、孤岛现象严重、质量管控薄弱及共享机制不完善等问题&#xff0c;提出…

8.【线性代数】——求解Ax=b

八 求解Axb 1. 解Axb求特解 x p x_p xp​求特解 x n x_n xn​所有解 2. Axb什么时候有解3. A m ∗ n A_{m * n} Am∗n​不同秩的Axb解分析3.1 列满秩 rn<m3.2 行满秩 rm<n3.3 rmn3.4 r<m 且 r < n3.5 综述 1. 解Axb 求解 { x 1 2 x 2 2 x 3 2 x 4 b 1 2 x 1…

动静态链接与加载

目录 静态链接 ELF加载与进程地址空间&#xff08;静态链接&#xff09; 动态链接与动态库加载 GOT表 静态链接 对于多个.o文件在没有链接之前互相是不知到对方存在的&#xff0c;也就是说这个.o文件中调用函数的的跳转地址都会被设定为0&#xff08;当然这个函数是在其他.…