KMP 算法详解(C++ Version)

news2025/1/11 18:41:08

KMP 算法详解(C++ Version)

  • 简述
  • 字符串匹配问题
  • Brute-Force 算法
  • Brute-Force 算法的改进思路
  • 跳过不可能成功的字符串比较
  • next 数组
  • 利用 next 数组进行匹配
  • 快速求 next 数组

简述

KMP 算法是一种字符串匹配算法,可以在 O(n+m) 的时间复杂度内实现两个字符串的匹配。

字符串匹配问题

所谓字符串匹配,是这样一种问题:字符串 P 是否为字符串 S 的子串?如果是,它出现在 S 的哪些位置?”

其中 S 称为主串;P 称为模式串。下面的图片展示了一个例子。

在这里插入图片描述

主串是莎翁那句著名的 “to be or not to be”,这里删去了空格。

“no” 这个模式串的匹配结果是:“出现了一次,从 S[6] 开始”;

“ob” 这个模式串的匹配结果是“出现了两次,分别从s[1]、s[10]开始”。

按惯例,主串和模式串都以 0 开始编号。

Brute-Force 算法

字符串匹配是一个非常频繁的任务。例如,今有一份名单,你急切地想知道自己在不在名单上;又如,假设你拿到了一份文献,你希望快速地找到某个关键字(keyword)所在的章节……凡此种种,不胜枚举。

我们先从最朴素的Brute-Force算法开始讲起。

顾名思义,Brute-Force 是一个纯暴力算法。

首先,我们应该如何实现两个字符串 s1、s2 的比较?

所谓字符串比较,就是问“两个字符串是否相等”。

最朴素的思想,就是从前往后逐字符比较,一旦遇到不相同的字符,就返回 false;如果两个字符串都结束了,仍然没有出现不对应的字符,则返回 true。

实现如下:

bool is_equal(string &s1, string &s2)
{
	if (s1.length() != s2.length())
		return false;

	for (int i = 0; i < s1.length(); i++)
		if (s1[i] != s2[i])
			return false;
	return true;
}

既然我们可以知道“两个字符串是否相等”,那么最朴素的字符串匹配算法 Brute-Force 就呼之欲出了:

  1. 枚举比较的起点 i = 0, 1, 2 … , len(s) - len(pattern);
  2. 将 s[i : i+len(pattern)] 与 pattern 作比较。如果一致,则找到了一个匹配。

现在我们来模拟 Brute-Force 算法,对主串 “AAAAAABC” 和模式串 “AAAB” 做匹配:

在这里插入图片描述

现在给出 Brute-Force 算法的实现:

vector<int> bruteForce(string &s, string &pattern)
{
	int slen = s.length(), plen = pattern.length();
	vector<int> res;
	for (int i = 0; i <= slen - plen; i++)
	{
		string tmp = s.substr(i, plen);
		if (is_equal(tmp, pattern))
			res.push_back(i);
	}
	return res;
}

我们成功实现了 Brute-Force 算法。现在,我们需要对它的时间复杂度做一点讨论。

按照惯例,记 n = len(s) 为字符串 s 的长度,m = len(pattern) 为模式串 pattern 的长度。

不难想到 Brute-Force 算法所面对的最坏情况:主串形如 “AAAAAAAAAAA…B”,而模式串形如 “AAAAA…B”。

在这里插入图片描述

每次字符串比较都需要付出 len(pattern) 次字符比较的代价,总共需要比较 len(s) - len(pattern) + 1次,因此总时间复杂度是 O(len(pattern) * (len(s) - len(pattern) + 1)),考虑到主串一般比模式串长很多,故 Brute-Force 的复杂度是 O(len(pattern) * len(s)),也就是 O(mn),这太慢了!

Brute-Force 算法的改进思路

我们很难降低字符串比较的复杂度(因为比较两个字符串,真的只能逐个比较字符)。因此,我们考虑降低比较的趟数。

如果比较的趟数能降到足够低,那么总的复杂度也将会下降很多。

