经典字符串匹配算法——KMP算法

article/2025/9/21 12:32:10

KMP算法

KMP算法是一种高效的字符串匹配算法,在传统暴力遍历匹配的基础上做了一定的优化。

首先KMP算法的实现也是使用了回退思想,不过与暴力遍历不同,KMP的回退,是让子串进行匹配,而不是主串。

KMP示例

首先我们来看两个例子来理解KMP算法:

例1:

分别从str的i和sub的j位置处开始匹配:

image-20211217202522303

此时a与c不匹配,如果暴力遍历的话,是i回到到b,j也回到a,重新一轮匹配。而KMP算法,是将子串的j回到第二个a,str[i]与sub[j]重新开始匹配。原因很明显,第二个ab与第一个ab是相同的,因为这一部分主串与子串是匹配的,所以在主串中也是这样,因而主串的后半部分的ab就匹配了子串的前半部分ab,就省去了再重新匹配的过程,直接继续之前的部分匹配结果再向后匹配。

image-20211217203432475

继续匹配:

image-20211217203636114

再看一个例2:

image-20211217202229958

开始匹配:

image-20211217201316065

我们看到,此时a 与 c不匹配,如果暴力遍历的话,是i回到到b,j也回到a,重新一轮匹配。而KMP算法,是将子串的j回到a,str[i]与sub[j]重新开始匹配。

很多人可能疑问,为什么是这样?好像很有道理的样子

image-20211217194633567

我们仔细观察,在子串中,j所指的c之前,除了最开头的a,找不到已经与主串匹配好了的字符串ab或者是a(要与c相邻),**注意,是除了最开头的a。**正是因为在这段区间找不到ab或a,所以才从头再开始遍历。i不动,因为在i前面的主串中已经找不到与abc(子串的前三个字符)匹配的字符串了。

j回到最开始后,再开始匹配:

image-20211217201428812

与之前一样,在j所指的d之前,除了最开头的a,找不到与主串已经匹配好了的abc或ab或a,所以j又要重新回头,到最开始。

再次遍历:

image-20211217201622267

KMP核心

看完上述过程后,我们要实现这种让子串回头的方法,就是定义一个next数组,里面对应子串中每一个字符出现不匹配情况时,要回头到达的位置。

KMP 的精髓就是 next 数组:也就是用 next[j] = k;来表示,每个j 都对应一个 K 值, 这个 K 就是将来j要移动时,要移动到的位置。
而 K 的值是这样求的:
1、规则:找到匹配成功部分的两个相等的真子串(不包含本身),一个以下标 0 字符开始另一个以 j-1 下标字符结尾
2、不管什么数据,next[0] = -1;next[1] = 0;
3、如果找得到,则k值等于相等的子串的长度;如果找不到,则k值等于0,即回到a

以例1中的子串为例:ababc

我们默认第一个和第二个字符出现不匹配情况时,next数组中存的值分别是-1和0。即,a不匹配时,k == -1,则需要主串的i向后走一步(这里先解释,可以到代码实现的时候再理解),b不匹配时,回到a重新匹配。

第三个字符a如果出现不匹配情况,我们找已经匹配的相等的两个子串:以a开头的字符串,与以b结尾的字符串,很明显找不到,所以它要回到a,k = 0

第四个字符b出现不匹配情况时,同样找已经匹配的相等的两个子串:以a开头的字符串,以a结尾的字符串,可以找到字符串a,所以k = 1,即回到b

第四个字符c出现不匹配情况时,找已经匹配的相等的两个子串:以a开头的字符串,以a结尾的字符串,可以找到ab,所以k = 2,即回到第二个a

至此,子串的next数组就为:[-1, 0, 0, 1, 2]

两道求next数组的练习:

练习 1: ”ababcabcdabcde”

next数组:-1 0 0 1 2 0 1 2 0 0 1 2 0 0

练习 2: ”abcabcabcabcdabcde”

next数组:-1 0 0 0 1 2 3 4 5 6 7 8 9 0 1 2 3 0

建议大家自主完成,这有助于我们后面的理解

next数组求解

那么,如何用代码求出next数组呢?

