C++ 算法进阶系列之从 Brute Force 到 KMP 字符串匹配算法的优化之路

news2025/1/9 1:50:50

1. 字符串匹配算法

所谓字符串匹配算法,简单地说就是在一个目标字符串中查找是否存在另一个模式字符串。如在字符串 ABCDEFG 中查找是否存在 EF 字符串。

可以把字符串 ABCDEFG 称为原始(目标)字符串EF 称为子字符串模式字符串

本文通过如下 3 种字符串匹配算法之间的差异性来探究 KMP 算法的本质。

  • BF(Brute Force,暴力检索算法)
  • RK (Robin-Karp 算法)
  • KMP (D.E.Knuth、J.H.Morris、V.R.Pratt 算法)

2. BF(Brute Force,暴力检索)

BF 算法是一种原始、低级的穷举算法。

2.1 算法思想

如下使用长、短指针方案描述 BF 算法:

  • 初始指针位置: 长指针指向原始字符串的第一个字符位置、短指针指向模式字符串的第一个字符位置。这里引入辅助指针概念,并不是必须的。

Tips: 辅助指针是长指针的替身,替长指针和短指针所在位置的字符比较。每次初始化长指针位置时,让辅助指针和长指针指向同一个位置。

bf01.png

  • 如果长、短指针位置的字符不相同,则短指针不动、长指针向右移动。如果长、短指针所指位置的字符相同,则用辅助指针替代长指针(长指针位置不动)和短指针位置的字符比较,如果比较相同,则同时向右移动辅助指针和短指针。

bf02.png

  • 如果辅助指针和短指针位置的字符不相同,则重新初始化长指针位置(向右移动),短指针恢复到最原始状态。

bf03.png

bf04.png

  • 借助循环或者递归的方案重复上述流程,直到出口条件成立。

    查找失败: 长指针到达了原始字符串的尾部。当 长指针位置=原始字符串长度 - 模式字符串长度+1 时就可以认定查找失败。

bf05.png

查找成功: 短指针到达模式字符串尾部。

bf06.png

2.2 编码实现

2.2.1 使用辅助指针

使用辅助指针替代长指针和短指针所在位置的字符进行比较。

#include <iostream>
using namespace std;
/*
*  BF 字符串匹配算法
*  参数说明
*  srcStr  原始字符串
*  subStr  子(模式)字符串
*  返回值说明 -1 表示查找失败
*/
int  bruteForceMatch(string srcStr,string subStr) {
	// 长指针,在原始字符串上移动
	int long_index = 0;
	// 短指针,在模式字符串上移动
	int short_index = 0;
	// 辅助指针,初始和长指针位置相同
	int fu_index = long_index;
	// 原始字符串长度
	int str_len = srcStr.size();
	// 模式字符串的长度
	int sub_len = subStr.size();
	while (long_index < str_len-sub_len+1) {
		// 把长指针的位值赋给辅助指针
		fu_index = long_index;
		//初始短指针位置
		short_index = 0;
		while (short_index < sub_len && srcStr[fu_index] == subStr[short_index]) {
			//辅助指针向右
			fu_index ++;
			//短指针向右
			short_index ++;
		}
		if (short_index == sub_len) {
            //匹配成功
			return  long_index;
		}
		//匹配不成功,则长指针向右移动
		long_index ++;
	}
	return -1;
}

测试:

int main(int argc, char** argv) {
	string srcStr="thismymyre";
	string subStr="myr";
	int res= bruteForceMatch(srcStr,subStr);
	cout<<res;
	return 0;
}

输出结果:

bf15.png

2.2.2 使用增量

以长指针为参照起点,需要比较时,以相对增量位置和短指针位置字符比较。

int  bruteForceMatch_(string srcStr,string subStr) {
	// 省略变量声明……
	while (long_index < str_len-sub_len+1) {
		//增量
        int i = 0;
	    int short_index = 0;
		while (short_index < sub_len && srcStr[long_index + i] == subStr[short_index]) {
			i++;
			// 短指针向右
			short_index++;
		}
		if (short_index == sub_len)
			return long_index;
		long_index ++;
	}
	return -1;
}

