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

kmp算法:我们所忽略的字符串匹配本质

一、先捅破窗户纸前后缀在匹配里到底起什么作用在讲next数组的计算之前我们必须先把“为什么有前后缀就能不回退主串”这个核心逻辑彻底讲透这是字符串匹配的本质核心。我们用一个有前后缀的经典案例把抽象逻辑落地主串Sabababaca模式串Pababaca我们先看匹配的关键节点我们从S[0]和P[0]开始比对一路匹配到S[4]a 和 P[4]b全部匹配成功此时已匹配的区间是S[0-4] ↔ P[0-4]也就是子串ababa接下来比对S[5]b 和 P[5]c匹配失败这时候暴力匹配会怎么做主串指针直接回退到S[1]模式串指针回退到P[0]从头再来。但你一眼就能发现这里的无效劳动已经匹配成功的ababa里有大量可以复用的信息。我们看ababa这个已匹配的子串它的最长相等前缀是aba最长相等后缀也是aba。这意味着什么意味着主串里已经匹配成功的后缀aba和模式串开头的前缀aba是完全一模一样的既然已经确认了它们相等我们根本不用再重新比对这3个字符只需要让模式串的前缀aba直接对齐主串里已经匹配好的后缀aba然后从模式串前缀的下一位也就是P[3]继续和主串当前的S[5]比对即可。这就是我前面说的“匹配过的字符重新当头”——那些已经匹配成功的后缀字符刚好和模式串的前缀完全一致它们可以直接成为新的匹配起点我们完全不需要让主串指针往回走去重复验证这些已经确定相等的内容。到这里KMP的核心本质已经呼之欲出KMP算法本质是通过预处理模式串为每一个位置提前计算好当这个位置匹配失败时模式串指针可以跳到哪个位置既能最大化复用已经匹配成功的字符又能保证不会错过正确的匹配位置最终实现主串指针全程不回退每个字符只被比对一次。二、next数组给它一个“人话”定义而非教科书式的干巴巴规则我们先约定模式串P的下标从0开始长度为m。next数组是一个和模式串长度相同的数组next[i]的核心含义是当模式串的P[i]位置和主串匹配失败时模式串指针应该回退到next[i]的位置继续和主串当前指针比对无需重新比对前面的字符。而这个next[i]的值刚好等于模式串中P[0]到P[i-1]这个子串的最长相等前后缀的长度。这里一定要注意是P[0]到P[i-1]不是P[0]到P[i]这是90%的人初学都会踩的坑也是硬记公式永远记不住的根源。为什么是i-1因为当P[i]匹配失败时只有P[0]到P[i-1]是已经和主串匹配成功的部分我们要复用的只能是这部分里的相等前后缀。我们用上面的模式串Pababaca手算一遍完整的next数组你会发现它和我们前面的匹配场景完全对应模式串下标i0123456P[i]字符ababacanext[i]-1001230手算逻辑拆解next[0]模式串第0位就匹配失败说明P[0]和主串当前字符完全不匹配我们只能让主串指针后移一位模式串指针还是停在0。所以我们约定next[0] -1这是一个哨兵位专门用来标记“主串指针需要后移”后面你会看到它的妙用。next[1]看P[0]到P[0]的子串a单个字符没有前后缀最长相等前后缀长度为0所以next[1]0。next[2]看P[0]到P[1]的子串ab前缀a和后缀b不相等长度为0next[2]0。next[3]看P[0]到P[2]的子串aba最长相等前缀a和后缀a长度为1next[3]1。next[4]看P[0]到P[3]的子串abab最长相等前缀ab和后缀ab长度为2next[4]2。next[5]看P[0]到P[4]的子串ababa最长相等前缀aba和后缀aba长度为3next[5]3。next[6]看P[0]到P[5]的子串ababac无相等前后缀长度为0next[6]0。你看我们前面匹配失败的位置是P[5]next[5]3刚好就是我们说的“最优起跳点”完美对应。三、next数组的推导本质是模式串自己和自己做KMP匹配很多人觉得next数组的代码晦涩难懂其实是因为没看懂一个核心真相next数组的推导过程本身就是一次KMP匹配——模式串自己既是主串也是模式串我们要做的就是用模式串的后缀去匹配模式串的前缀找到每一个位置的最长匹配长度。它的推导逻辑和后面主串与模式串的匹配逻辑完全一致从头到尾贯彻同一个核心思想主指针永不回退只调整副指针最大化复用已匹配的信息。这里我们用双指针法推导全程不用递归不用复杂公式3条核心规则记一辈子都不会忘定义两个指针前缀指针j初始值为-1指向当前已经匹配成功的前缀的末尾后缀指针i初始值为0指向当前正在遍历的后缀的末尾核心规则规则1当j -1或者P[i] P[j]时i和j都向后移动一位然后令next[i] j含义j-1说明匹配从头开始或当前两个字符相等我们找到了更长的相等前后缀直接记录下来规则2当P[i] ! P[j]时令j next[j]含义当前字符不匹配让j回退到next[j]的位置复用已经匹配的前缀继续比对和KMP匹配逻辑完全一致规则3循环执行直到i遍历完整个模式串。我们还是用Pababaca完整走一遍推导过程你会发现结果和手算完全一致初始状态i0j-1next[0]-1第1轮j-1符合规则1 → i1j0next[1]0第2轮i1j0P[1]b≠P[0]a符合规则2 → jnext[0]-1此时j-1符合规则1 → i2j0next[2]0第3轮i2j0P[2]aP[0]a符合规则1 → i3j1next[3]1第4轮i3j1P[3]bP[1]b符合规则1 → i4j2next[4]2第5轮i4j2P[4]aP[2]a符合规则1 → i5j3next[5]3第6轮i5j3P[5]c≠P[3]b符合规则2 → jnext[3]1仍不匹配jnext[1]0仍不匹配jnext[0]-1此时j-1符合规则1 → i6j0next[6]0这就是KMP最精妙的设计之一它用完全自洽的一套逻辑既完成了next数组的预处理又完成了主串和模式串的匹配没有任何多余的设计算法的美感体现得淋漓尽致。【代码实现】求next数组#include iostream #include vector #include string using namespace std; // 功能计算模式串P的next数组 // 参数P - 模式串 // 返回next数组vectorint类型长度等于P.size() vectorint getNext(const string P) { int m P.size(); vectorint next(m, 0); int j -1; // 前缀指针初始为-1哨兵位 int i 0; // 后缀指针初始为0 next[0] -1; // 初始化next[0]为-1 while (i m - 1) { // 规则1j-1 或 P[i]P[j]双指针后移记录next[i] if (j -1 || P[i] P[j]) { i; j; next[i] j; } // 规则2字符不匹配j回退到next[j] else { j next[j]; } } return next; }四、完整的KMP匹配流程把所有逻辑串起来现在我们有了next数组就可以完整走一遍KMP的匹配流程彻底看懂它是怎么做到O(nm)的线性时间复杂度的。还是用之前的案例主串Sa b a b a b a c a下标0-8长度9模式串Pa b a b a c a下标0-6长度7next数组[-1, 0, 0, 1, 2, 3, 0]匹配的核心规则和next数组的推导逻辑完全一致同样只有3条定义主串指针i0模式串指针j0当j -1或者S[i] P[j]时i和j都向后移动一位当S[i] ! P[j]时令j next[j]终止条件如果j等于模式串的长度m说明匹配成功返回起始位置i-j如果i遍历完主串j还没到m说明匹配失败返回-1我们一步步走完整个匹配过程初始i0j0S[0]a P[0]a → i1j1S[1]b P[1]b → i2j2S[2]a P[2]a → i3j3S[3]b P[3]b → i4j4S[4]a P[4]a → i5j5S[5]b ≠ P[5]c → 匹配失败jnext[5]3此时j3S[5]b P[3]b → i6j4S[6]a P[4]a → i7j5S[7]c P[5]c → i8j6S[8]a P[6]a → i9j7此时j7等于模式串的长度7匹配成功返回起始位置i-j9-72也就是主串中从下标2开始就是模式串的完整位置完全正确。你会发现整个过程中主串指针i从0走到9全程没有往回退过一步主串的每一个字符只被比对了一次这就是KMP比暴力匹配快的核心原因。【代码实现】KMP核心匹配函数// 功能KMP算法主匹配函数 // 参数S - 主串P - 模式串 // 返回模式串在主串中第一次出现的起始下标未找到返回-1 int kmpSearch(const string S, const string P) { int n S.size(); int m P.size(); if (m 0) return 0; // 空模式串默认匹配成功 if (n m) return -1; // 主串比模式串短直接失败 // 1. 预处理获取next数组 vectorint next getNext(P); int i 0; // 主串指针永不回退 int j 0; // 模式串指针 while (i n j m) { // 规则1j-1哨兵位主串后移或字符匹配双指针后移 if (j -1 || S[i] P[j]) { i; j; } // 规则2字符不匹配模式串指针回退到next[j] else { j next[j]; } } // 终止条件判断j走完模式串说明匹配成功 if (j m) { return i - j; // 返回起始下标 } return -1; // 匹配失败 }五、补充被很多人忽略的next数组优化nextval数组最后我们补充一个绝大多数教程会提到但很少讲透本质的优化nextval数组。我们先看一个经典的反例模式串Paaaaab它的next数组是[-1,0,1,2,3,4]。如果主串Saaabaaaaab当匹配到S[3]b和P[3]a时匹配失败jnext[3]2此时S[3]b和P[2]a还是不匹配jnext[2]1还是不匹配jnext[1]0还是不匹配jnext[0]-1。你看这里做了4次完全无效的回退——因为P[3]、P[2]、P[1]、P[0]都是a和P[3]的值完全一样既然P[3]和主串不匹配那前面的a肯定也不匹配这些回退完全是多余的。所以nextval数组的优化本质就是消除这些无效的回退当我们计算next[i]时如果P[i] P[next[i]]那么我们就把next[i]更新为next[next[i]]直接跳过上一个相同的字符避免无效比对。还是用Paaaaab计算它的nextval数组模式串下标i012345P[i]字符aaaaabnext[i]-101234nextval[i]-1-1-1-1-14优化后刚才的匹配失败场景j会直接从3跳到-1一步到位完全消除了无效回退。【代码实现】优化版nextval数组// 功能计算优化后的nextval数组 // 参数P - 模式串 // 返回nextval数组 vectorint getNextval(const string P) { int m P.size(); vectorint nextval(m, 0); int j -1; int i 0; nextval[0] -1; while (i m - 1) { if (j -1 || P[i] P[j]) { i; j; // 【核心优化】如果当前字符和回退位置的字符相同继续回退 if (P[i] P[j]) { nextval[i] nextval[j]; } else { nextval[i] j; } } else { j nextval[j]; } } return nextval; } // 【优化版匹配函数】使用nextval数组 int kmpSearchOptimized(const string S, const string P) { int n S.size(); int m P.size(); if (m 0) return 0; if (n m) return -1; // 使用优化后的nextval数组 vectorint nextval getNextval(P); int i 0; int j 0; while (i n j m) { if (j -1 || S[i] P[j]) { i; j; } else { j nextval[j]; } } if (j m) return i - j; return -1; }【完整可运行示例】把所有代码拼在一起用我们的经典案例测试一下int main() { string S abababaca; // 主串 string P ababaca; // 模式串 // 1. 测试基础版KMP int pos kmpSearch(S, P); cout 【基础版KMP】模式串在主串中的起始下标: pos endl; // 预期输出2 // 2. 测试优化版KMP int posOpt kmpSearchOptimized(S, P); cout 【优化版KMP】模式串在主串中的起始下标: posOpt endl; // 预期输出2 // 3. 打印next数组和nextval数组对照手算结果 vectorint next getNext(P); vectorint nextval getNextval(P); cout \n模式串: P endl; cout next数组: ; for (int val : next) cout val ; cout \nnextval数组: ; for (int val : nextval) cout val ; cout endl; return 0;

