经典字符串匹配算法——KMP算法_kmp字符串匹配算法-程序员宅基地

技术标签: 算法  C++  c++  

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 + 1
		if (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 + 1
		if (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;//遍历str
	int subj = 0;//遍历subStr

	while (stri < strLength && subj < subLength)
	{
    
		//回退至子串第一个元素还未匹配,或者正常匹配,都需要将两个坐标+1
		if (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即可

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/aaaaaaaWoLan/article/details/122005602

智能推荐

王者竞速游戏服务器维护了,《王者荣耀》不停机更新维护-程序员宅基地

文章浏览阅读170次。今天王者荣耀的服务器似乎出了点小问题,玩家在游戏里出现了许多BUG,所以官方对全服的玩家进行了一次不停机的更新,那么此次更新的内容相信大家都很想知道吧,小编为大家整理了相关的资讯,感兴趣的玩家就跟着小编一起来看看吧,希望能帮到你。王者荣耀7月17日进行了不停机更新维护,下面给大家带来具体的更新内容,一起来看看吧。亲爱的召唤师:我们计划在2019年7月17日 8:30-9:30 对全服进行不停机更新..._为什么王者荣耀今天不停机

将LGBM用作二分类问题之上_matlablgbm模型-程序员宅基地

文章浏览阅读460次,点赞8次,收藏9次。LGBM(Light Gradient Boosting Machine)可以用于解决二分类问题。事实上,LGBM在实际应用中被广泛用于分类问题,包括二分类问题。在使用LGBM进行二分类问题时,你可以指定目标变量的类型和相关参数。对于二分类问题,你可以使用。指定了二分类问题的目标。你可以根据具体问题和数据集的特点调整其他参数,以优化模型性能。表示使用对数损失作为损失函数,是二分类问题的默认设置。被用于创建一个二分类模型,_matlablgbm模型

Java包装类;基本数据类型与字符串的相互转换_java 基本类型转包装类-程序员宅基地

文章浏览阅读531次。Java包装类;基本数据类型与字符串的相互转换_java 基本类型转包装类

【重构架构设计】_重构设计-程序员宅基地

文章浏览阅读368次,点赞9次,收藏9次。通过以上两个示例,可以看到领域驱动设计的特点:每个领域都有自己的模型(User和Order类),聚合根(User和Order类的实例)和业务逻辑(changePassword、addItem等方法)。引入领域驱动设计(DDD):DDD是一种面向领域模型设计的方法,通过将业务领域划分为多个小的子领域来进行解耦。通过使用微服务架构,可以将系统解耦为多个独立的服务,提高系统的可用性和可伸缩性。通过以上的步骤,可以有效进行业务解耦,提高代码的高可用性和可维护性。在订单管理领域中,专注于订单的信息、行为和业务规则。_重构设计

cvc-complex-type.2.4.c: The matching wildcard is strict, but no declaration的解决-程序员宅基地

文章浏览阅读2.3w次,点赞7次,收藏12次。导入了一个工程,编译什么的都还好,但是报了一个XML的错误。cvc-complex-type.2.4.c: The matching wildcard is strict, but no declaration can be found for element 'dubbo:application'. 具体错误如下:Multiple annotations found at this line: ..._cvc-complex-type.2.4.c: the matching wildcard is strict, but no declaration

如何避免对话冲突-《关键对话》笔记与心得_关键对话 为什么我们会面临关键冲突-程序员宅基地

文章浏览阅读1.9k次。 目录 1~关键对话的含义2~关键对话的目标3~处在关键对话场景下的心理状态4~好公司和差公司的区别5~ 关键对话的重要性6~跟踪表现良好的人行为7~专注于真正想要的结果8~ 谈话禁忌9~决策的四种方式 自勉1~关键对话的含义两个或更多人参与讨论,条件:1)事关重大ps: 个..._关键对话 为什么我们会面临关键冲突

随便推点

Solidworks装配体打包/Pack and Go和另存为两种方法的区别-程序员宅基地

文章浏览阅读7.6k次。Solidworks装配体打包/Pack and Go和另存为两种方法的区别_pack and go

戴尔笔记本怎么安装统信uos系统?戴尔笔记本安装统信uos+win双系统_win7和uos双系统-程序员宅基地

文章浏览阅读2.6k次。答案是肯定的,还有的网友问,能不能保留本地windows系统然后再安装统信uos形成双系统,答案也是肯定的,下面小编就教大家在保留本地windows系统的同时安装统信uos系统形成双系统的方法教程。1,插入制作好的统信uos系统盘,重启按F12或FN+F12调出启动管理对话框,默认因为是uefi的引导,所以要选择uefi开头的USB HDD识别到U盘启动进入统信uos系统安装界面,如下图所示;3,接着进入统信uos系统安装界面后,因为我们要保留本地的windows系统,所以这里选择自定义安装,如下图所示;_win7和uos双系统

react-router v6实现动态的title(react-router-dom v6)_react router v6 改 title-程序员宅基地

文章浏览阅读5.2k次,点赞3次,收藏2次。react-router v6实现动态的title(react-router-dom v6)_react router v6 改 title

掌握之分布式-6.分布式数据库-程序员宅基地

文章浏览阅读162次。掌握高并发、高可用架构第三章 分布式本章介绍分布式架构的底层技术。主要说明面试过程中可能被问到的技术点。第六节 分布式数据库MyCat分库分表 Sharding1. 分库分表的方法垂直切分,也就是因为表多而数据多,将关系紧密(比如统一模块)的表切分出来放到一个服务器中水平切分,表不多,而是表中数据量庞大,也就是把表的数据按照某种规则切分到多个服务器中现实中多是这两种的混合2. 分..._分布式数据库使用group by 代价大吗

克隆的虚拟机无法修改静态ip_vm克隆的虚拟机ip不能改为静态ip-程序员宅基地

文章浏览阅读547次。job for network.service failedsystemctl restart network.service failed造成这种情况,一般可能是由于克隆的虚拟机,MAC地址与本机的对应不上,所以需要修改MAC地址与本机对应上。ip addr#查看本机的MAC地址vim /etc/sysconfig/network-script/ifcfg-ens33#修改MAC地址有时候ip地址会莫名的消失,因为有2套网络管理工具将NetworkManager关闭systemctl s_vm克隆的虚拟机ip不能改为静态ip

OpenOCD的调试_c# openocd的配置-程序员宅基地

文章浏览阅读4.3k次,点赞2次,收藏5次。1、工具本文使用的软、硬件工具如下:目标开发板:STSPEAr310EVB2.0(官网www.st.com)及其交叉编译环境。仿真器:OpenJTAG(官网www.100ask.net)驱动(www.ftdichip.com/Drivers/D2XX.htm)操作系统:Fedora(官网fedoraproject.org)调试软件:openocd(官网openocd.sourceforge.net)2、安装OpenJTAG驱动本文不介绍交叉编译环境的安装,若有需要请..._c# openocd的配置