在 Brute-Force 算法中,如果从 s[i] 开始的那一趟比较失败了,算法会直接开始尝试从 s[i+1] 开始比较。这种行为,属于典型的 “没有从之前的错误中学到东西”。我们应当注意到,一次失败的匹配,会给我们提供宝贵的信息——如果 s[i : i+len(pattern)] 与 pattern 的匹配是在第 r 个位置失败的,那么从 s[i] 开始的 (r-1) 个连续字符,一定与 pattern 的前 (r-1) 个字符一模一样!

在这里插入图片描述

需要实现的任务是“字符串匹配”,而每一次失败都会给我们换来一些信息——主串的某一个子串等于模式串的某一个前缀。

但是这又有什么用呢?

跳过不可能成功的字符串比较

有些趟字符串比较是有可能会成功的;有些则毫无可能。

我们刚刚提到过,优化 Brute-Force 算法的思路是 “尽量减少比较的趟数”,而如果我们跳过那些绝不可能成功的字符串比较,则可以希望复杂度降低到能接受的范围。

那么,哪些字符串比较是不可能成功的?

来看一个例子。已知信息如下:

  • 模式串 pattern = “abcabd”。
  • 和主串从 s[0] 开始匹配时,在 pattern[5] 处失配。

在这里插入图片描述

首先,利用上一节的结论,既然是在 pattern[5] 失配的,那么说明 s[0:5] 等于 pattern[0:5],即 “abcab”。

现在我们来考虑:从 s[1]、s[2]、s[3] 开始的匹配尝试,有没有可能成功?

从 s[1] 开始肯定没办法成功,因为 s[1] = pattern[1] = ‘b’,和 pattern[0] 并不相等。

从 s[2] 开始也是没戏的,因为 s[2] = pattern[2] = ‘c’,并不等于 pattern[0]。

但是从 s[3] 开始是有可能成功的——至少按照已知的信息,我们推不出矛盾。

在这里插入图片描述

带着“跳过不可能成功的尝试”的思想,我们来看 next 数组。

next 数组

next 数组是对于模式串 pattern 而言的。

pattern 的 next 数组定义为:next[i] 表示 pattern[0] ~ pattern[i] 这一个子串,使得前 k 个字符恰等于后 k 个字符的最大的 k。特别地,k 不能取 i+1(因为这个子串一共才 i+1 个字符,自己肯定与自己相等,就没有意义了)。

在这里插入图片描述

上图给出了一个例子。pattern=“abcabd” 时,next[4]=2,这是因为 pattern[0:4] 这个子串是 “abcab”,前两个字符与后两个字符相等,因此 next[4]=2;而 next[5]=0,是因为 “abcabd” 找不到前缀与后缀相同,因此只能取 0。

如果把模式串视为一把标尺,在主串上移动,那么 Brute-Force 算法就是每次失配之后只右移一位;改进算法则是每次失配之后,移很多位,跳过那些不可能匹配成功的位置。但是该如何确定要移多少位呢?

在这里插入图片描述

在 s[0] 尝试匹配,失配于 s[3] <=> pattern[3] 之后,我们直接把模式串往右移了两位,让 s[3] 对准 pattern[1]。

接着继续匹配,失配于 s[8] <=> pattern[6], 接下来我们把 pattern 往右平移了三位,把 s[8] 对准 pattern[3]。

此后继续匹配直到成功。

我们应该如何移动这把标尺?很明显,如图中蓝色箭头所示,旧的后缀要与新的前缀一致。如果不一致,那就肯定没法匹配上了!

回忆 next 数组的性质:pattern[0] 到 pattern[i] 这一段子串中,前 next[i] 个字符与后 next[i] 个字符一模一样。既然如此,如果失配在 pattern[r], 那么 pattern[0:r-1] 这一段里面,前 next[r-1] 个字符恰好和后 next[r-1] 个字符相等——也就是说,我们可以拿长度为 next[r-1] 的那一段前缀,来顶替当前后缀的位置,让匹配继续下去!

验证一下上面的匹配例子:pattern[3] 失配后,把 pattern[next[3-1]] 也就是 pattern[1] 对准了主串刚刚失配的那一位;pattern[6] 失配后,把 pattern[next[6-1]] 也就是 pattern[3] 对准了主串刚刚失配的那一位。

