KMP算法及C++实现

KMP算法由D.E.Knuth,J.H.Morris和V.R.Pratt提出,因此称为KMP算法。KMP算法是一种用于字符串匹配的算法,能够从一个主串中快速找出需要的模式串。相比于暴力法,KMP算法在时间复杂度上有很大的改善。暴力法在匹配失败时单纯地往前进一格再重新开始匹配,而KMP算法通过先前生成的next数组,在匹配失败利用该数组记录的信息尽量往前跳,避免不必要的匹配,从而节省时间。网络上介绍该算法的优质文章很多,但我这里还是想自己记一下笔记,写一些自己的理解,班门弄斧,还请谅解。

基本原理

对于某个字符串,我们称为主串,我们想知道里面是否包含另一个指定的字符串,如果有就找出来,我们称这个想找的字符串为模式串,要解决这一问题的方法是不断得将模式串和主串进行匹配。我们先让两个字符串头部对其开始匹配,所谓暴力法,就是在匹配失败时,我们就把模式串往前移动一个字符位置,让模式串的第一个字符与主串的第二个字符对齐,再进行匹配,以此类推直到匹配成功或者失败,匹配到模式串剩下的长度明显不够了就说明失败了。暴力法的时间复杂度较高,若主串长度为m,模式串长度为n,显然时间复杂度为O(mn)。

使用KMP算法,可以实现O(m+n)的时间复杂度。KMP算法先不急着匹配,先让模式串进行“自我认知”(这个词我自己瞎编的),即自己和自己匹配。要从别人身上找共同点,先要更好得了解自己。模式串通过“自我认知”,得到一个next数组,KMP算法最核心的概念就是next数组,这个概念很难用简短的语言描述。要明白next数组的含义之前,首先要明确模式串上有什么信息值得去发掘,可以帮助加速匹配。这里要提到一个最长公共前后缀的概念

字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串,后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。所谓最长公共前后缀,顾名思义,就是同时处于一个字符串的前缀和后缀中且最长的那个序列(或许直接理解词义比看我这段话还好理解)。用一个具体的例子来理解最长公共前后缀的概念:
来看一个简单的序列:“ABAABA”
这个序列的前缀有:A、AB、ABA、ABAA、ABAAB
这个序列的后缀有:A、BA、ABA、AABA、BAABA

可以看到公共的前后缀有A和ABA两个,最长公共前后缀就是ABA了。

知道这一信息可以带来什么好处?看下面一个简单的例子。
主串和模式串在匹配到位置6处的字符时发现不匹配,如果是暴力法那么模式串前进一格。但此时我们其实知道了模式串前面部分"ABAABA"是和模式串前面部分匹配的,那么模式串对应部分也是"ABAABA",这个时候我们可以利用事先得到的匹配部分"ABAABA"的最长公共前后缀信息,移动模式串使得前缀位置到后缀位置,即往前移动3格。显然仅仅移动一格或是两格是不能匹配的。模式串自己和自己已经匹配过了,知道"ABAABA"的最长公共前后缀是"ABA",要想能够成功匹配,只有让前缀"ABA"移动到后缀"ABA"处才有这个可能。之后我们知道模式串前缀"ABA"和主串中位置3-5的元素已经匹配上了,因此直接继续往后匹配就行了。

这里我们可以这样想,移动一格其实就是让前缀"BAABA"和后缀"ABAAB"对齐,移动两格就是让前缀"AABA"和后缀"ABAA"对齐,显然是对不齐的,我们通过让模式串自我匹配事先得到最长公共前后缀信息,省去这些不必要的匹配。
在这里插入图片描述
这里再思考一个问题,为何要最长
这里还是看一下例子。我个人感觉是虽然想加速匹配,但不想因为速度太快而错过成功的可能。字符串"ABAABA"中,"A"也是公共前后缀,如果用该信息进行匹配,那就要把模式串往前移动到使得前缀"A"落在后缀"A"的位置,这显然是不对的,其错过了成功匹配的机会。
在这里插入图片描述
next数组

对于最长公共前后缀,我们没有必要知道它到底是什么,我们只需要知道它的长度就可以帮助进行移位了,比如上述例子,我们只要知道最长公共前后缀的长度是3,我们就能移动模式串,安排模式串位置3处的节点和主串匹配失败的位置处,即位置6处的节点对齐。由此我们要记录这个最长公共前后缀长度信息,使用next数组进行记录。

next数组是一个一维整型数组,next[i]表示字符串前 i 个字符组成的字符串的最长公共前后缀的长度。注意我这里数组从位置 0 开始,有的喜欢从位置 1 开始,位置 0 存放数组长度。