2.2.3 长指针和短指针直接比较

在原始字符串和模式字符串开始齐头并进逐一比较时,最好不要修改长指针的位置,否则,在比较不成功的情况下,重修正长指针的逻辑有点繁琐。

int  bruteForceMatch_0(string srcStr,string subStr) {
    //省略变量声明……
	while (long_index < str_len) {
		short_index = 0;
		// 长指针和短指针位置的字符比较
		while (short_index < sub_len and srcStr[long_index] == subStr[short_index]) {
			long_index++;
			// 短指针向右
			short_index++;
		}
		if (short_index == sub_len)return long_index-short_index;
		// 修正长指针的位置
		long_index = long_index-short_index+1;
	}
	return -1;
}

2.3 BF 算法的时间复杂度

BF 算法直观,易于实现,但是缺少变通,是典型的穷举思想。

如果原始字符串的长度为 m ,模式字符串的长度为 n。时间复杂度则是 O(m*n),时间复杂度较高。

3. RK(Robin-Karp 算法)

RK算法 ( 指纹字符串查找) 在 BF 算法的基础上做了些改进,基本思路:

如下图示,模式字符串原始字符串比较 3 次后,才发现两者匹配不上,意味着前面的 3 次比较,除了浪费时间,无其它意义。能不能通过一种算法,快速判断出本次比较是否有必要进行。

bf07.png

bf071.png

3.1 RK 的算法思想

  • 选定一个哈希函数(可自定义)。

  • 使用哈希函数计算模式字符串的哈希值。如上计算 thia 的哈希值。

  • 再从原始字符串的开始比较位置起,截取一段和模式字符串长度一样的子串,也使用哈希函数计算哈希值。如上计算 this 的哈希值。

  • 如果两次计算出来的哈希值不相同,则可判断两段模式字符串不相同,没有开始比较的必要。

  • 如果两次计算的哈希值相同,因存在哈希冲突,还是需要使用 BF 算法进行逐一比较。

RK 算法使用哈希函数算法杜绝了不必要的比较。

3.2 编码实现:

/*
*自定义哈希函数
*累加字符串中字符的ASCII
*仅用于研究
*/
int myHash(string str) {
	int total=0;
	for(int i=0; i<str.size(); i++) {
		total+=int(str[i] );
	}
	return total;
}
/*
*
*/
int rkMatch(string srcStr,string subStr) {
	// 长指针
	int long_index = 0;
	// 短指针
	int short_index = 0;
	// 辅助指针
	int fu_index = 0;
	// 原始字符串长度
	int str_len = srcStr.size();
	//  模式字符串的长度
	int sub_len =subStr.size();
	while (long_index < str_len - sub_len + 1) {
		// hash 
		if ( myHash(subStr) != myHash( srcStr.substr(long_index,sub_len) ) ) {
			//哈希值一样
			long_index++;
			continue;
		}
		// 把长指针的位置赋给辅助指针
		fu_index = long_index;
		short_index = 0;
		while (short_index < sub_len && srcStr[fu_index] == subStr[short_index]) {
			//辅助指针向右
			fu_index ++;
			//短指针向右
			short_index ++;
		}
		if (short_index == sub_len)return long_index;
		long_index++;
	}
}

RK 的时间复杂度:

RK 的代码逻辑和 BF 一样,但内置了哈希判断。如果原始子符串长度为 m,模式字符串的长度为 n。时间复杂度为 O(m+n),如果不考虑哈希冲突问题,理想状态下的时间复杂度可以为 O(m)

很显然 RK 算法比 BF 算法要快很多。

4. KMP算法

算法的本质是穷举,这是由计算机的思维方式决定的。

我们谈论"好"、“坏” 算法时,所谓的指能让穷举的次数少一些。比如前面的 RK 算法,通过一些特性提前判断是否值得比较,这样可以省掉很多不必要的内循环。