在这里插入图片描述

如上图所示,绿色部分是成功匹配,失配于红色部分。深绿色手绘线条标出了相等的前缀和后缀,其长度为 next[右端]。由于手绘线条部分的字符是一样的,所以直接把前面那条移到后面那条的位置。因此说,next 数组为我们如何移动标尺提供了依据。接下来,我们实现这个优化的算法。

利用 next 数组进行匹配

了解了利用next数组加速字符串匹配的原理,我们接下来用代码实现。

分为两个部分:

  1. 建立 next 数组。
  2. 利用 next 数组进行匹配。

首先是建立 next 数组。我们暂且用最朴素的做法,以后再回来优化:

vector<int> getNxt(string &pattern)
{
	int m = pattern.length();
	vector<int> nxt(m, 0);
	for (int i = 0; i < m; i++)
	{
		string sub = pattern.substr(0, i + 1);
		for (int j = sub.length() - 1; j >= 0; j--)
		{
			string prev = sub.substr(0, j);
			string suf = sub.substr(sub.length() - j, j);
			if (prev == suf)
			{
				nxt[i] = j;
				break;
			}
		}
	}
	return nxt;
}

计 m = len(pattern) 为模式串 pattern 的长度,不难发现它的时间复杂度是 O(m2)。

接下来,实现利用 next 数组加速字符串匹配。代码如下:

vector<int> kmp(string &s, string &pattern)
{
	int m = pattern.length();
	vector<int> nxt = getNxt(pattern);
	vector<int> res;
	int tar = 0; // 主串中将要匹配的位置
	int pos = 0; // 模式串中将要匹配的位置
	while (tar < s.length())
	{
		if (s[tar] == pattern[pos])
		{
			// 若两个字符相等,则 tar、pos 各进一步
			tar++;
			pos++;
		}
		else if (pos != 0)
		{
			// 失配,如果 pos != 0,则依据 nxt 移动标尺
			pos = nxt[pos - 1];
		}
		else
		{
			// pos[0] 失配,标尺右移一位
			tar++;
		}

		if (pos == pattern.length())
		{
			res.push_back(tar - pos);
			pos = nxt[pos - 1];
		}
	}
	return res;
}

如何分析这个字符串匹配的复杂度呢?按照惯例,记 n = len(s) 为字符串 s 的长度,m = len(pattern) 为模式串 pattern 的长度。

乍一看,pos 值可能不停地变成 next[pos-1],代价会很高;但显然pos值一共顶多自增 len(s) 次,因此 pos 值减少的次数不会高于 len(s) 次。由此,复杂度是可以接受的,不难分析出整个匹配算法的时间复杂度:O(n+m)。

快速求 next 数组

终于来到了我们最后一个问题——如何快速构建 next 数组。

首先说一句:快速构建 next 数组,是 KMP 算法的精髓所在,核心思想是 “pattern 自己与自己做匹配”。

为什么这样说呢?回顾 next 数组的完整定义:定义 “k-前缀” 为一个字符串的前 k 个字符; “k-后缀” 为一个字符串的后 k 个字符。k 必须小于字符串长度。 next[x] 定义为: pattern[0:x] 这一段字符串,使得 “k-前缀” 恰等于 “k-后缀” 的最大的 k。这个定义中,不知不觉地就包含了一个匹配——前缀和后缀相等。

接下来,我们考虑采用递推的方式求出 next 数组。如果next[0], next[1], … next[x-1] 均已知,那么如何求出 next[x] 呢?

来分情况讨论。首先,已经知道了 next[x-1](以下记为 now),如果 pattern[x] 与 pattern[now] 一样,那最长相等前后缀的长度就可以扩展一位,即 next[x] = now + 1。图示如下:

在这里插入图片描述

刚刚解决了 pattern[x] = pattern[now] 的情况。那如果 pattern[x] 与 pattern[now] 不一样,又该怎么办?

在这里插入图片描述

