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

【算法深入浅出】字符串匹配之 KMP 算法

KMP 算法是一种字符串匹配算法。字符串匹配算法的目标是:在字符串 s 中找到与模式串 p 相等的子串,输出其位置。例如:s = “abcdef”,p = “cdef”,p 在 s 中的位置是 2(从 0 开始计数)。

在这里插入图片描述
容易想到的方式就是暴力算法。

    int findString(string s, string p) {int n = s.size(), m = p.size();int i, j;for (i = 0; i < n - m + 1; i++) {for (j = 0; j < m; j++) {if (s[i] == s[j]) continue;break;}if (j == m) return i;}return -1;}

暴力算法的最坏情况下,时间复杂度是 O ( n m ) O(nm) O(nm)。而本文尝试要讲明白的 kmp 算法,他的摊还时间复杂度是 O ( n ) O(n) O(n) 的。
kmp 算法执行速度要快不少,但是也是有代价的:也就是出了名的难理解。但我认为,只要 提纲挈领的找到 kmp 算法的主干,那还是比较好理解的,也就 2 分钟的事吧。算法实现上会有一些坑,这才是最难的地方,需要将里面的思路捋顺,但花些时间总能搞出来。那我们就开始吧。

从直觉开始

假设有这样一个样例:s = “bacbababababcbab”,p = “ababaca”。算法运行到如下位置时,匹配错误了。
在这里插入图片描述
暴力算法会移进 1 字符,然后重新从 s 的 5 号字符和 p 的 0 号字符开始匹配。相当于 s 的索引回退到 5,p 的索引回退到 0。
在这里插入图片描述
而 kmp 算法就比较聪明一些。既然 p 字符串的 0 到 4 号元素都匹配成功了,我们何不利用这个信息,多向前移动几个字符呢?怎么做呢? kmp 算法的思想如下:到目前位置,已经成功匹配了 p 的前缀中 “ababa” 这个字符串。然后我们同时考虑它的 前缀 和 后缀,会发现
在这里插入图片描述
它的前三个字符和后三个字符是完全一样的。所以我们完全可以向下图一样,从 s 的 9 号 字符和 p 的 3 号字符开始匹配,相当于 s 的索引没有回退,p 的索引回退到 3。这可比暴力匹配回退的少多了,因此也就快多了。
在这里插入图片描述
至此 kmp 算法的思想就讲完了。上述思想,如果不考虑如何实现的话,可能最多 2 分钟就可以理解。
但是想要实现它却又不知该怎么下手,那我们就一点一点的拆解它。

next 数组引入

