KMP算法(超详细)

00:历史背景           

        KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)

        简单点来说,kmp是一种改进的字符串查找算法

01:暴力查找

        俗称BF算法

        假设有一个字符串str,一个字符串sub,一个整型变量pos,请从str的pos下标开始查找,如果有sub字符串的话返回下标,没有的话返回-1。

暴力查找的思路

假设        str为"ababc"

               sub为"abc"

                pos为0

        开始查找时设中间变量 i=0 ,j=0

KMP算法(超详细)_第1张图片

暴力算法实现

让我们用c语言来实现一下

/*
* str  源字符串
* sub  目标字符串
* pos  开始查找的位置
* return  目标字符串在源字符串的位置的下标
*/
int BF_strfind(char* str, char* sub, int pos)
{
	assert(str && sub);      //字符串地址为空的话报错
	
	int len_str = strlen(str);
	int len_sub = strlen(sub);
	
	assert(pos >= 0 && pos < len_str);         //如果越界访问的话报错
	if (len_sub > len_str) return -1;   //如果sub串长度大于str,肯定找不到

	int i = 0, j = 0;
	while (j < len_sub && i < len_str)
	{
		if (str[i] == sub[j])     //单个元素相等
			i++, j++;
		else
			i = i - j + 1, j = 0;//单个元素不相等
	}
	if (j == len_sub) 
		return i - j;		//找到啦
	else 
		return -1;			//没找到
}

暴力算法的一些问题

假设        str为"abababc"

                sub为"ababc"

按照暴力算法的思路

KMP算法(超详细)_第2张图片

请注意,由于sub中,由于sub[0]==sub[2],sub[1]==sub[3],所以在上述过程中第三,四步和第七八步是一样的,第六步是一定为假,换句话说,当走完第三四五步之后,可以直接跳过第六七八步.

让我们列出其中的i,j值

1.i=0,j=0        2.i=1,j=1        3.i=2,j=2        4.i=3,j=3        5.i=4,j=4        6.i=1,j=0

7.i=2,j=0        8.i=3,j=1        9.i=4,j=2        10.i=5,j=3      11.i=6,j=4      

这个就是暴力算法与kmp算法的主要不同之处.无意义的回溯导致算法效率低下

02:kmp算法

还是以上面那道题为例:让我们列出改进后的i,j值

1.i=0,j=0        2.i=1,j=1        3.i=2,j=2        4.i=3,j=3        5.i=4,j=4       6.i=4,j=2            7.i=5,j=3      8.i=6,j=4      

可以看出两者的差距:

  1. i值总是单调递增的
  2. 匹配失败后,j值并不一定会归0

i值很好推测:匹配成功是i++,失败时不变即可

那么问题来了,j值的变化量应该是多少呢。显然,应该是和sub字符串有关的。

一般来说,我们会新建一个数组next来存放j值在不同的下标匹配失败后退回到的位置

next数组

  1. next数组的大小应与sub一致
  2. next数组用于存放下标,应为整型数组

next数组的计算

首先,在sub[0]处匹配失败后j应仍为0,这个是一个特殊位置,一般来说我们会让sub[0]=-1

在sub[1]处匹配失败后j值应回退至0,故sub[1]=0。这两个位置的值是固定的。

对于其他位置的j值来说,如果j下标之前的k个元素与从0开始可k个元素完全一样,则推到k下标即可

举个例字:

KMP算法(超详细)_第3张图片

显然,因为sub[0]=sub[3],sub[1]=sub[4],sub[2]=sub[5],所以当在第一次匹配失败时,j从6变成3。故此时next[6]=3,同理可推其他位置

代码

生成next数组

/*
* 该函数用于生成一个next数组
* next 匹配失败回溯的下标
* sub  待匹配字符串
* len  sub长度
*/
void GetNext(int* next, char* sub, int len)
{
	assert(next && sub&&len!=0);

	next[0] = -1;
	if (len == 1) return;

	next[1] = 0;

	int i = 2;
	int k = next[1];

	while (i < len)
	{
		if (k==-1||sub[i-1] == sub[k])
			k++,next[i]=k,i++;          //k为-1时,代表没能回溯的子串,next[i]也就应为0,k不为-1时,代表有子串与之匹配,此时他应比next[i-1]多一个,也就为k+1。
		else
			k = next[k];
	}
}