如上图,长度为 now 的子串 A 和子串 B 是 pattern[0:x-1] 中最长的公共前后缀。可惜 A 右边的字符和 B 右边的那个字符不相等,next[x] 不能改成 now+1 了。

因此,我们应该缩短这个now,把它改成小一点的值,再来试试 pattern[x] 是否等于 pattern[now]。

now 该缩小到多少呢?显然,我们不想让 now 缩小太多。因此我们决定,在保持 “pattern[0:x-1] 的 now-前缀 仍然等于 now-后缀” 的前提下,让这个新的 now 尽可能大一点。 pattern[0:x-1] 的公共前后缀,前缀一定落在串 A 里面、后缀一定落在串 B 里面。换句话讲:接下来 now 应该改成:使得 A 的 “k-前缀” 等于 B 的 “k-后缀” 的最大的 k。你应该已经注意到了一个非常强的性质——串 A 和串 B 是相同的!B 的后缀等于 A 的后缀!因此,使得 A 的 “k-前缀” 等于B的 “k-后缀” 的最大的 k,其实就是串 A 的最长公共前后缀的长度 —— next[now-1]!

请添加图片描述

来看上面的例子。当 pattern[now] 与 pattern[x] 不相等的时候,我们需要缩小 now——把 now 变成 next[now-1],直到 pattern[now]=pattern[x] 为止。

pattern[now]=pattern[x] 时,就可以直接向右扩展了。

代码实现如下:

vector<int> getNxt(string &pattern)
{
	vector<int> nxt;
	// next[0] 必然是 0
	nxt.push_back(0);
	// 从 next[1] 开始求
	int x = 1, now = 0;
	while (x < pattern.length())
	{
		if (pattern[now] == pattern[x])
		{
			// 如果 pattern[now] == pattern[x],向右拓展一位
			now++;
			x++;
			nxt.push_back(now);
		}
		else if (now != 0)
		{
			// 缩小 now,改成 nxt[now - 1]
			now = nxt[now - 1];
		}
		else
		{
			// now 已经为 0,无法再缩小,故 next[x] = 0
			nxt.push_back(0);
			x++;
		}
	}
	return nxt;
}

不难证明构建 next 数组的时间复杂度是 O(m) 的。至此,我们以 O(n+m) 的时间复杂度,实现了构建 next 数组、利用 next 数组进行字符串匹配。

以上就是 KMP 算法。它于 1977 年被提出,全称 Knuth–Morris–Pratt 算法。让我们记住前辈们的名字:Donald Knuth(K),James H. Morris(M),Vaughan Pratt(P)。

最后给出完整模板:

vector<int> getNxt(string &pattern)
{
	vector<int> nxt;
	// next[0] 必然是 0
	nxt.push_back(0);
	// 从 next[1] 开始求
	int x = 1, now = 0;
	while (x < pattern.length())
	{
		if (pattern[now] == pattern[x])
		{
			// 如果 pattern[now] == pattern[x],向右拓展一位
			now++;
			x++;
			nxt.push_back(now);
		}
		else if (now != 0)
		{
			// 缩小 now,改成 nxt[now - 1]
			now = nxt[now - 1];
		}
		else
		{
			// now 已经为 0,无法再缩小,故 next[x] = 0
			nxt.push_back(0);
			x++;
		}
	}
	return nxt;
}

vector<int> kmp(string &s, string &pattern)
{
	int m = pattern.length();
	vector<int> nxt = getNxt(pattern);
	vector<int> res;
	int tar = 0; // 主串中将要匹配的位置
	int pos = 0; // 模式串中将要匹配的位置
	while (tar < s.length())
	{
		if (s[tar] == pattern[pos])
		{
			// 若两个字符相等,则 tar、pos 各进一步
			tar++;
			pos++;
		}
		else if (pos != 0)
		{
			// 失配,如果 pos != 0,则依据 nxt 移动标尺
			pos = nxt[pos - 1];
		}
		else
		{
			// pos[0] 失配,标尺右移一位
			tar++;
		}

		if (pos == pattern.length())
		{
			res.push_back(tar - pos);
			pos = nxt[pos - 1];
		}
	}
	return res;
}

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

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