next 数组,往往又叫前缀函数。我们不整那么高深的东西,就为了解决一个问题:p 的索引该回退到哪? 根据上面的分析,s 的索引是没有回退的,那么 p 的索引该回退到哪里呢?这个问题需要再精确一些:如果 p 在 索引 i 的位置匹配失败了,那么 p 的索引该回退到哪里?这就是 next 数组的作用:索引应该回退到 next[i] 的位置。
为了不使代码复杂化,我们先假设,如果已经得到了 next 数组,算法框架是什么样的呢?

  • 版本1:
    int findString(string s, string p) {int n = s.size(), m = p.size();int i, j = 0;for (i = 0; i < n;) {if (s[i] == p[j]) { // 如果相等,i 和 j 都进 1i++, j++;} else { // 如果不相等,i 保持不变,j 变为 next[j]j = next[j];}// 如果 j == m 表示 s 在 i 处匹配完成了,返回 i - m;if (j == m) return i - m;}return -1;}

根据版本一的注释,看起来好像没什么问题,但不幸的是,它有 bug。j = next[j] 这一行没有处理边界情况:即当s[i] 不等于 s[j],并且,当 j == 0 时,意味着 0 号位置也匹配失败了,这时候 next[j] 应该等于多少呢?答案是:还是 0。为什么呢?即使 0 号位置匹配失败了,下一次应该是要把 s 的索引加 1,而 p 的索引依旧是 0 才对。于是有了下面的代码版本。
plus. 有一些别的 kmp 算法版本 定义为 -1。实际上是把整个 next 数组做了一次变换,只要改一下 next 数组的定义即可。我们学会一个就好了。

版本2.1

    int findString(string s, string p) {int n = s.size(), m = p.size();int i, j = 0, flag = 0;for (i = 0; i < n;) {if (s[i] == p[j]) { // 如果相等,i 和 j 都进 1i++, j++;} else { // 如果不相等,i 保持不变,j 变为 next[j]j = next[j];if (flag == 1) {flag = 0;i++;} else if (j == 0) flag = 1;}// 如果 j == m 表示 s 在 i 处匹配完成了,返回 i - m + 1;if (j == m) return i - m;}return -1;}

版本2.1是一个正确的版本,但是不够优雅。作为一个程序员,要有一定的审美,版本 2.1 本质上是用单层循环模拟了多层循环,如果能够拆出来,就更好了。
版本2.2

		int n = s.size(), m = p.size();int i, j = 0;for (i = 0; i < n; i++) {// 如果不相等,i 保持不变,j 变为 next[j]while (j != 0 && s[i] != p[j]) {j = next[j];}if (s[i] == p[j]) { // 如果相等,j 进 1j++;}// 如果 j == m 表示 s 在 i 处匹配完成了,返回 i - m + 1;if (j == m) return i - m + 1;}return -1;}

嗯,版本2.2 就优雅许多了。

next 数组生成

那么 next 数组该如何生成呢?
在这里插入图片描述
对照这幅图,当 p 的 5 号索引匹配失败是,j 应该跳转到 3 号。可以观察到,next[i] 表示 [0, i - 1] 区间表示的字符串 p i p_i pi 中 重叠的前缀与后缀的最长值 k,并且 k 要小于 p i p_i pi 的长度,因为自己等于自己可不算。
所以算法如下

     vector<int> next(m, 0);for (int i = 2; i < m; i++) { // 前两个数字默认为 0int t = next[i - 1]; // 前面的前面的那个区间有最长多少个重叠的前后缀// 前面的区间末尾 和 前面的前面的区间的末尾如果不相等就一直 next// 这点和匹配算法很像while (t != 0 && p[i - 1] != p[t]) { t = next[t];}next[i] = t;if (p[i - 1] == p[t]) next[i] = t + 1; //如果相等就 +1}

合成如下

    int strStr(string s, string p) {int n = s.size(), m = p.size();// 求 next 数组vector<int> next(m, 0);for (int i = 2; i < m; i++) {int t = next[i - 1];while (p[i - 1] != p[t] && t != 0) t = next[t];if (p[i - 1] == p[t]) t++;next[i] = t;}// 匹配int j = 0;for (int i = 0; i < n; i++) {while (s[i] != p[j] && j != 0) j = next[j];if (s[i] == p[j]) j++;if (j == m) return i - m + 1;}return -1;}

总结

kmp 算法思想就是使用已匹配的前缀字符串,从中抽取出前后缀的重叠部分信息,用以减少字符串匹配中的回退,从而达到加速的作用。其实现有两个坑,一个是next 数组含义的准确定义(我的定义应该和书上不一样),第二个坑就是使用内循环来多次使用 next 数组跳转。其中第二个坑相信大家自己写都能意识到,第一个坑才是决定你能不能写出来的关键因素。我的实现可能不是最好的,但是希望能够好懂一点。

kmp 算法已将前缀信息使用到了极致,还有一些别的字符串比较算法要比 kmp 容易一些,比如 sunday 算法等。还有一些算法可能比 kmp 还要复杂一些,使用到了后缀信息,例如:bm 算法。还有 一个 RK 算法使用哈希字符串的方式,简单易写。这些算法有机会再说吧。

相关文章:

【算法深入浅出】字符串匹配之 KMP 算法

KMP 算法是一种字符串匹配算法。字符串匹配算法的目标是&#xff1a;在字符串 s 中找到与模式串 p 相等的子串&#xff0c;输出其位置。例如&#xff1a;s “abcdef”&#xff0c;p “cdef”&#xff0c;p 在 s 中的位置是 2&#xff08;从 0 开始计数&#xff09;。 容易想到…...

放弃webstrom转战vscode

本来是webstrom的忠实用户&#xff0c;无奈webstrom要么需要在网上找一个破解版或者不断的去找激活码&#xff0c;且破解版和激活码的文章总是很多&#xff0c;但是要找到真正有效的却总是要花费不少功夫。终于忍无可忍&#xff0c;转战vscode。&#xff08;注&#xff1a;文中…...

VSCode 和 CLion

文章目录 一、VSCode1、文档2、插件3、智能编写4、VSCode 与 C&#xff08;1&#xff09;安装&#xff08;2&#xff09;调试&#xff08;a&#xff09;使用 CMake 进行跨平台编译与调试&#xff08;b&#xff09;launch.json&#xff08;c&#xff09;传参 &#xff08;3&…...

Learn Prompt- Midjourney Prompt:Prompt 提示语

基础结构​ 一个基本的提示可以简单到一个单词、短语或表情符号。非常短的提示将在很大程度上依赖于 Midjourney 的默认样式。 完整 prompt&#xff1a;可以包括一个或多个图像链接、多个文本短语或单词&#xff0c;以及一个或多个后缀参数 Image Prompts: 可以将图像 URL 添加…...

uvm白皮书练习_ch2_ch223_加入objection机制

UVM中通过objection机制来控制验证平台的关闭。 在每个phase中&#xff0c;UVM会检查是否有objection被提起&#xff08;raise_ objection&#xff09;&#xff0c;如果有&#xff0c;那么等待这个objection被撤销&#xff08;drop_objection&#xff09;后停止仿真&#xff1b…...

利用C++开发一个迷你的英文单词录入和测试小程序-增强功能

小玩具基本完成之后&#xff0c;在日常工作中&#xff0c;记录一些单词&#xff0c;然后定时再复习下&#xff0c;还真的有那么一点点用&#xff08;毕竟自己做的小玩具&#xff09;。 在使用过程中&#xff0c;遇到不认识的单词&#xff0c;总去翻译软件翻译&#xff0c;然后…...

kibana启动报错

1.响应 超过时间30000ms (1) docker rm elasticsearch #从docker中删除es docker rm kibana #从docker中删除kibana (2)重新安装启动es加大最大运行内存 :1024M docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \ -e "discovery.typesingle-node" \ -…...

排查内存泄露

1 通过Performance确认是否存在内存泄露 一个存在内存泄露的 DEMO 代码&#xff1a; App.vue <template><div><button click"myFn" style"width: 200px; height: 200px;"></button><home v-if"ishow"></hom…...

【LeetCode-简单题】501. 二叉搜索树中的众数

文章目录 题目方法一&#xff1a;暴力哈希方法二&#xff1a;利用二叉搜索树的特性&#xff08;递归双指针&#xff09; 题目 方法一&#xff1a;暴力哈希 这是针对于普通二叉树的解法 统计number出现次数 然后将次数最大的众数集 取出来 Map<Integer , Integer > map …...

MAC word 如何并列排列两张图片

系统&#xff1a;MAC os 参考博客 https://baijiahao.baidu.com/s?id1700824516945958911&wfrspider&forpc 步骤1 新建一个word文档和表格 修改表格属性 去掉自动重调尺寸以适应内容 插入图片 在表格的位置插入对应的图片如下 去除边框 最终结果如下...

PTA第三章作业题

文章目录 前言7-1 比较大小Ⅰ. 方法一 &#xff1a;直接判断法Ⅱ. 方法二&#xff1a;交换法 7-2 比较两个数的大小Ⅰ. 方法 &#xff1a;直接判断法 7-3 成绩等级Ⅰ. 方法 &#xff1a;直接判断法 7-4 打鱼晒网Ⅰ. 方法 &#xff1a;直接判断法 7-5 计算奖金Ⅰ. 方法 &#xf…...

vscode vue html 快捷键

css文件 选择多行 按下ctrl不放 按下鼠标滚轮不放&#xff08;鼠标中键&#xff09; 鼠标向下移动 同时修改多个相同的字符串 <style> .base-goods-item li {width: 304px;height: 404px;background-color: #eef9f4; } .base-goods-item li {display: block; } .base-…...

mysql锁相关的总结

1、参考文章 MySQL 主键索引在 RR 和 RC 隔离级别下的加锁情况总结_51CTO博客_mysql二级索引加锁 2、 show OPEN TABLES where In_use > 0; -- 类似rc的需求 show variables like innodb_locks_unsafe_for_binlog; SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX; -- …...

计算机竞赛 深度学习乳腺癌分类

文章目录 1 前言2 前言3 数据集3.1 良性样本3.2 病变样本 4 开发环境5 代码实现5.1 实现流程5.2 部分代码实现5.2.1 导入库5.2.2 图像加载5.2.3 标记5.2.4 分组5.2.5 构建模型训练 6 分析指标6.1 精度&#xff0c;召回率和F1度量6.2 混淆矩阵 7 结果和结论8 最后 1 前言 &…...

docker-compose搭建的mysql,如何定时备份数据

一、前言 使用docker-compose搭建的mysql中自带了mysqldump&#xff0c;所以在服务器上如何使用容器中的mysqldump命令是实现备份的原理&#xff0c;下面是主要实现的命令 docker exec -it mysql mysqldump -u root -p$mysql_password $database_name > $backup_file二、备…...

webpack:关于处理html文件的插件html-webpack-plugin、add-asset-html-webpack-plugin

简介 add-asset-html-webpack-plugin 将 JavaScript或CSS文件添加到由html-webpack-plugin插件生成的HTML中去。 html-webpack-plugin 默认配置会在出口目录中&#xff08;通过output.path选项配置&#xff09;生成一个index.html文件&#xff1b; 生成的index.html文件将会…...

如何两个不同的脚本文件之间传递参数

两个不同的Shell脚本之间如何访问传递的参数取决于它们是如何调用的。如果一个Shell脚本1调用另一个Shell脚本2并且想要将参数传递给被调用的脚本2&#xff0c;可以使用以下方法&#xff1a; 方法1&#xff1a;通过位置参数传递参数 这是一种常见的方法&#xff0c;其中一个脚…...

一篇文章彻底搞懂熵、信息熵、KL散度、交叉熵、Softmax和交叉熵损失函数

文章目录 一、熵和信息熵1.1 概念1.2 信息熵公式 二、KL散度和交叉熵2.1 KL散度(相对熵)2.2 交叉熵 三、Softmax和交叉熵损失函数3.1 Softmax3.2 交叉熵损失函数 一、熵和信息熵 1.1 概念 1. 熵是一个物理学概念&#xff0c;它表示一个系统的不确定性程度&#xff0c;或者说是…...

[架构之路-223]:数据管理能力成熟度评估模型DCMM简介

目录 一、背景 二、评估依据 三、评估内容 四、主要适用对象 五、能力等级 六、不同层次的文件&#xff1a; 一、背景 信息技术与经济社会的交汇融合引发了数据爆发式增长。数据蕴含着重要的价值&#xff0c;已成为国家基础性战略资源&#xff0c;正日益对全球生产、流通…...

十大排序算法的实现(C/C++)

以下是十大经典排序算法的简单 C 实现&#xff1a; 冒泡排序&#xff08;Bubble Sort&#xff09;&#xff1a; 思想&#xff1a;重复地遍历要排序的列表&#xff0c;比较相邻的两个元素&#xff0c;如果它们的顺序错误就交换它们。时间复杂度&#xff1a;最坏情况和平均情况…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)

文章目录 1.什么是Redis&#xff1f;2.为什么要使用redis作为mysql的缓存&#xff1f;3.什么是缓存雪崩、缓存穿透、缓存击穿&#xff1f;3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...

uni-app学习笔记二十二---使用vite.config.js全局导入常用依赖

在前面的练习中&#xff0c;每个页面需要使用ref&#xff0c;onShow等生命周期钩子函数时都需要像下面这样导入 import {onMounted, ref} from "vue" 如果不想每个页面都导入&#xff0c;需要使用node.js命令npm安装unplugin-auto-import npm install unplugin-au…...

【机器视觉】单目测距——运动结构恢复

ps&#xff1a;图是随便找的&#xff0c;为了凑个封面 前言 在前面对光流法进行进一步改进&#xff0c;希望将2D光流推广至3D场景流时&#xff0c;发现2D转3D过程中存在尺度歧义问题&#xff0c;需要补全摄像头拍摄图像中缺失的深度信息&#xff0c;否则解空间不收敛&#xf…...

微信小程序 - 手机震动

一、界面 <button type"primary" bindtap"shortVibrate">短震动</button> <button type"primary" bindtap"longVibrate">长震动</button> 二、js逻辑代码 注&#xff1a;文档 https://developers.weixin.qq…...

力扣-35.搜索插入位置

题目描述 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 class Solution {public int searchInsert(int[] nums, …...

C++.OpenGL (20/64)混合(Blending)

混合(Blending) 透明效果核心原理 #mermaid-svg-SWG0UzVfJms7Sm3e {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-SWG0UzVfJms7Sm3e .error-icon{fill:#552222;}#mermaid-svg-SWG0UzVfJms7Sm3e .error-text{fill…...

【Linux】自动化构建-Make/Makefile

前言 上文我们讲到了Linux中的编译器gcc/g 【Linux】编译器gcc/g及其库的详细介绍-CSDN博客 本来我们将一个对于编译来说很重要的工具&#xff1a;make/makfile 1.背景 在一个工程中源文件不计其数&#xff0c;其按类型、功能、模块分别放在若干个目录中&#xff0c;mak…...

tomcat指定使用的jdk版本

说明 有时候需要对tomcat配置指定的jdk版本号&#xff0c;此时&#xff0c;我们可以通过以下方式进行配置 设置方式 找到tomcat的bin目录中的setclasspath.bat。如果是linux系统则是setclasspath.sh set JAVA_HOMEC:\Program Files\Java\jdk8 set JRE_HOMEC:\Program Files…...

高考志愿填报管理系统---开发介绍

高考志愿填报管理系统是一款专为教育机构、学校和教师设计的学生信息管理和志愿填报辅助平台。系统基于Django框架开发&#xff0c;采用现代化的Web技术&#xff0c;为教育工作者提供高效、安全、便捷的学生管理解决方案。 ## &#x1f4cb; 系统概述 ### &#x1f3af; 系统定…...

算法—栈系列

一&#xff1a;删除字符串中的所有相邻重复项 class Solution { public:string removeDuplicates(string s) {stack<char> st;for(int i 0; i < s.size(); i){char target s[i];if(!st.empty() && target st.top())st.pop();elsest.push(s[i]);}string ret…...