字符串模式匹配朴素暴力算法与KMP算法C++实现(附图精讲)

字符串模式匹配问题的两种算法

问题描述

给定文本串 t ,与模式串 p,要求从文本串 t 中找出第一次出现模式串 p 的位置。

基本的匹配算法(暴力求解)

思路

文本串t从i=0开始,模式串p从就开始,依次比较,如图
在这里插入图片描述
当出现不匹配时,j回溯首位,i++,再次依次按位比较。想法朴素,简单,但是每次相当于p串只向后移动了一位。
在这里插入图片描述

暴力法代码(C++):

#include <iostream>
using namespace std;
int BruteForceSearch(char *t, char *p) {
	int i = 0;
	int j = 0;
	int t_Len = strlen(t);//求出字符擦混长度
	int p_Len = strlen(p);
	while (i < t_Len&&j < p_Len) {
		if (t[i + j] == p[j]) {
			j++;//如果匹配,j右移
		}
		else {//否则,i向右移动,j回溯到0;
			i++;
			j = 0;
		}
	}
	if (j >= p_Len) {//如果j到达了末尾,说明全部匹配,返回i的位置,就是目标位置
		return i;
	}
	return -1;
}
int main() {
	char t[] = "qwertyertueriotyep";
	char p[] = "ertueri";
	cout<<"首次匹配的字符串的位置索引:"<<BruteForceSearch(t, p);
	system("pause");
}

暴力法时间复杂度O(m*n),空间复杂度O(1)

暴力法思路简单,但也很容易发现,其实有的比较是多余的,比如, TODO ,能不能对它进行改进呢,下面请看KMP算法:

KPM算法

简单介绍下

KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。

思路

看下面这个例子,当t [i+j] != p[j]时,看看暴力法是如何做的,还是一次移动一位,是不是显得有些冗余呢?
在这里插入图片描述
再来看看如果我们人工做的话,可以发现可以直接将相同的部分对齐,再来比较下一位。如下图。

在这里插入图片描述
这就是暴力算法可以改进的地方,其实,只要细心不难发现在p的前j-1想中出现了对前面的两位c,d和后面的c,d完全相同,我们将其称为字符串p[0…j-1]的最大相等前缀,和最大相等后缀,(或者称为最大重复子串)而 j 的位置直接赋值给了b所在位置的下标k,其实也就是相同的前缀,后缀的个数k。现在,问题就转化为了如何寻找j下次移动的位置,也就是k的位置,即,重复的个数k;

next数组

我们把p串中每个位置 j 对应的k求出来,也就是不包括 j ,0~j-1位中前缀和后缀相同的个数,存放到一个数组next中,为什么要把所有的k都求出来呢?因为你不知道是什么时候p[j]与t[i+j]不匹配呀。如果你还是不要清楚next数组是个什么东东。看下面的表格

j 0 1 2 3 4 5 6 7 8
模式串p a b a a b c a b a
next -1 0 0 1 1 2 0 1 2

例如:当j=5时,不包括p[5]=c,在c前面有前缀ab,与后缀ab相同且最长,所以个数为2,即k=2,也即next[5]=2;同理若没有相同的就为0,特殊的,我们令next[0]=-1。

如何求next数组?

上面提到,next[0]总是等于-1的,我们可以试着看能否得到某种递推关系,依次找到next[1],next[2]…呢?

假设next[j]=k已知,要求next[j+1],如图,既然已知next[j]=k,就说明图中左边的青色蓝色青色部分与右边的青色蓝色青色部分相同,现在要求,next[j+1],就是要把p[j]也包含进去,现在我们只需要比较p[k]与p[j]即可,
情况一:
if p[k]=p[j],
易得:next[j+1]=next[j]+1=k+1;

在这里插入图片描述
情况二:
if p[k]!=p[j],
令next[k]=h,
比较p[h]与p[j]
if p[h]=p[j],next[j+1]=h+1=next[next[k]]
if p[h]!=p[j],重复上述过程,比如令h’=next[h]这样继续下去…

!!!!!!下面这个图很重要,理解k=next[k]的关键!!!!!!
在这里插入图片描述
解释:如图,当p[k]!=p[j]时,我们再找next[k],为什么呢,如图,因为我们已经知道next[j]即两段长度为K的部分相等了,那么利用对称(宏观对称,不考虑内部逐个元素)就很容易都得到h前面的蓝色部分与j前面的蓝色部分相等,那么此时我们只需要比较p[h]与p[j]即可,若相等,next[j+1]=h+1=next[next[k]],若不相等,继续分割h,找到next[h],重复上面的过程即可。
这样我们就得到了求next的地推关系了,就可以根据已知的next[0]依次求出整个next数组了。下面看球next数组的代码。

求next数组的代码(C++)

//求出匹配串p中每个字符的next的值
void getNext(char *p, int next[]) {
	int p_Len = strlen(p);
	next[0] = -1;
	int k = -1;
	int j = 0;
	while (j < p_Len - 1) {
		if (k == -1 || p[k] == p[j]) {
			k++;
			j++;
			next[j] = k;
		}
		else {
			k = next[k];//相当于前面将的h=next[k],然后再到上面比较p[h]与p[j],这里直接将next[k]赋值给k.
		}
	}
}

在我第一次看到这个代码的时候,k=next[k]困扰了我好久好久,现在看来,只要理解了上面的图,就变得非常简单了。

KMP求字符串模式匹配完整代码

求出了next数组,利用KMP优化暴力算法求模式匹配就很简单了,直需要将next[j]的值赋给j就可以啦。完整代码如下:

#include <iostream>
using namespace std;

//求出匹配串p中每个字符的next的值
void getNext(char *p, int next[]) {
	int p_Len = strlen(p);
	next[0] = -1;
	int k = -1;
	int j = 0;
	while (j < p_Len - 1) {
		if (k == -1 || p[k] == p[j]) {
			k++;
			j++;
			next[j] = k;
		}
		else {
			k = next[k];
		}
	}
}
int KPM(char *t, char*p, int next[]) {
	int result = -1;
	int t_Len = strlen(t);
	int p_Len = strlen(p);
	int i = 0, j = 0;
	while (i < t_Len) {
		if (j == -1 || t[i] == p[j]) {//如果二者相等,依次比较后面的即可
			i++;
			j++;
		}
		else {
			j = next[j];//出现不匹配的情况,这就将next[j]的值赋给j
		}
		if (j >= p_Len) {
			result = i - p_Len;
			break;
		}
	}
	return result;
}
int main() {
	char t[] = "qwertyertueriotyep";
	char p[] = "ertueri";
	int next[50];
	getNext(p, next);
	cout << "首次匹配的字符串的位置索引:" << KPM(t, p, next);
	return 0;
	system("pause");
}

KMP算法时间复杂度O(M+N),空间复杂度O(N)(N为文本串t长度,M为模式串p长度)

结束语

相互学习,共同进步,望批评指正!

猜你喜欢

转载自blog.csdn.net/K__Ming/article/details/105317743