相关文章

ZYNQ程序固化

文章目录 一、简介二、固化操作2.1 生成固化文件2.2 固化到SD卡2.3 固化到Flash 参考 将程序存储在非易失性存储器中&#xff0c;在上电或者复位时让程序自动加载运行。 这个过程需要启动引导程序( Boot Loader)参与&#xff0c;Boot Loader会加载FPGA配置文件&#xff0c;以及…

读元宇宙改变一切笔记12_元宇宙+

1. 元宇宙的价值 1.1. 元宇宙的价值&#xff0c;将“超过”物理世界 1.2. 移动互联网时代不是突然降临的 1.2.1. 我们可以确定一项特定的技术是何时被创造、测试或部署的&#xff0c;但不能确定一个时代何时开始或何时结束 1.2.2. 转型是一个迭代的过程&#xff0c;在这个过…

web架构师编辑器内容-完成属性设置的优化

对于业务组件来说&#xff0c;其属性是有很多的&#xff0c;如果把所有属性都平铺在页面上&#xff0c;就会非常长&#xff0c;而且想要更改其中的某些属性&#xff0c;可能需要向下滚动很久才能找到&#xff0c;对于UI的交互不是很友好&#xff0c;需要对属性的不同特性进行分…

彩色图像处理之彩色图像直方图处理的python实现——数字图像处理

彩色图像的直方图处理是一种重要的图像处理技术&#xff0c;用于改善图像的视觉效果&#xff0c;增强图像的对比度&#xff0c;或为后续的图像处理任务&#xff08;如图像分割、特征提取&#xff09;做准备。彩色图像通常由红色&#xff08;R&#xff09;、绿色&#xff08;G&a…

