【数据结构与算法】KMP算法

KMP算法详解

1. KMP算法简介

  KMP算法是一种用于字符串匹配的算法,从一串字符串中查询,是否包含某个子串。比如

const char *a = "abcdefabcdee"; //主串
const char* b = "def"; //模式串

  b就是属于a的子串,a的起始下标为3。我们把长的串那个叫做主串,用于匹配的串叫做模式串

2. KMP算法的前身—暴力破解法

  起初人们匹配字符串使用的是所谓的暴力破解法,我们举个例子,从主串S="goodgoogle",寻找子串T="google",具体步骤为:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
  其实就是不断的将模式串与主串进行匹配,如果匹配不成功,主串回溯到原来位置的下一个位置,然后模式串回溯到最开头。具体代码为:

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

int ViolentMatch(const char* S, const char* T)
{
	int Slen = strlen(S); //主串
	int Tlen = strlen(T);  //模式串
	
	int i = 0;
	int j = 0;
	while (i < Slen &&j < Tlen)
	{
		if (S[i] == T[j])
		{
			i++; //字符串如果匹配,就继续向前搜索
			j++;
		}
		else
		{
			i = i - j + 1; //如果不匹配,主串回退搜索的长度,并且往后偏移一个位置
			j = 0; //模式串从头继续搜索
		}

		
	}

	if (j == Tlen)
	{
		return i - j; //返回匹配位置
	}
	else
	{
		return -1;
	}
}

int main()
{
	const char* a = "ABCERDS";
	const char* b = "RD";
	int n = ViolentMatch(a, b);
	cout << n << endl;
	
	return 0;
}


2.KMP算法的实现

2.1 KMP算法的思路

  一般来说,普通的暴力破解法能够满足我们的需求,但是对于一些计算量很大的搜索,就显得运算效率过低,比如在一本书中搜索一个专有名词。我们还是通过一个例子引入,说明KMP算法的优越之处。

  主串S=“abcdefgab”,模式串T = “abcdex”。如果用暴力搜索,过程为:

在这里插入图片描述

  其实这里面步骤2-5都是没有意义的。因为本身模式串的前5个字符完全不同,又和主串完全匹配,所以主串的第2-5个位置不可能有与模式串第1个位置相同的元素。所以,直接进入第6步骤即可。这就是KMP算法的高明之处。

  如果模式串中出现了重复的部分,比如主串S=“abcabcabc",模式串T="abcabx”,匹配过程为

在这里插入图片描述

  与前面相同,2和3步骤是多余的。而由于模式串中有相同的子串ab,在4和5中,主串中的ab已经在第1步骤中与模式串第二个ab成功匹配过了,所以,当模式串第一个ab过来的时候,不需要二次匹配。因此步骤4和5也是多余的。

  因此我们知道了,KMP算法的核心就是:

  • 寻找不匹配的结点位置
  • 根据模式串的特征,往后进行平移到一个合适的位置继续与主串进行比对,使得主串不需要进行指针回溯。

2.2 对模式串特征的描述

  在KMP算法中,我们需要一个很重要的东西,就是对模式串的特征描述。因为我们需要知道,当模式串与主串不匹配的时候,应该怎么把模式串移动到什么位置继续匹配

  比如模式串 T=“ABABAAA”;

  (1)如果有主串 S = “BBBBBBBBB”

主串BBBBBBBBB
模式串ABABAAA

  当主串与模式串的第一个位置不匹配的时候,模式串往后移动一位,跳过主串第一位置,继续匹配。

主串BBBBBBBBB
模式串ABABAAA

(2)如果有主串 S = “ACBBBBBBB”

主串ACBBBBBBB
模式串ABABAAA

  当第二个位置不匹配的时候,模式串往后移动一位,下标为0的元素继续与第二位匹配.

主串ACBBBBBBB
模式串ABABAAA

(3)如果有主串 S = “ABBBBBBBB”

主串ABBBBBBBB
模式串ABABAAA