对应的匹配代码

/*
* str  源字符串
* sub  目标字符串
* pos  开始查找的位置
* return  目标字符串在源字符串的位置的下标
*/
int KMP_strfind(char* str, char* sub, int pos)
{
	assert(str && sub);      //字符串地址为空的话报错
	
	int len_str = strlen(str);
	int len_sub = strlen(sub);
	
	assert(pos >= 0 && pos < len_str);         //如果越界访问的话报错
	if (len_sub > len_str) return -1;   //如果sub串长度大于str,肯定找不到

	int* next = malloc(sizeof(int) * len_sub);
	GetNext(next, sub, len_sub);
	int i = 0, j = 0;
	while (j < len_sub && i < len_str)
	{
		if (j==-1||str[i] == sub[j])     //单个元素相等
			i++, j++;
		else
			j=next[j];//单个元素不相等
	}
	if (j == len_sub) 
		return i - j;		//找到啦
	else 
		return -1;			//没找到
}

改进型next数组

改进型的next数组俗称nextval

再举个例子:

KMP算法(超详细)_第4张图片

可以看出由于sub[0]=sub[1]=sub[2]=sub[3],进行第六项后第七八九项是可以跳过的,也就是说,改进后的nextval[3]=nextval[2]=nextval[1]=nextval[0]=-1

换个更容易理解的例子

KMP算法(超详细)_第5张图片

对于sub[3]来说,当在sub[3]处不匹配时,原本会到next[3]处再匹配一次,但是因为sub[3]=sub[next[3]],所以next[3]处匹配必定失败,因此改进算法在在sub[i]==sub[nextval[i]]时,令nextval[i]=nextval[nextval[i]]

最终代码

/*
* 该函数用于生成nextval数组
* nextval匹配失败后不同位置的返回值
* sub待匹配字符串
* len字符串长度
*/
void GetNextval(int* nextval, char* sub, int len)
{
	assert(nextval && sub&&len!=0);
	nextval[0] = -1;
	if (len == 1) return;
	nextval[1] = 0;
	int i = 2;
	int k = nextval[1];
	while (i < len)
	{
		if (k == -1 || sub[i - 1] == sub[k])//前者代表此处与前串匹配必定失败,需要从下一处开始匹配,后者代表匹配成功,
		{
			k++;
			if (sub[i] == sub[k])           //代表原next数组找到的两个相同的子串的后一位与子串的开头相同
				nextval[i] = nextval[k];
			else                            //与next相同
				nextval[i] = k;
			i++;
		}
		else
			k = nextval[k];					//未能成功匹配,回溯到之前
	}
}
/*
* str  源字符串
* sub  目标字符串
* pos  开始查找的位置
* return  目标字符串在源字符串的位置的下标
*/
int KMP_strfind(char* str, char* sub, int pos)
{
	assert(str && sub);      //字符串地址为空的话报错
	
	int len_str = strlen(str);
	int len_sub = strlen(sub);
	
	assert(pos >= 0 && pos < len_str);         //如果越界访问的话报错
	if (len_sub > len_str) return -1;   //如果sub串长度大于str,肯定找不到

	int* nextval = malloc(sizeof(int) * len_sub);
	GetNextval(nextval, sub, len_sub);
	int i = 0, j = 0;
	while (j < len_sub && i < len_str)
	{
		if (j==-1||str[i] == sub[j])     //单个元素相等
			i++, j++;
		else
			j = nextval[j];//单个元素不相等
	}
	if (j == len_sub) 
		return i - j;		//找到啦
	else 
		return -1;			//没找到
}

KMP算法(超详细)_第6张图片

感谢观看!!!

你可能感兴趣的:(数据结构)