2023年12月 Scratch 图形化(一级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch图形化等级考试(1~4级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 观察下列每个圆形中的四个数,找出规律,在括号里填上适当的数?( ) A:9 B:17 C:21 D:5 答案:C 左上角的数=下面两个数的和+右上角的数

【UEFI基础】EDK网络框架(UDP4)

UDP4 UDP4协议说明 UDP的全称是User Datagram Protocol&#xff0c;它不提供复杂的控制机制&#xff0c;仅利用IP提供面向无连接的通信服务。它将上层应用程序发来的数据在收到的那一刻&#xff0c;立即按照原样发送到网络。 UDP报文格式&#xff1a; 各个参数说明如下&…

Tomcat Notes: Web Security

This is a personal study notes of Apache Tomcat. Below are main reference material. - YouTube Apache Tomcat Full Tutorial&#xff0c;owed by Alpha Brains Courses. https://www.youtube.com/watch?vrElJIPRw5iM&t801s 1、Overview2、Two Levels Of Web Securi…

跨部门算法迭代需求,从提出到上线的全流程实践

文章目录 引言需求评审技术方案评审模块开发系统联调QA测试产品验收经验教训 引言 最近工作中有一个算法迭代的需求&#xff0c;我在其中作为技术侧负责人&#xff08;技术主R&#xff09;推动需求完成上线。 需求涉及多个部门&#xff0c;前后耗时接近1个月。 我第一次在这…

transdata笔记:手机数据处理

1 mobile_stay_duration 每个停留点白天和夜间的持续时间 transbigdata.mobile_stay_duration(staydata, col[stime, etime], start_hour8, end_hour20) 1.1 主要参数 staydata停留数据&#xff08;每一行是一条数据&#xff09;col 列名&#xff0c;顺序为[‘starttime’,…

Istio

1、Istio介绍 Istio 是由 Google、IBM 和 Lyft 开源的微服务管理、保护和监控框架。 官网&#xff1a;https://istio.io/latest/zh/ 官方文档&#xff1a;https://istio.io/docs/ 中文官方文档&#xff1a;https://istio.io/zh/docs Github地址&#xff1a;https://github.com…

System.Data.SqlClient.SqlException:“在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误

目录 背景: 过程: SQL Express的认识: 背景: 正在运行程序的时候&#xff0c;我遇到一个错误提示&#xff0c;错误信息如下&#xff0c;当我将错误信息仔细阅读了一番&#xff0c;信息提示的很明显&#xff0c;错误出现的来源就是连接数据库代码这块string connStr "s…

编写servlet

编写servlet 上述代码中的HTML页面将雇员ID发送给servlet。要创建servlet读取客户机发送的雇员ID并检索雇员的详细信息,需要执行以下步骤: 在“项目”选项卡中右击“Employee”节点,然后选择“新建”→Servlet。将显示“新建Servlet”对话框。在“类名”文本框中输入Employ…

【Kaggle】泰坦尼克号生存预测 Titanic

文章目录 前言案例背景数据集介绍加载数据集探索性数据分析&#xff08;EDA&#xff09;可视化特征和目标值之间关系缺失值分析 数据预处理数据清洗缺失值处理去除噪声并且规范化文本内容 数据转换 数据划分建模逻辑回归模型决策分类树模型随机森林模型梯度提升树模型 预测LR 完…

C++笔记之bool类型的隐式转换现象与应用

C++笔记之bool类型的隐式转换现象与应用 —— 《C++ Primer Plus》 文章目录 C++笔记之bool类型的隐式转换现象与应用1.C++中,有几种类型和表达式可以隐式转换为bool类型2.使用explicit关键字来声明显示转换运算符,这样只有在使用static_cast<bool>时才能将对象转换为…

SpringCloud之OpenFeign的学习、快速上手

1、什么是OpenFeign OpenFeign简化了Http的开发。在RestTemplate的基础上做了封装&#xff0c;在微服务中的服务调用发送网络请求起到了重要的作用&#xff0c;简化了开发&#xff0c;可以让我们跟写接口一样调其他服务。 并且OpenFeign内置了Ribbon实现负载均衡。 官方文档…

GEE:最小距离分类器(minimumDistance)分类教程(样本制作、特征添加、训练、精度、最优参数、统计面积)

作者:CSDN @ _养乐多_ 本文将介绍在Google Earth Engine (GEE)平台上进行最小距离分类(minimumDistance)的方法和代码,其中包括制作样本点教程(本地、在线和本地在线混合制作样本点,合并样本点等),加入特征变量(各种指数、纹理特征、时间序列特征、物候特征等),运行…

中仕教育:国考调剂和补录的区别是什么?

国考笔试成绩和进面名单公布之后&#xff0c;考生们就需要关注调剂和补录了&#xff0c;针对二者之间的区别很多考生不太了解&#xff0c;本文为大家解答一下关于国考调剂和补录的区别。 1.补录 补录是在公式环节之后进行的&#xff0c;主要原因是经过面试、体检和考察&#…

高速CAN总线 m 个节点竞争总线时 电压分析(共 n 个节点)

电路的串并联关系参考<<高速CAN总线 A C节点竞争总线时 电压分析(共ABC三个节点)>> M个节点同时发送显性电平 如下图: 由上图可以看出,上下并联的M组30Ω的等效电阻R0 &#xff08;30/m&#xff09; Ω 中间并联的电阻R1 由公式&#xff1a; 1/R1 1/120 1/120…

LV.19 D1 C++简介 学习笔记

一、C概述 1.1 C的前世今生 C是一种被广泛使用的计算机程序设计语言。它是一种通用程序设计语言&#xff0c;支持多重编程范式&#xff0c;例如过程化程序设计、面向对象程序设计、泛型程序设计和函数式程序设计等。 C的发展&#xff1a; 1.2 C的主要应用领域 C是一门运用很广…

海外抖音TikTok、正在内测 AI 生成歌曲功能,依靠大语言模型 Bloom 进行文本生成歌曲

近日&#xff0c;据外媒The Verge报道&#xff0c;TikTok正在测试一项新功能&#xff0c;利用大语言模型Bloom的AI能力&#xff0c;允许用户上传歌词文本&#xff0c;并使用AI为其添加声音。这一创新旨在为用户提供更多创作音乐的工具和选项。 Bloom 是由AI初创公司Hugging Fac…