KMP算法详解及C++实现

一、介绍KMP算法解决的问题

KMP算法实际上解决的是一个字符串匹配的问题,即从一个目标字符串(通常非常长)中找到与给定字符串(也称为模式串)相匹配的字串的位置,例如:
在这里插入图片描述

如果用人脑去找,很容易找出模式串在目标串出现的位置有第5个第21个,但是当目标串非常长的时候,显然人脑搜索就不太现实,那么如何来找呢?

首先我们想到的第一个方法就是暴力搜索,即一个一个地把目标串和模式串从头匹配到尾

第一轮对比在匹配到第5个时发现不匹配,即模式串的A和目标串的B不同,那么就进入下一轮对比,把模式串整个后移一位,即

在这里插入图片描述

然后继续从模式串的第一位开始比对,这样一轮一轮地匹配,直到搜索到目标串的结尾,但是很显然,这样做的效率太低了,接下来介绍的KMP算法将会大大提升搜索的效率

二、KMP算法的理论理解

KMP算法的核心思想:利用已匹配的信息来减少回溯的次数
那么如何利用已匹配的信息来减少回溯的次数呢?
我们先来看看什么是已匹配的信息:
在第一轮比对中,已经将前四个字符匹配完毕,即ABAB是匹配的,唯一不匹配的字符是第五个字符在这里插入图片描述
那么此时已匹配的信息就是ABAB
如果我们能让模式串在移动的过程中不再进行这步
在这里插入图片描述
而是直接向后移动到这里
在这里插入图片描述
将AB模式串的AB直接移动到目标串上可以匹配AB的部分,再去匹配后面的字符,这样无疑减少了无效比对的步骤,提高了算法的效率

那么如何实现将模式串直接移动到可以匹配一部分字符的位置呢?

我们来看第一轮比对中找到的已匹配信息:ABAB
不难发现第1个字符到第2个字符和第3个字符到第4个字符是相等的,都为AB,那么如果在第五个字符失配时直接把模式串往后移动两位,使得模式串的第1位到第2位替代原本第3位到第4位的位置,就做到了减少无效比对的次数

而这样做的原理就是模式串的第1位到第2位和第3位到第4位相同,即模式串前4位中前面2位(第1位和第2位)和后面两位(第3位和第4位)相同,那么如果我们能知道模式串中每个前缀子串(例如本例子中就是:A,AB,ABA,ABAB,ABABC)前n位和后n位相同的情况,就能在任意位置失配的时候,将模式串往前直接移动n位(这个n取决于失配的位置),从而达到快速匹配的效果了

那么接下来的工作就显而易见了,统计模式串的每一个前缀子串前n位和后n位相同的情况

首先要介绍前缀和后缀的概念:
实际上前缀和后缀就是上文中说的前n位和后n位,举个例子:
在这里插入图片描述
很显然对于这个模式串的不同前缀子串,它们的前缀后缀也是不同的,而我们要统计的就是每个前缀子串的最长相等前后缀长度,为什么是最长呢?
这是因为根据这种方法得到的匹配结果在失配位置前是能保证一定匹配的,例如第一轮匹配的中在第5位时失配,那么根据这种方法找到的下一个匹配位置一定能保证在目标串的第5位之前还能与模板串匹配的部分是一定匹配的,即在这里插入图片描述
那么最长实际上能保证在以这种方式快速推进模式串的过程中不会漏掉和模式串完全匹配的字串,因为:每个前缀子串的相等前后缀长度越长,(即子串中的重复度越高),模式串在失配时按照此方法前进的距离越短,如果在一个前缀子串中前后缀相同的最大长度为3,而我们记录的前后缀相同长度为1,那么在这个前缀子串的下一个字符失配时,在这里插入图片描述

按照此方法就会把模式串中第1位的字符匹配到前缀子串最后一个字符原本的位置,在这里插入图片描述

而实际上正确的做法是把该模式串的第3个字符匹配到最后一个字符原本的位置,在这里插入图片描述

若按照前面那种方法就可能错过匹配的子串

明白了这一点后,我们就可以整理出模式串中所有前缀子串的最大前后缀相等长度了,我们用数组的形式储存(就是所谓的next数组)在这里插入图片描述
那么我们应该如何使用这个整理出来的next数组呢?
例如在第一轮匹配中,我们发现模式串的第5位不匹配,在这里插入图片描述
那么我们只需要查询失配字符之前的前缀子串的最大前后缀长度(即失配字符的前一位对应的next数组的值)为2,然后将模式串的第2位匹配到失配字符之前的前缀子串的末位在这里插入图片描述
这样就达成了最开始期待的效果,以此类推,在之后遇到不匹配的字符时,按照这种方法来推进,从而实现快速匹配的效果,这就是KMP算法