求sub[j]的k值

1、当sub[j-1] == sub[k]时(k为sub[j-1]的k值)

同样以ababc为例

我们已知前面四个字符的next数组为:[-1, 0, 0, 1 ],求c的k值

如果按照上面的求法,我们知道k = 2,但仔细观察,c前面的b,即sub[j - 1],与第二个字符b,即sub[k]相同(该k为前一个字符b的k值),第二个b之前已经匹配的子串aba中,两个符合要求的相等的子串为a,而现在的匹配的子串为abab,这两个b相等,在前面字符k值的基础上,c的k值就可以简单地看成是前一个字符的k + 1,即c的k == 1+1 = 2

2、当sub[j-1] != sub[k]时

以ababcd为例

我们已知ababcd的前五个字符的next数组:[-1, 0, 0, 1, 2],求d的k值

很明显,在已经匹配的子串ab中,找不到相等的两个子串:以a开头的字符串,以c结尾的字符串。所以d要回退到最开始的a处。

此时的j指向d,且sub[j - 1] != sub[k],即c != a,d应该回退到元素sub[k] (即第二个a)的k值处,也就是d的k == next[k] = 0

代码实现:

1、求出next数组

分两种情况:

  1. sub[j-1] == sub[k]
  2. sub[j-1] != sub[k]
    还有一种情况之前我们没有提到,就是回退到了最开始元素的k值处,即-1.此时说明主串中的字符与j及j之前的所有字符串都不匹配,所以需要向后走一步,从下一个字符再重新开始匹配