当第三个位置不匹配的时候,模式串往后移动两位,下标为0的元素继续与第三位匹配.

主串ABBBBBBBB
模式串ABABAAA

(4)如果有主串 S = “ABACBBBBBB”

主串ABACBBBBBB
模式串ABABAAA

当第四个位置不匹配的时候,模式串往后移动,下标为1的元素继续与第四位匹配.

主串ABACBBBBBB
模式串ABABAAA

(5)如果有主串 S=“ABABCCCCCCCCC”

主串ABABCCCCCCCCC
模式串ABABAAAA

当第五个位置不匹配的时候,模式串往后移动,下标为2的元素继续与第五位匹配.

  其余省略,这种当模式串第n个位置不匹配的时候,下标为i的元素继续与主串原位置匹配的描述,就是对模式串特征的描述

2.3 next数组含义

  2.2个那种对模式串的特征的描述,实际上就是在找,当模式串第i个位置不匹配的时候,前i-1个元素最长公共前后缀的长度是多少。求这个长度获得的结果,就是next数组,表征某个位置不匹配的时候,模式串应该移动到哪个下标位置继续与主串匹配。比如上面的模式串ABABAAA,这个模式串的next数组就是

ABABAAA
-1001211

  举例说明,比如下标为4的元素对应的最长公共前后缀长度是2。这是因为,下标为0-3的元素是ABAB,这个子串的前缀可以是A,AB,ABA;这个子串的后缀可以是B,AB,BAB,可以看出,最长公共前后缀为AB,长度为2,就填入下标4的位置。也就是标识,当下标为4的元素与主串不匹配的时候,应该从下标为2的元素继续开始与主串进行匹配。

  这里需要对-1进行解释,因为一旦第一个位置不匹配,主串的相应位置就会被跳过,由下一个元素继续与模式串的下标为0的元素进行匹配,为了标记这种不同的跳过操作,所以使用-1作为标记。

2.4 next数组的求法

  求next数组有比较巧妙的方法。我们并不直接求next[j]的最长公共前后缀,而是利用next[j-1]来求next[j]。

  比如T=ABAB最长公共前后缀是AB,假设前缀最后一个下标k,也就是T[k]=B;后缀最后一个下标是i,即T[i]=B,最长公共前后缀为2。

  (1)如果,T[k+1]=T[i+1],就比如T = ABABA,如果前缀的下一个元素和后缀的下一个元素继续匹配,则这个串的最长公共前后缀数会比原来+1。

  即T[k+1]=T[i+1]的时候,next[i+2]=next[i+1]+1

ABABAE
23

也就是next[5]=next[4]+1

  (2)如果T[k+1]!=T[i+1]会怎么样呢?比如下面这个

index0123456789101112
TABCABDABCABEF
next-100012012345

  对于next[11]来说前缀ABCAB和后缀ABCAB匹配,也就是T[k]=T[4],T[j]=T[10],而T[k+1]=D,T[j+1]=E,二者并不相等。这个时候如果要求next[j+1],我们应该查找next[K+1],如果不为0,说明0-k部分还有更小一级的对称的部分可以继续考虑。
n e x t [ j + 1 ] = n e x t [ k + 1 ] next[j+1]=next[k+1] next[j+1]=next[k+1]
  在这里就是继续比对

T [ n e x t [ k + 1 ] ] = = T [ i + 1 ] T[next[k+1]]==T[i+1] T[next[k+1]]==T[i+1]

  从表中我们看出,虽然0-5的元素ABCABD与6-11的元素ABCABE不匹配,但是0-4的元素有一个公共前后缀AB,与9-10元素完全一样,如果T[2]=T[11]的话,仍然满足T[i+1]=T[k]+1的关系。但是T[2]并不等于T[11],于是继续从0-1中查找是否还有公共前后缀有比较的价值。

2.5 实现next表的创建