三、KMP的代码实现(C++)

明白了KMP算法的理论部分,接下来就是用代码去实现了,首先要实现的是将统计出模式串的所有前缀子串的最大前后缀长度,也就是next数组,最容易想到的方法当然是暴力搜索,把所有前缀和后缀罗列出来,一个个去比对,找出最长的长度,但是当模式串较长时,暴力搜索的效率就显得很低下,那么要如何实现快速找到最长前后缀呢?
还是运用KMP算法的核心思想:利用已匹配的信息来减少回溯的次数
首先next数组的第一个元素一定是0(因为只有一个字符的子串连前后缀都没有,更没有相同的前后缀了),那么我们得出一个前缀子串加上下一个字符后形成的前缀子串的最大前后缀长度的方法就是根据该前缀子串的最大前后缀长度来考虑,分为两种情况:
1、最简单的情况,当前前缀子串的下一个字符和该前缀子串的第n + 1位(n为当前前缀子串的最大前后缀子串的长度)相同,那么加上这个字符后形成的新的前缀子串的最大前后缀长度就是n + 1,例如(当前前缀子串为ABA(最大前后缀长度为1),下一个字符为B)在这里插入图片描述
翻译成代码就很简单了:

if(pattern[i] == pattern[j])
{
	j++;
}

i代表新的字符,j代表最大前后缀长度,也就是上面提到的n,pattern就是模式串,由于数组下标从0开始,所以所有的第j+1位在数组中的表现形式都是next[j],以下出现的同理

2、比较复杂,也是整个算法的难点,当前前缀子串的下一个字符和该前缀子串的第n + 1位(n为当前前缀子串的最大前后缀子串的长度)不相同,这时就要往回找,直到找到和新的字符能匹配的位置,而往回找的过程也是要遵循一个基本原则:不匹配字符之前的部分一定能保证匹配,而我们在之前找到的next部分数组恰好记录了前面所有子串的最大前后缀长度,那么我们就可以利用这点来进行往回找能与新字符匹配的字符而能遵循基本原则并且快速,举个例子:在这里插入图片描述
如果X不是B那就继续按照这个规则往前找,直到找到相同的字符或者找到第一个字符(即j变成0,在代码中的体现就是&&左边的j>0),这里显然需要的是一个循环,翻译成代码就是:

while(j > 0 && pattern[i] != pattern[j])
{
	j = next[j - 1];
}

这样我们已经基本将next数组的构造过程梳理完了,最后一步就是根据这个next数组来实现快速匹配了,我们先将上面介绍过的方法搬运下来:

查询失配字符之前的前缀子串的最大前后缀长度(即失配字符的前一位对应的next数组的值)为n,然后将模式串的第n位匹配到失配字符之前的前缀子串的末位(在代码中的体现就是失配字符target[i]和第n+1个字符pattern[j]比对),翻译成代码如下:

for (int i = 0, j = 0; i < target.length(); i++)
	{
		while (j > 0 && target[i] != pattern[j])
		{
			j = next[j - 1];
		}
		if (target[i] == pattern[j])
		{
			j++;
		}
		if (j == pattern.length())
		{
			cout << "Found pattern at " << i - j + 1 << endl;
			j = next[j - 1];//这一步就是继续往后找是否还有匹配的子串
		}
	}
	return 0;
}

target是目标串
这样KMP算法就实现了,下面是完整代码和运行结果:
(用vector容器代替了原本的数组,不用手动管理内存)

#include <iostream>
using namespace std;
#include <vector>

vector<int> Next(string pattern)
{
	vector<int> next;
	next.push_back(0);	//next容器的首位必定为0
	for (int i = 1, j = 0; i < pattern.length(); i++)
	{
		while (j > 0 && pattern[j] != pattern[i])
		{ 
			j = next[j - 1];
		}
		if (pattern[i] == pattern[j])
		{
			j++; 
		}
		next.push_back(j);
	}
	return next;
}

int main()
{
	string pattern = "ABABC", target = "ABABA ABABCABAA CAADB ABABCAADKDABC";
	vector<int>next = Next(pattern);

	for (int i = 0, j = 0; i < target.length(); i++)
	{
		while (j > 0 && target[i] != pattern[j])
		{
			j = next[j - 1];
		}
		if (target[i] == pattern[j])
		{
			j++;
		}
		if (j == pattern.length())
		{
			cout << "Found pattern at " << i - j << " position" << endl;
			j = next[j - 1];
		}
	}
	return 0;
}

运行结果:
在这里插入图片描述

  • 67
    点赞
  • 285
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值