相关文章:

kmp算法:我们所忽略的字符串匹配本质

一、先捅破窗户纸:前后缀在匹配里到底起什么作用?在讲next数组的计算之前,我们必须先把“为什么有前后缀,就能不回退主串”这个核心逻辑彻底讲透,这是字符串匹配的本质核心。我们用一个有前后缀的经典案例,…...

在树莓派上运行本地 LLM 和 VLM

原文:towardsdatascience.com/running-local-llms-and-vlms-on-the-raspberry-pi-57bd0059c41a?sourcecollection_archive---------0-----------------------#2024-01-14 在树莓派上使用 Ollama 本地运行 Phi-2、Mistral 和 LLaVA 等模型 https://medium.com/pyes…...

利用计算机视觉进行跑步效率分析:与埃利乌德·基普乔格的比较分析

原文:towardsdatascience.com/running-efficiency-with-computer-vision-a-comparative-analysis-with-eliud-kipchoge-736eb80c574f 如何利用计算机视觉提高跑步效率? https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/…...

无代码玩法:OpenClaw网页控制台配合Qwen3.5-9B处理电商截图

无代码玩法:OpenClaw网页控制台配合Qwen3.5-9B处理电商截图 1. 为什么选择OpenClaw处理电商截图 作为一个经常网购的技术爱好者,我发现自己经常需要手动整理不同平台的商品价格信息。传统的做法是截图后人工录入Excel,既耗时又容易出错。直…...