void get_next(const char* t,int * next) //获取回溯的表
{
	
	int len = strlen(t);
	int i = 0;//尾缀
	int j = -1;  //前缀
	next[0] = -1; 
	while (i < len)
	{
		if (j == -1 || t[i] == t[j])
		{
			j++;
			i++;
			next[i] = j;
		}
		else
		{
			j = next[j]; //如果没匹配上,寻找有没有对称的子列,顺着上一个对称的位置继续匹配
			//比如ABCABDABCABE   ,前面ABCAB和后面ABCAB是完全匹配的,但是ABCABD和ABCABE不匹配,
			//我们就要把指向D位置的j指针回溯,回溯到哪里呢?我们发现ABCABD前面的ABCAB是有对称元素的
			//那么ABD与后面不匹配,缩小范围,前面的ABC有没有可能与后面的继续匹配呢?如果匹配了,就顺着
			//这个地方的next值继续+1,不匹配就再往前回溯
		}
	}
}


2.6 KMP算法的实现

有了next数组之后,KMP算法就容易了。我们向后依次比对,如果在模式串第i个位置不匹配,查询模式串的next[i]作为回溯值,继续与主串这个位置匹配。如果回溯值为-1,说明主串的元素该向后移动了。


#include<iostream>
#include<cstring>

using namespace std;



string t="ababaaaba";


					 //j-2 j-1 j      i-2 i-1 i
//获取next表思路是
//(1)如果前面字符都配上了比如 A B C       A   B  C ,如果j+1和i+1还能继续配对,那么
//next[i+1] = next[i]+1 匹配数+1
//(2) 如果next[i+1]和next[j+1]没配上,我们就找next[j+1]的next表值,如果不是0,说明0-j部分,还是有对称
//的元素,我们就把j回溯到前一个对称值的下一个位置,再更小范围内搜索看看有没有匹配项

void get_next(const char* t,int * next) //获取回溯的表
{
	
	int len = strlen(t);
	int i = 0;
	int j = -1;  //尾缀
	next[0] = -1; //前缀
	while (i < len)
	{
		if (j == -1 || t[i] == t[j])
		{
			j++;
			i++;
			next[i] = j;
		}
		else
		{
			j = next[j]; //如果没匹配上,寻找有没有对称的子列,顺着上一个对称的位置继续匹配
			//比如ABCABDABCABE   ,前面ABCAB和后面ABCAB是完全匹配的,但是ABCABD和ABCABE不匹配,
			//我们就要把指向D位置的j指针回溯,回溯到哪里呢?我们发现ABCABD前面的ABCAB是有对称元素的
			//那么ABD与后面不匹配,缩小范围,前面的ABC有没有可能与后面的继续匹配呢?如果匹配了,就顺着
			//这个地方的next值继续+1,不匹配就再往前回溯
		}
	}
}

int Index_KMP(const char*S,const char*T )
{
	int next[100]; 
	get_next(T, next);//获取模式串用于标识回溯位置的表
	int i = 0;
	int j = 0;
	int Slen = strlen(S);//主串
	int Tlen = strlen(T); //模式串
	while (i < Slen && j < Tlen)
	{
		if (j==-1||S[i] == T[j])
		{
			i++;
			j++;
		}
		else
		{
			j = next[j];
		}
	}
	if (j == Tlen)//匹配成功
	{
		return i - j;
	}
	else
	{
		return -1;
	}

}
int main()
{
	const char* b = "AAAAAIIIISLSLSLABCABDABCABEPPPPSS";
	const char* a= "ABCABDABCABE";
	int n = Index_KMP(b, a);
	cout << n << endl;


	
	return 0;
}



2.7 KMP算法总结

  KMP算法比暴力破解法,优越的地方在于,保留了每次匹配的成功之处,下次可以从失败处继续,也就是从哪里跌倒就在哪里爬起来。

3.KMP算法的优化

3.1 KMP算法的问题

  遗憾的是,KMP并非是完美无缺的,因为它只总结了每次匹配的成功,却没有在匹配失败的时候进行总结。比如

主串00011111
模式串0000

  模式串的next数组为