还是针对模式串"ABAABAB"为例,见图。首先next[0]和next[1]都是0,因为不存在前0个字符组成的字符串,前1个字符组成的字符串不存在什么前后缀。正式计算从位置2开始,由于"AB"的最长公共前后缀长度是0,即没有公共前后缀,因此next[2] = 0,之后"ABA"最长公共前后缀是"A",next[3] = 1,以此类推。
在这里插入图片描述
next数组算是这样算的,然而之所以叫做next数组,还是因为它能指示我们如何移动模式串到下一个(next)位置。比如模式串位置6处匹配失败,我们移动模式串,让模式串位置3的字符移动到这里。

理解了next数组可以说是完成了理解KMP算法的一小步,后面还有一步是关于next数组如何求解的问题,如果求解next数组的方法比较复杂,则依然会引起比较高的时间复杂度。

这里先记录C++代码。

求解next数组的C++代码

void GetNext(string &str, vector<int> &next){
	next[0] = -1;
	int i = 0, j = -1;

	while (i < str.size()-1){//前一位比较完就行了,最后一位比的话就越界了
		if (j == -1 || str[i] == str[j]){
			++i;
			++j;
			next[i] = j;
		}	
		else{
			j = next[j];
		}
	}
}

注意这里为了方便后续的 j 值计算直接让next[0] = -1了。

网上有许多博客对 next 数组的求解做了很详细的讲解,因此这里我仅仅谈谈我自己的理解。前文已经提到,在匹配模式串和主串对模式串的自我进行认知,实质上是一个自我匹配的过程,这里使用双指针的方式完成这个过程。当 str[i] = str[j] 的时候,其实就相当于一次字符的匹配成功,用语言很难描述,还是看图。注意我在前面的实例模式串后面加个了字符’C’,没办法,一开始没想到好的示例。

当 i = 3,j = 0 时,显然满足条件,因此两个指针都后移一位,注意这个时候其实就找出了字符串"ABAA"的最长公共前后缀的长度,显然 j+1 就是这个长度了。其实这里就是在 j 指示前缀, i 指示后缀,j 不仅仅指示前缀的位置,还指示前缀的长度。移位以后再进行比较,依旧相等,此时就相当于找出了字符串"ABAAB"的最长公共前后缀的长度,以此类推。

在这里插入图片描述

直到 i = 6, j = 3,这个时候就出问题了,所指向的两个字符并不相等,怎么办呢。这里我们要让 j 回退,其实就是让下面的串前进,那么应该怎么前进呢?

这里其实我们知道 j 前面的三个字符与 i 前面的三个字符是匹配的,那么我们就可以用已经匹配的这部分字符串,即"ABA",用其最长公共前后缀的长度来指示移位,而这一信息早就算出来了,就是next[3]!(感觉有点动态规划的思想)

因此,在遇到字符不匹配时,我们就让 j = next[j],让 j 回退的实质就是让字符串前进。
在这里插入图片描述
这里可以思考一下为什么 next[0] 本应该是 0 确设置成 -1。以下图为例,如果在 j = 0的 情况下就不匹配了,这个时候要移动字符串,我们想要的效果是 i = 2, j = 0,如果next[0] = 0,则 j = next[0] = 0,就是原地踏步,那么后面 i 就要单独自增,这样就要加一层逻辑,判断之前是否发生回退,由此判断 i 是单独自增还是 i,j 都自增,因为广从 j = 0 我们并不能知道之前有没有回退。如果能让next[0] = -1,就没有这个麻烦,就相当于让 j 先后退一步,之后就可以一起自增。
在这里插入图片描述

KMP算法编码
最后还有一步就阶段性胜利了,就是把整个过程理清楚,编码。
整个过程其实很清楚的,先针对模式串计算 next 数组,再遍历主串进行匹配,匹配失败按照next数组所提供的信息移动模式串。可以看到 KMP 算法匹配过程的代码和求解 next 数组的代码极为相似。

int KMP(string &str1, string &str2)//str1表示主串,str2表示模式串
{
	int m = str1.size(), n = str2.size();
	vector<int> next(n);
	GetNext(str2, next);//求解next数组
	int i = 0, j = 0;
	while (i < m && j < n) {
		if (j == -1 || str1[i] == str2[j]) {
			++i;
			++j;
		}
		else {
			j = next[j];
		}
	}
	if (j == n) {
		return i - j;//返回模式串在主串的起始位置
	}
	else {
		return -1;//没有找到返回-1
	}
}

了解了KMP算法的原理,也能理解其时间复杂度为O(m+n)。因为KMP算法先对模式串进行一次顺序遍历,后面正式匹配时对主串进行一次顺序遍历,主串和模式串的长度分别为 m 和 n ,因此其时间复杂度为 O(m+n) 。

尾声:这个东西确实比较难以理解,至少是对我来说,因此我的表述也可能有很多不妥的地方,回过头再看几个技术博客感觉别人的笔记都写得不错,我的水平还有待提高。另外貌似还有改进的KMP算法,这里先就不写了。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值