void GetNext(vector<int>& next, const string& subStr, int n)
{//默认处理next[0] = -1;next[1] = 0;int i = 2;//从第3个元素开始处理int k = 0;//表示i的前一个元素的next数组元素值while (i < n){//可能回退至首元素了,说明当前元素需要重新匹配,即从首元素开始,所以也是k = k + 1if (k == -1 || subStr[i - 1] == subStr[k])//P[i - 1] == P[k]{k = k + 1;next[i] = k;i++;}else//不匹配则回退{k = next[k];}}}

2、匹配逻辑的实现

与暴力求解类似,只不过当字符不相等时,不是双方都回头,而是子串回头。正因为子串会回退,当回退的下标j == -1时,表明主串需要向后走。

完整代码:

#include<iostream>
#include<string>
#include<vector>using namespace std;void GetNext(vector<int>& next, const string& subStr, int n)
{next[0] = -1;next[1] = 0;int i = 2;int k = 0;//表示i的前一个元素的next数组元素值while (i < n){//可能回退至首元素了,说明主串需要向后走,子串从首元素开始,所以也是k = k + 1if (k == -1 || subStr[i - 1] == subStr[k])//P[i - 1] == P[k]{k = k + 1;next[i] = k;i++;}else//不匹配则回退{k = next[k];}}}int KMP(const string& str, const string& subStr, int pos)//从主串的str位置开始匹配
{int strLength = str.size();int subLength = subStr.size();if (str.empty() || subStr.empty()) { return -1; }if (pos >= strLength || pos < 0) { return -1; }vector<int> next(subLength);//子串的next数组GetNext(next, subStr, subLength);int stri = pos;//遍历strint subj = 0;//遍历subStrwhile (stri < strLength && subj < subLength){//回退至子串第一个元素还未匹配,或者正常匹配,都需要将两个坐标+1if (subj == -1 || str[stri] == subStr[subj]){stri++;subj++;}else//不匹配了,回退{subj = next[subj];}}if (subj == subLength)//说明匹配到位了{//返回主串的起始匹配位置return stri - subLength + 1;}return -1;
}

next数组的优化

以子串aaaaaaaab为例,它的next数组为:[-1, 0, 1, 2, 3, 4, 5, 6, 7]当主串字符str[i] != 子串中的最后一个a时,匹配的字符回退到下标6的字符a,此时str[i]还是 != a,所以依旧需要回退,一直回退到第一个字符a

对于这种情况,我们可以做一个优化,当回退的字符s2与当前字符s1相同,则继续回退至s2的k值处,一直回退到不相等或者最开始处。所以s1的k值就等于s2的k值。

练习:模式串 t=‘abcaabbcabcaabdab’ ,该模式串的 next 数组的值为( D ) , nextval 数组的值为 (F)。
A. 0 1 1 1 2 2 1 1 1 2 3 4 5 6 7 1 2 B. 0 1 1 1 2 1 2 1 1 2 3 4 5 6 1 1 2
C. 0 1 1 1 0 0 1 3 1 0 1 1 0 0 7 0 1 D. 0 1 1 1 2 2 3 1 1 2 3 4 5 6 7 1 2
E. 0 1 1 0 0 1 1 1 0 1 1 0 0 1 7 0 1 F. 0 1 1 0 2 1 3 1 0 1 1 0 2 1 7 0 1

注意:这里是将第一个元素和第二个元素的初始k值设置为0和1,所以我们只要把求出的结果+1即可


http://chatgpt.dhexx.cn/article/4WBssDDr.shtml

相关文章

16.算法之字符串匹配算法

前言 字符串匹配是我们在程序开发中经常遇见的功能&#xff0c;比如sql语句中的like,java中的indexof,都是用来判断一个字符串是否包含另外一个字符串的。那么&#xff0c;这些关键字&#xff0c;方法&#xff0c;底层算法是怎么实现的么&#xff1f;本节&#xff0c;我们来探…

字符串匹配算法(BM)

文章目录 1. BM&#xff08;Boyer-Moore&#xff09;算法1.1 坏字符规则1.2 好后缀规则1.3 两种规则如何选择 2. BM算法代码实现2.1 坏字符2.2 好后缀2.3 完整代码2.4 调试 3. 总结 1. BM&#xff08;Boyer-Moore&#xff09;算法 思想&#xff1a;有模式串中不存在的字符&…

六种字符串匹配算法详解(含代码演示)

1. Brute-Force算法 2. Rabin-Karp Hash算法 3. Kmp算法 4. Kmp的优化算法 5. Sunday算法 6. Shift-And算法 ps&#xff1a;字符串匹配其实是单模匹配问题 1.Brute-Force 朴素匹配算法&#xff08;暴力匹配&#xff09; 时间复杂度&#xff1a;O(n*m) //返回 文本串s中第一…

字符串匹配算法(BF、KMP)

BF算法 描述&#xff1a; BF&#xff0c;Brute Force&#xff0c;暴力匹配的意思&#xff0c;是最简单直观的字符串匹配算法。假设有主串s1和子串s2&#xff0c;根据BF算法判断s1是否包含s2的步骤如下&#xff1a; 初始下标指针 i, j 分别指向s1, s2的首位置&#xff0c;若s1…

这可能是全网最好的字符串匹配算法讲解

点击上方 好好学java &#xff0c;选择 星标 公众号重磅资讯&#xff0c;干货&#xff0c;第一时间送达 今日推荐&#xff1a;14 个 github 项目&#xff01;个人原创100W 访问量博客&#xff1a;点击前往&#xff0c;查看更多为保证代码严谨性&#xff0c;文中所有代码均在 le…

Spring boot 项目(五)——AOP切面

一、AOP简介 1、在软件业&#xff0c;AOP为Aspect Oriented Programming的缩写&#xff0c;意为&#xff1a;面向切面编程&#xff0c;通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。 2、AOP是OOP的延续&#xff0c;是软件开发中的一个热点&#xff0c;也…

Spring AOP 切面(Aspect)应用详解

1. AOP 切面应用 下面是一个AOP切面的一个简单的应用实例 引入AOP依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>创建切面类对象 Aspect Component pub…

springboot实现AOP切面编程

概述 AOP(Aspect Oriented Programming) 即面向切面编程。面向切面是面向对象中的一种方式而已。在代码执行过程中&#xff0c;动态嵌入其他代码&#xff0c;叫做面向切面编程&#xff08;将交叉业务逻辑封装成成切面&#xff0c;利用AOP功能将切面织入到主业务逻辑———与主…

spring AOP切面及日志记录实现

目录 1.什么是AOP切面 2.理解AOP 3.AOP实例 1.自定义注解 2.创建一个切面类 3.将自定义注解标注在测试接口上 1.什么是AOP切面 AOP&#xff08;Aspect Oriented Programming&#xff09;&#xff0c;面向切面思想&#xff0c;是Spring的三大核心思想之一。 在项目中经常…

Aop切面自定义注解的使用

一&#xff1a;功能简介 本文主要记录如何使用aop切面的方式来实现日志记录功能。 主要记录的信息有: 操作人&#xff0c;方法名&#xff0c;参数&#xff0c;运行时间&#xff0c;操作类型(增删改查)&#xff0c;详细描述&#xff0c;返回值。 二&#xff1a;项目结构图 三…

AOP切面注解

一.前言 在以前的项目中&#xff0c;很少去关注spring aop的具体实现与理论&#xff0c;只是简单了解了一下什么是aop具体怎么用&#xff0c;看到了一篇博文写得还不错&#xff0c;就转载来学习一下&#xff0c;博文地址&#xff1a;http://www.cnblogs.com/xrq730/p/4919025.h…

AOP切面执行顺序

文章目录 一. 概述二. 讲述1. 单切面中各通知方法的执行顺序2. 多切面中各通知方法的执行顺序3. 多切面的通知方法中抛出异常 参考资料 一. 概述 本文主要讲述以下几点 单AOP切面时&#xff0c;各通知方法的执行顺序。多AOP切面时&#xff0c;多切面的执行顺序和各通知方法的执…

spring aop切面执行顺序

spring aop切面执行顺序 切面执行顺序 现有切面1、切面2对同一个切点按先后顺序执行&#xff08;切面1先于切面2执行&#xff09; 切面1&#xff1a;Before、After、AfterReturnning、AfterThrowing、Around 执行前、Around 正常执行、Around 异常执行、Around 执行后切面2&am…

一文带你搞定AOP切面

摘要&#xff1a;AOP在spring中又叫“面向切面编程”&#xff0c;是对传统我们面向对象编程的一个补充&#xff0c;主要操作对象就是“ 切面”&#xff0c; 可以简单的理解它是贯穿于方法之中&#xff0c;在方法执行前、执行时、执行后、返回值后、异常后要执行的操作。 本文分…

SpringBoot AOP切面实现

文章目录 一、AOP简介二、AOP体系与概念三、AOP实例1、创建SpringBoot工程2、添加依赖3、AOP相关注解3.1、Aspect3.2、Pointcut3.2.1、execution()3.2.2、annotation() 3.3、Around3.4、Before3.5、After3.6、AfterReturning3.7、AfterThrowing 一、AOP简介 AOP&#xff08;As…

AOP切面编程的理解

一、什么是Spring的AOP&#xff1f; AOP在spring中又叫“面向切面编程”&#xff0c;它可以说是对传统我们面向对象编程的一个补充&#xff0c;从字面上顾名思义就可以知道&#xff0c;它的主要操作对象就是“切面”&#xff0c;所以我们就可以简单的理解它是贯穿于方法之中&a…

AOP切面使用

一、主要设计注解&#xff1a; Aspect After before Pointcut Around pom文件引入 <!--用于aop切面编程--> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency> 二、AOP核心…

AOP面向切面

1.什么是Spring的AOP? AOP又叫"面向切面编程",是对传统的面向对象编程的一个补充,主要的操作对象就是"切面 ",可以简单的理解它是贯穿于方法之中,在方法执行前、执行时、执行后、返回值后、异常后要执行的操作。 相当于将我们原本一条线执行的程序在中间切…

【JavaEE】Spring AOP (面向切面)详解

目录&#xff1a; 1. 什么是 Spring AOP&#xff1f;1.1 AOP1.2 使用 AOP 的场景 2. AOP 组成2.1 切面&#xff08;Aspect&#xff09;2.2 连接点&#xff08;Join Point&#xff09;2.3 切点&#xff08;Pointcut&#xff09;2.4 通知&#xff08;Advice&#xff09; 3. AOP 概…

斜杠,反斜杠说明

/ 斜杠 \反斜杠 在window中都用斜杠 反斜杠是用来转译字符串的 eg: \"a\" 输出"a"