0000
-1012

  在模式串的第4个位置匹配失败后,会回溯到下标为2的位置继续与主串匹配,但是模式串下标0,1,2元素都与3一致,3都不一样的,0-2的元素没必要继续匹配了。而依照next表,模式串会继续与主串匹配

主串00011111
模式串(1)0000
(2)0000
(3)0000
(4)0000

  其实上面(1)到(3)部分都是多余的。为了改进KMP算法,我们在求next数组的时候,进行了新的改进

3.2 next数组的改进

  其实求这个next数组的时候,我们就需要记住一点,在某个失败的位置,如果要往前回溯,回溯位置的值与现在位置的值如果相同,应该放弃这个位置,继续向前回溯。


void get_next(const char* t, int* next) //获取回溯的表
{

	int len = strlen(t);
	int i = 0;
	int j = -1;  //尾缀
	next[0] = -1; //前缀
	while (i < len)
	{
		if (j == -1 || t[i] == t[j])
		{
			j++;
			i++;

			//与普通KMP差异的地方
			if (t[i] != t[j])
			{
				next[i] = j;
			}
			else
			{
				next[i] = next[j];
				//这个主要是考虑匹配失败的时候,如果匹配失败了,必然往回回溯,但是如果回溯的下一个
				//地方还是这个元素,必然没有意义,还要往前回溯,那就把前一个元素的回溯值直接交给后面就可以了
				
				//比如  S = abacababc
				//		T = abab
				//匹配第四个元素不同,必然往前回溯,变成
				//      S = abacababc
				//		  T = abab
				//对着的还是b,不可能匹配成功的。
				//所以,如果有匹配的子串,而且其下一个元素也相同,那就意味着,如果下一个元素匹配失败的时候
				//往前回溯,对应元素还是这个值。
			}
			//差异结束
		
		}
		else
		{
			j = next[j]; 
		}
	}
}

3.3 改进的KMP算法实现


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



string t = "ababaaaba";



void get_next(const char* t, int* next) //获取回溯的表
{

	int len = strlen(t);
	int i = 0;
	int j = -1;  //尾缀
	next[0] = -1; //前缀
	while (i < len)
	{
		if (j == -1 || t[i] == t[j])
		{
			j++;
			i++;

			//与普通KMP差异的地方
			if (t[i] != t[j])
			{
				next[i] = j;
			}
			else
			{
				next[i] = next[j];
				//这个主要是考虑匹配失败的时候,如果匹配失败了,必然往回回溯,但是如果回溯的下一个
				//地方还是这个元素,必然没有意义,还要往前回溯,那就把前一个元素的回溯值直接交给后面就可以了
				
				//比如  S = abacababc
				//		T = abab
				//匹配第四个元素不同,必然往前回溯,变成
				//      S = abacababc
				//		  T = abab
				//对着的还是b,不可能匹配成功的。
				//所以,如果有匹配的子串,而且其下一个元素也相同,那就意味着,如果下一个元素匹配失败的时候
				//往前回溯,对应元素还是这个值。
			}
			//差异结束
		
		}
		else
		{
			j = next[j]; 
		}
	}
}

int Index_KMP(const char* S, const char* T)
{
	int next[100];
	get_next(T, next);//获取模式串用于标识回溯位置的表
	int i = 0;
	int j = 0;
	int Slen = strlen(S);//主串
	int Tlen = strlen(T); //模式串
	while (i < Slen && j < Tlen)
	{
		if (j == -1 || S[i] == T[j])
		{
			i++;
			j++;
		}
		else
		{
			j = next[j];
		}
	}
	if (j == Tlen)//匹配成功
	{
		return i - j;
	}
	else
	{
		return -1;
	}

}
int main()
{
	const char* b = "AAAAAIIIISLSLSLABCABDABCABEPPPPSS";
	const char* a = "ABCABDABCABE";
	int n = Index_KMP(b, a);
	cout << n << endl;



	return 0;
}



4. 参考资料

【1】大话数据结构

【2】「天勤公开课」KMP算法易懂版

【3】浙大-数据结构

【4】 清华-数据结构

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值