KMP 也是一样,也是尽可能减少比较的次数。

4.1 KMP 算法思想

KMP 的基本思路和 BF 是一样的(字符串逐一比较)。但在BF 算法做了性能上的优化。

让我们再次回到前面的BF比较流程中。如下图所示,在比较 4 次后,辅助指针和短指针对应位置字符不相同,说明匹配失败。

bf08.png

BF的做法是,让长指针向右移一位,短指针恢复原始状态。再重新逐一比较。

bf09.png

但是,这里应该会有一个思考?难道前面的 4 次成功的比较就没有一点可利用的价值吗?

那就再回放,仔细观察一番。

bf101.png

会发现一个有趣的地方。部分匹配成功的ABAB字符串,在原始字符串后面的AB字符和模式字符串前面的AB字符是相同的。如下填充灰色区域。

bf102.png

直观告诉我们,长指针可以不用回到最初开始的位置,只需要让短指针稍微回一下。

如下图所示:

bf103.png

很明显示缩短了很多不必要的比较次数。

那么这个现象有没有通用性?

再分析如下 2 个字符串的比较,即使前面有 4 次比较成功,当匹配失败后,短指针必须回到最初位置,再重新开始。

bf104.png

bf105.png

那么在什么情况下可以让短指针只做稍微的移动?

说清楚这个问题之前,先理解几个概念:

  • 前缀集合:

    如: ABAB 的前缀(不包含字符串本身)集合 {A,AB,ABA}

  • 后缀集合:

    如:ABAB 的后缀(不包含字符串本身)集合 { BAB,AB,B }

  • PMT值: 前缀、后缀两个集合的交集元素中最长元素的长度。

    如:先求 {A,AB,ABA}{ BAB,AB,B } 的交集,得到集合 {AB} ,再得到集合中最长元素的长度, 所以 ABAB 字符串的 PMT 值是 2

一通前缀、后缀、交集概念说完后,但其结论很简单:仅当共同匹配成功的字符串,其最前面最后面有相同的部分时,方可以减少短指针的移动量。当相同部分越多,短指针移动的量就越小。

bf106.png

这里就有 2 个问题又摆在面前。

  • 如何知道已经匹配成功的字符串中有公共的前缀后缀以及最大相同长度值。
  • 如何根据最大PMT值修正短指针的位置。

如上的 2 个问题,便是KMP 算法的核心。KMP会把这些信息存储中 部分匹配表(PMT:Partial Match Table)中,修改短指针的位置便是根据这个表中数据。

4.2 PMT 的计算

KMP 算法中 的 “部分匹配表(PMT)” 是怎么计算出来的?

如前面图示,原始字符串和模式字符串逐一比较时,前 4 位即 ABAB 是相同的,而 ABAB 存在最大长度的前缀和后缀 ‘AB’ 子串。意味着下一次比较时,可以直接让模式字符串的前缀和原始字符串中已经比较的字符串的后缀对齐。

这些信息都是从PMT表中获取。所以,KMP 算法的核心是得到 PMT 表。

bf103.png

现使用手工方式计算 ABABCAPMT 值:

  • 当仅匹配第一个字符 A 时,A 没有前缀集合也没有后缀集合,所以 PMT[0]=0,短指针要移到模式字符串的 0 位置。

  • 当仅匹配前二个字符 AB 时,AB的前缀集合{A},后缀集合是{B},没有交集。通俗理解,AB不存在前后相同部分。所以 PMT[1]=0,短指针要移到模式字符串的 0 位置。

  • 当仅匹配前三个字符 ABA 时,ABA 的前缀集合{A,AB} ,后缀集合{BA,A},交集{A}。所以 PMT[2]=1,短指针要移到模式字符串 1 的位置。

  • 当仅匹配前四个字符 ABAB 时,ABAB 的前缀集合 {A ,AB,ABA },后缀集合{BAB,AB,B},交集{AB},所以 PMT[3]=2,短指针要移到模式字符串 2 的位置。

  • 当仅匹配前五个字符 ABABC 时,ABABC 的前缀集合{ A,AB,ABA,ABAB },后缀集合{ C,BC,ABC,BABC },没有交集,所以PMT[4]=0,短指针要移到模式字符串的 0 位置。

  • 当全部匹配后,意味着匹配成功。所以 PMT[5]=0