UPnP_Generic库:嵌入式设备自动端口映射实战指南

1. UPnP_Generic库深度技术解析:嵌入式设备自动端口映射的工程实践1.1 为什么嵌入式开发者需要UPnP_Generic库在嵌入式物联网项目中,当设备需要从公网访问本地Web服务(如传感器数据页面、远程控制接口或OTA升级服务器)时&#xff…...

OpenClaw会议纪要助手:Qwen3-14b_int4_awq转写与重点提炼

OpenClaw会议纪要助手:Qwen3-14b_int4_awq转写与重点提炼 1. 为什么需要自动化会议纪要 作为远程工作者,我每周要参加至少15场跨时区会议。过去手动整理纪要时经常遇到三个痛点:一是录音转文字耗时(1小时会议需要2小时整理&…...

OpenClaw长期运行优化:Qwen3.5-9B-AWQ-4bit内存泄漏排查

OpenClaw长期运行优化:Qwen3.5-9B-AWQ-4bit内存泄漏排查 1. 问题背景与现象描述 上周我的OpenClaw网关服务在连续运行72小时后突然崩溃,导致自动化任务全部中断。查看系统监控发现内存占用从初始的2GB逐渐增长到16GB(我的服务器总内存&…...

ssh进阶用法

ssh登录与ssh配置文件 使用ssh可以从一台设备登录到另一台已开启sshd服务的远程设备。 Ubuntu-22.04 coliDESKTOP-J45M1NUM:~$ ssh yukari172.28.24.152 The authenticity of host 172.28.24.152 (172.28.24.152) cant be established. ECDSA key fingerprint is SHA256:YSC…...

基于WebAssembly的Harness扩展机制

基于WebAssembly的Harness扩展机制:构建灵活、安全且高性能的CI/CD生态系统 一、引言 钩子 (The Hook) 想象一下这个场景:您正在使用Harness构建您的CI/CD流水线,但您需要一个特定的功能——也许是一个专有的代码扫描工具,或者是与您内部系统集成的自定义步骤。传统上,…...

AI Agent Harness Engineering 的记忆架构:短期、长期与情景记忆的工程实现

AI Agent Harness Engineering 的记忆架构:短期、长期与情景记忆的工程实现 副标题:构建具有类人记忆能力的智能代理系统完整指南 第一部分:引言与基础 (Introduction & Foundation) 1. 引人注目的标题 (Compelling Title) “AI Agent Harness Engineering 的记忆架构…...

多核通信中的环形缓冲区设计与实现

1. 核间通信与环形缓冲区基础在现代多核处理器系统中,核间通信(IPC)是实现并行计算和任务协同的关键技术。共享内存是最常用的核间通信方式之一,它允许多个处理器核心通过访问同一块物理内存区域来交换数据。这种方式的优势在于避免了数据拷贝&#xff0…...

TLT库:面向Arduino的Telit ME310G1蜂窝通信轻量级C++ SDK

1. 项目概述TLT(Telit Library for Arduino)是一个面向嵌入式蜂窝通信的轻量级C库,专为CodeZoo ME310G1 Telit模块在Arduino平台上的集成而设计。该库并非从零构建,而是基于Arduino官方MKRNB库(arduino-libraries/MKRN…...

M5Unit-DigiClock模块:基于I²C的即插即用数字时钟解决方案

1. 项目概述 M5Unit-DigiClock(SKU: U146)是 M5Stack 推出的一款紧凑型数字时钟单元模块,专为 M5Stack Core 系列主控(如 Core2、CoreS3、Atom Echo)及兼容 ESP32 系列 MCU 的开发板设计。该模块并非通用 RTC 芯片的简…...

企业SEO优化与网站内容建设的关系是什么

企业SEO优化与网站内容建设的关系是什么 在现代数字营销中,企业SEO优化与网站内容建设是两个密不可分的重要环节。SEO优化(Search Engine Optimization)旨在提升网站在搜索引擎中的排名,而网站内容建设则是展示和传递企业信息的基…...

主流开源协议解析与选择指南

1. 开源协议:程序员必须掌握的法律常识第一次在GitHub上创建仓库时,面对那一长串开源协议选项,我和大多数新手一样直接懵了。MIT、Apache、GPL...这些看似简单的缩写背后,实则隐藏着影响深远的法律约束。作为从业十年的开发者&…...

OpenClaw多模型切换指南:Qwen3-4B与本地LLM混合调用

OpenClaw多模型切换指南:Qwen3-4B与本地LLM混合调用 1. 为什么需要多模型混合调用 去年冬天,当我第一次尝试用OpenClaw自动化处理技术文档时,发现一个尴尬的现象:用Qwen3-4B生成代码示例效果很好,但让它润色一段产品…...

Linux 的 link 命令

Linux 中的 link 命令用于创建硬链接(hard link),这是 Linux/Unix 文件系统中的一种特殊文件连接方式。与符号链接(symbolic link)不同,硬链接直接指向文件的 inode,而不是通过路径名引用。 命…...

Linux 的 df 命令

df (disk free) 命令是 Linux 系统中用于显示文件系统磁盘空间使用情况的常用工具。它可以报告文件系统的总容量、已用空间、可用空间以及挂载点等信息。 基本语法 df [选项] [文件或目录]常用选项 -h 或 --human-readable 以易读格式显示大小(KB, MB, GB&#xf…...

OpenClaw开源贡献:为Qwen3-4B开发新技能并提交社区

OpenClaw开源贡献:为Qwen3-4B开发新技能并提交社区 1. 为什么我们需要更多社区贡献的技能 去年冬天,当我第一次尝试用OpenClaw自动化处理每周的Markdown文档整理时,发现现有的技能库缺少一个能批量处理Front Matter的工具。这个痛点让我意识…...

RTOS在嵌入式开发中的核心价值与实战应用

1. RTOS在嵌入式开发中的核心价值我第一次接触RTOS是在2015年开发工业控制器时遇到的困境。当时用裸机编程实现多任务调度,代码已经膨胀到难以维护的程度。一个简单的功能修改需要通读上万行代码,调试一个BUG经常引发连锁反应。直到引入RTOS后&#xff0…...

OpenClaw多任务测试:Qwen3-32B在RTX4090D上的并行处理极限

OpenClaw多任务测试:Qwen3-32B在RTX4090D上的并行处理极限 1. 测试背景与动机 最近在折腾本地AI自动化时,遇到一个实际问题:当OpenClaw同时处理多个任务时,显存会成为瓶颈吗?我手头正好有台配备RTX4090D(…...

第23章 2014真题作文

目录 题目2014.11-论软件需求管理 题目2014.11-论非功能性需求对企业应用架构设计的影响 题目2014.11-论软件的可靠性设计 题目2014.11-论网络安全体系设计 题目2014.11-论软件需求管理 软件需求管理是一个对系统需求变更了解和控制的过程。需求管理过程与需求开发过程相互…...

第22章 2013真题作文

目录 题目2013.11-论软件架构建模技术与应用 题目2013.11-企业应用系统的分层架构风格 题目2013.11-论软件可靠性设计技术的应用 题目2013.11-分布式存储系统架构设计 题目2013.11-论软件架构建模技术与应用 软件架构用来处理软件高层次结构的设计和实施,它以精…...

如何利用地理位置信息优化网站的本地SEO效果

如何利用地理位置信息优化网站的本地SEO效果 在当今数字化时代,网站的本地SEO(搜索引擎优化)效果直接影响着网站的流量和用户转化率。利用地理位置信息进行本地SEO优化,不仅能够提升网站在本地用户中的可见性,还能有效…...

【复现】基于Lyapunov非线性控制-模型预测控制(LMPC)与反步法+自主水下航行器(AUV)的轨迹跟踪控制研究(Matlab代码实现)

💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...

Linux内核模块加载机制深度解析

1. Linux内核模块加载机制深度解析在Linux系统开发中,内核模块的动态加载机制为开发者提供了极大的灵活性。作为一名长期从事内核开发的工程师,我经常需要深入理解模块加载的完整流程,这对调试复杂驱动问题和性能优化至关重要。本文将以linux…...

MacOS极简部署OpenClaw:Phi-3-mini-128k-instruct镜像快速体验

MacOS极简部署OpenClaw:Phi-3-mini-128k-instruct镜像快速体验 1. 为什么选择这个组合? 上周我在测试各种开源模型时,偶然发现了Phi-3-mini-128k-instruct这个轻量级模型。它的响应速度和对指令的理解能力让我印象深刻,特别是12…...

Arduino控制乐歌/升谱电动升降桌的UART物联网方案

1. 项目概述LoctekMotion_IoT_arduino 是一个面向 Loctek Motion(国内常称“乐歌”)与 FlexiSpot(国内常称“升谱”)品牌电动升降桌的开源 Arduino 控制库,核心目标是将传统电动升降桌改造为具备物联网能力的智能办公终…...

PicoBricks-for-ESP32库详解:面向教育的ESP32硬件抽象封装

1. 项目概述PicoBricks-for-ESP32 是 Robotistan 官方发布的 Arduino 兼容库,专为 ESP32 微控制器平台设计,用于驱动 PicoBricks 教育开发板。该库并非通用硬件抽象层,而是面向特定硬件拓扑的垂直集成方案——其核心价值在于将 PicoBricks 板…...

STC51单片机串口ISP下载程序全攻略

1. STC51单片机ISP串口下载程序详解作为一名嵌入式开发工程师,我经常需要给各种单片机下载程序。STC51系列单片机因其性价比高、开发简单而广受欢迎。今天我就来详细讲解STC51单片机通过串口ISP下载程序的全过程,包括硬件连接、软件配置和常见问题处理。…...