【算法】KMP

本期博客来介绍字符串匹配算法:KMP算法 

在开始KMP算法讲解之前,我们先来看看KMP算法的基础BF算法

一、BF算法

BF算法,即暴力(Brute Force)算法,是普通的模式匹配算法,BF算法的思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果:

该算法比较简单,如果设目标串长度为N,模式串长度为M,其时间复杂度为O(N*M),并不理想

二、KMP算法

2.1 KMP算法的思路

KMP(Knuth-Morris-Pratt)算法是一种字符串匹配算法,用于在一个主文本串中查找一个模式串的出现位置。其核心思想是利用已经匹配过的信息,尽量减少不必要的比较次数,从而提高匹配效率。

KMP算法的关键在于构建一个部分匹配表(Partial Match Table),也称为失配函数(Failure Function)或Next数组,用于指导匹配过程中的跳转操作。部分匹配表记录了模式串中每个位置的最长公共前后缀的长度。

2.1.1 next数组的引出

我们先来看一个例子:

 我们先按BF算法的思路来匹配两个字符串:

发现匹配到i和j的下标到5时出现了失配的情况,但是下面我们不将i下标进行回退,而是将j下标回退到一个合理的位置继续和i下标进行匹配

那j回退到那个下标合理呢?我们下面来分析一下:

我们可以看到在匹配过的模式串序列中有两个子串是相同的:T[0]-T[4]和T[3]-T[4]:

那既然匹配过的模式串序列中有两个子串是相同的,那匹配过的目标串中必然也有子串和模式串中的子串是相同的:

那我们不如让j回退到2下标来继续和i指向的位置继续比较:

可以发现,j回退的位置就是相同子串的长度!

由上面的例子可以知道,我们可以找到模式串匹配过的序列中的最长公共前后缀的长度k,然后让j回退到k位置再继续进行匹配

因此当匹配时出现了失配,模式串中失配位置的下标都对应着一个回退值,所以我们需要构建一个数组,来存储模式串中每个位置发生失配将要回退到的下标值(我们将其称为next数组);例如上述例子中next数组的5号位元素值为2,即模式串中五号位元素发生失配j回退到2位置

2.1.2 next数组的计算

因此我们需要在匹配之前算出next数组中的每个值k

具体计算方法为:

找到匹配成功部分的两个相等的真子串(不包含本身):一个子串是从0下标开始,以j-1下标相同的字符结尾;另一个子从串0下标相同的字符开始,以j-1下标结尾。

找到子串的长度就是我们所要的k值。(在这里默认next[0] = -1;next[1] = 0,有的教材中会默认next[0] = 0;next[1] = 1)

例如下面例子中模式串对应的next数组为:

我们从next数组中发现一个规律:如果next数组中有连续的元素在增长(递增),那后一个元素一定是前一个元素加1;

那为什么会这样呢?下面来分析一下:

假设next数组中第i个元素为k,那么就一定有模式串T中从0到k-1元素之间形成的字符串,与x(未知)到i-1元素之间形成的字符串是相同的;即T[0]—T[k-1] = T[x]—T[i-1]

由于两个相同的字符串元素个数时相同的,我们可以得出k-1-0=i-1-x;即x=i-k

那上面推导出的公式可以写为:T[0]—T[k-1] = T[i-k]—T[i-1]

如果这时T[i]==T[k],显然我们可以得出T[0]—T[k] = T[i-k]—T[i],即next[i+1]=k+1

从上面的推导中我们可以清楚的看到:如果next[i]=k且T[i]=T[k],则next[i+1]=k+1

那如果T[i]!=T[k]呢?我们后面要怎么计算下一个next[i+1]呢?

来看到下面的例子:

这时出现了T[k]!=T[i]的情况,我们将k重新赋值回退一下:k=next[k],这时k的值就变为了0,再来对比T[k]是否与T[i]相等,如果相等就可以确定next[i+1]=k+1,如果还是不相等的话就一直回退,直到T[k]=T[i]或者k的值为-1为止:

2.2 KMP算法的实现

下面是用C++实现的KMP算法:

#include<iostream>
#include<string>
#include<vector>

using namespace std;