bf12.png

其实在 KMP 算法中,没有直接使用 PMT 表,而是引入了next 数组的概念,next 数组中的值是 PMT 的值向右移动一位。

bf13.png

KMP算法实现: 先不考虑 next 数组的算法,暂且用上面手工计算出来的值作为 KMP 算法的已知数据。

#include <iostream>
using namespace std;
int kmp(string src_str,string sub_str) {
	// next 数组
	int p_next[] = {-1, 0, 0, 1, 2, 0};
	// 指向原始字符的第一个位置
	int long_index = 0;
	// 指向模式字符串的第一个位置
	int short_index = 0;
	// 原始字符串的长度
	int src_str_len = src_str.size();
	// 模式字符串的长度
	int sub_str_len = sub_str.size();
	// 查询条件
	while (long_index < src_str_len && short_index < sub_str_len) {
		// -1 是一个神奇的存在,能保证在没有任何一个比较成功时,也能让长指针向前移动至少一步。
		if (short_index == -1 || src_str[long_index] == sub_str[short_index]) {
			long_index ++;
			short_index ++;
		} else {
             //修正短指针
			short_index = p_next[short_index];
		}
	}
	if (short_index == sub_str_len)
		return long_index - short_index;
	return -1;
}
//测试
int main(int argc, char** argv) {
	string srcStr="ABABABCAEF";
	string subStr="ABABCA";
	int res= kmp(srcStr,subStr);
	cout<<res;
	return 0;
}

上面的代码没有通用性的,现在实现求解 netxt 数组的算法。

next 也可以认为是一个字符串匹配过程,只是原始字符串和模式字符串都是同一个字符串。可以认为是自己的尾部和自己的头部比较,找出相同的部分有多少。

  • 当仅匹配一个 A。没前缀和后缀。则 PMT[0]=0,next[0]=-1

  • 当匹配 2 个。如下图,前缀、后缀没有相同的部分。则 PMT[1]=0,next[0]=0

bf107.png

  • 当匹配3 个。如下图,其前缀、后缀有一个字符 A相同。则 PMT[2]=1,next[0]=0

bf108.png

  • 当匹配4 个。如下图,其前缀、后缀 AB字符串相同。则 PMT[3]=2,next[0]=1

bf109.png

  • 当匹配5 个。如下图,其前缀、后缀没有相同。则 PMT[4]=0,next[0]=2

bf110.png

  • 全部匹配,表示程序匹配成功。PMT值为 0

编码实现:

// 求解 next 的算法
int* getNext(string patter) {
	int mLen = patter.size();
	int* pnext=new int[mLen];
	for(int k=0; k<mLen; k++)pnext[k]=-1;
	int i=0;
	int j=-1;
	while (i < mLen - 1) {
		if (j == -1 || patter[i] == patter[j]) {
			i += 1;
			j += 1;
			pnext[i] = j;
		} else
			j = pnext[j];
	}
	return pnext;
}
//测试
int main(int argc, char** argv) {
	string srcStr="ABABABCAEF";
	string subStr="ABABCA";
	int * n= getNext(subStr);
	for(int i=0; i<subStr.size(); i++) {
		cout<<*(n+i)<<"\t";
	}
	return 0;
}

bf111.png

KMP算法的时间复杂度可以达到 O(m+n)。但是,如果模式字符串中不存在相同的前缀和后缀时,时间复杂度接近BF算法。

5. 总结

字符串匹配算法除了上述几种外,还有 Sunday等算法。

从暴力算法开始,其它算法都是在尽可能减少计算次数,从而提高运行速度。

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

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

相关文章

Docker搭建SonarQube服务 - Linux

Docker搭建SonarQube服务 - Linux 本文介绍如何在Linux服务器上使用docker简便并快速的搭建SonarQube服务。 参考文档&#xff1a; Prerequisites and Overview&#xff5c;SonarQube Docs Installing SonarQube from the Docker Image | SonarQube Docs 本文使用的镜像版本…

假期来临,Steam内容文件锁定怎么办?

忙忙碌碌又一年&#xff0c;春节假期终于进入倒计时了&#xff01;已经能想象到Steam将迎来一波玩家的狂欢。 不过小编想起不少Windows用户反映过的一个问题&#xff1a;Steam更新游戏时不断收到报错&#xff0c;提示内容文件锁定&#xff0c;怎么办&#xff1f; 为了不妨碍大…

研发与环境的那些事儿

文章目录影响开发效率的环境问题研发需要的环境环境的演变测试单体环境到多环境的演变单体环境上线流程多环境上线流程提供高效研发环境环境是开发工作的核心步骤之一&#xff0c;对研发的开发测试是有影响的。研发与环境之间的关系是非常重要的&#xff0c;研发环境的质量直接…

完美解决了报错:app.js:249 Uncaught TypeError: Cannot redefine property: $router

场景&#xff1a; 项目打包优化阶段&#xff0c;为了解决打包成功后&#xff0c;单文件体积过大的问题 &#xff0c;可以通过 webpack 的 externals 节点&#xff0c;来配置并加载外部的 CDN 资源 原因&#xff1a;报错的原因就是重新定义了$router&#xff0c;因为在项目中安装…

分享134个ASP源码,总有一款适合您

ASP源码 分享134个ASP源码&#xff0c;总有一款适合您 134个ASP源码下载链接&#xff1a;https://pan.baidu.com/s/1eZwPKoGGSnzItVBM3_Z77w?pwdxvqz 提取码&#xff1a;xvqz 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&am…

详细搞懂vue2.0 3.0区别-summernote富文本使用

文章目录背景介绍必备知识实操安装回退脚手架vue cli版本vue-使用summernote富文本功能&#xff08;不失效版&#xff09;小知识如果感觉有用点个关注&#xff0c;一键三连吧&#xff01;蟹蟹&#xff01;&#xff01;&#xff01;背景 一开始只是准备实现summernote富文本&…

Stitch it in Time: GAN-Based Facial Editing of Real Videos翻译

代码地址 论文下载 摘要 生成对抗网络在其潜空间内编码丰富语义的能力已被广泛用于面部图像编辑。然而&#xff0c;事实证明&#xff0c;在视频上复制他们的成功具有挑战性。高质量的面部视频集是缺乏的&#xff0c;在视频上存在一个需要克服的基本障碍——时间一致性。我们认…

【Leetcode面试常见题目题解】6. 电话号码的字母组合

题目描述 本文是LC第17题&#xff0c;电话号码的字母组合&#xff0c;题目描述如下&#xff1a; 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意…

智能遍历测试在回归测试与健壮性测试的应用

首先来看业界用的较早也是经常听过的一款工具—— Monkey。这是 Android 官方提供的一个工具。谷歌原本设计这款工具是为了对 App 进行压力测试的。谷歌早期在设计 Android 的时候&#xff0c;Android 需要响应滑动、输入、音量、电话等事件&#xff0c;早期 activity 设计不完…

常见的 5 种 消息队列 使用场景

消息队列中间件是分布式系统中重要的组件&#xff0c;主要解决应用耦合&#xff0c;异步消息&#xff0c;流量削锋等问题。 实现高性能&#xff0c;高可用&#xff0c;可伸缩和最终一致性架构。 使用较多的消息队列有 RocketMQ&#xff0c;RabbitMQ&#xff0c;Kafka&#xf…

Android实战场景 - 输入手机号、银行卡号、身份证号时动态格式化

在日常项目开发中&#xff0c;如果稍微严谨点的话&#xff0c;其中关于手机号、银行卡号、身份证号的输入格式有做了限制格式化操作&#xff0c;主要是为了给用户带来更好的体验感&#xff1b; 最近同事正好问到了我这个问题&#xff0c;虽然以前做过这类型功能&#xff0c;但是…