void GetNext(const string& T, vector<int>& next)
{
	int len = T.size();
	if (len >= 1)//防止模式串长度小于等于2
	{
		next[0] = -1;
		if (len >= 2)
		{
			next[1] = 0;
		}
	}
	
	int k = 0, i = 1;
	while (i < len - 1)//计算剩下的每个位置的next数组
	{
		if (k == -1 || T[i] == T[k])
		{
			next[i + 1] = k + 1;
			++i;
			++k;
		}
		else
		{
			k = next[k];
		}
	}
}

int KMP(const string& S, const string& T, int pos = 0)//S为目标串,T为模式串,pos是S中开始比较的位置
{
	int lenS = S.size(), lenT = T.size();
	if (lenS == 0 || lenT == 0 || pos < 0 || pos >= lenS)
		return -1;

	if (lenT > lenS - pos)
		return -1;

	vector<int> next(lenT);
	GetNext(T, next);

	int i = pos, j = 0;//i遍历目标串,j遍历模式串
	while (i < lenS && j < lenT)
	{
		if (j == -1 || S[i] == T[j])//当j为-1时表示next数组使其回退到了模式串的首元素,这时直接++就好(从模式串的首元素重新开始匹配)
		{
			++i;
			++j;
		}
		else
		{
			j = next[j];
		}
	}
	if (j >= lenT)
	{
		return i - j;//返回匹配成功的目标串中第一个元素的位置
	}
	return -1;
}

2.3 next数组的优化

我们下面来看到一个例子:

在这个例子中当‘e’与‘f’不匹配时,我们按照next数组进行回退会发现:每次回退进行匹配的字符都是'x',这么多次的回退是没有意义很浪费时间的,我们何不如让模式串直接回退到首元素再继续比较呢?

下面就有了nextval数组,该数组的计算是这样的:该数组的首元素还是-1,nextval第i个位置的元素要看next[i]的值指向的位置k的字符是否与i位置的字符相同:如果相同nextval第i个位置的元素就为nextval第k个位置的值(即nextval[i]=nextval[next[i]]);如果不相同nextval第i个位置的元素就为next第i个位置的值(即nextval[i]=next[i]):

下面我们来优化一下代码:

2.4 KMP算法的优化

#include<iostream>
#include<string>
#include<vector>

using namespace std;

void GetNextval(const string& T, vector<int>& nextval)
{
	int len = T.size();
	nextval[0] = -1;

	int k = 0, i = 1;
	while (i < len - 1)//计算剩下的每个位置的nextval数组
	{
		if (k == -1 || T[i] == T[k])
		{
			++i;
			++k;
			if (T[i] == T[k])
			{
				nextval[i] = nextval[k];
			}
			else
			{
				nextval[i] = k;
			}
		}
		else
		{
			k = nextval[k];
		}
	}
}

int KMP(const string& S, const string& T, int pos = 0)//S为目标串,T为模式串,pos是S中开始比较的位置
{
	int lenS = S.size(), lenT = T.size();
	if (lenS == 0 || lenT == 0 || pos < 0 || pos >= lenS)
		return -1;

	if (lenT > lenS - pos)
		return -1;

	vector<int> nextval(lenT);
	GetNextval(T, nextval);

	int i = pos, j = 0;//i遍历目标串,j遍历模式串
	while (i < lenS && j < lenT)
	{
		if (j == -1 || S[i] == T[j])//当j为-1时表示nextval数组使其回退到了模式串的首元素,这时直接++就好(从模式串的首元素重新开始匹配)
		{
			++i;
			++j;
		}
		else
		{
			j = nextval[j];
		}
	}
	if (j >= lenT)
	{
		return i - j;//返回匹配成功的目标串中第一个元素的位置
	}
	return -1;
}

2.5 KMP算法的时间复杂度

我们设目标串S长度为N,模式串t的长度为M。求next数组的时间复杂度为O(m),因后面匹配中主串不回溯,比较次数可记为N,所以KMP算法的总时间复杂度为O(M+N),空间复杂度记为O(m)。相比于BF算法时间复杂度O(m*n),KMP算法速度的提升是非常大的

  • 9
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

1e-12

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值