你了解RTK技术吗?—— 揭秘GNSS中的定位技术

上期文章中我们一起探讨了GNSS仿真及其对测试验证的重要意义&#xff0c;今天我们将一起走进GNSS中的定位技术—RTK技术。什么是RTK技术&#xff1f;传统RTK技术与网络RTK技术又有什么区别呢&#xff1f;随着GNSS系统的迅速发展&#xff0c;RTK技术由于可以在作业区域内提供实时…

OpenMLDB v0.7.0 发布

2023 新年伊始&#xff0c;OpenMLDB v0.7.0 正式发布。本次版本更新重点增强了易用性和稳定性&#xff0c;下文将详细介绍主要改进和更新内容。更多 0.7.0 版本内容详见链接&#xff1a;Release v0.7.0 4paradigm/OpenMLDB 系统性改进消息和错误码&#xff0c;提升易用性 在…

【数据库概论】第二章 关系数据库

第二章 关系数据库 目录第二章 关系数据库2.1 关系数据结构2.1.1关系2.1.2关系模式2.1.3关系数据库2.2 关系操作2.2.1 基本的关系操作2.2.2关系数据语言的分类2.3 关系的完整性2.3.1 实体完整性2.3.2 参照完整性2.3.3 用户定义的完整性2.4 关系代数2.4.1 传统集合运算2.4.2 专门…

multimodal remote sensing survey 遥感多模态综述阅读

遥感多模态 参考&#xff1a;From Single- to Multi-modal Remote Sensing Imagery Interpretation: A Survey and Taxonomy Keywords&#xff1a;multimodal remote sensing 文章目录遥感多模态AbstractIntroductionTaxonomy1. Multi-source Alignment1.1 Spatial Alignment1…

《MySQL系列-InnoDB引擎15》慢查询日志拓展-如何开启MySQL慢查询日志?

慢查询日志拓展-如何开启MySQL慢查询日志&#xff1f; 1.查看MySQL慢查询日志是否开启&#xff1f; show variables like %query%; 查询出的结果中&#xff0c;主要观察如下三条&#xff1a; long_query_time 通过long_query_time设置阈值&#xff0c;设置阈值后&#xff0c…

Linux学习笔记 超详细 0基础(中)

Vi/Vim编辑器在Linux下一切皆文件&#xff0c;Vi编辑器和Vim编辑器是可以直接对文本文件进行编辑和操作&#xff0c;没什么大区别&#xff0c;vim有颜色区分更美观&#xff0c;vim 文件路径文件名即可进入一般模式&#xff0c;一般模式就是只读文件&#xff0c;不可进行操作。V…

K8s: Windows 下安装 K8s 开源桌面面板工具 OpenLens 查看集群信息

写在前面 分享一个桌面端的 k8s 面板工具 OpenLens博文内容为 OpenLens 简单介绍和 下载安装教程。安装非常简单,感兴趣的小伙伴快去尝试吧理解不足小伙伴帮忙指正 我所渴求的&#xff0c;無非是將心中脫穎語出的本性付諸生活&#xff0c;為何竟如此艱難呢 ------赫尔曼黑塞《德…

《c++ primer》第三章 字符串、vector、数组

前言 本章内容相比第二章要简单不少&#xff0c;里面比较重要的内容主要是vector和迭代器&#xff0c;这里只是很简单的介绍了一下&#xff0c;在后续的章节会有更详细、复杂的说明。以下记录的都是比较重要或者易混淆的知识点&#xff0c;对于像string、vector只列举了部分方法…

Sentienl一:下载,启动

Hystrix &#xff1a;1需要自己搭建监控平台 2 没有一套web界面可以给我们进行更加细粒度化的配置流控&#xff0c;速率控制 服务熔断&#xff0c;服务降级 Sentinel: 1 单独一个组件&#xff0c;可以独立出来 2 直接界面化的细粒度统一配置 一&#xff1a;丰富的应用场景&…