基本kmp、加强kmp、扩展kmp、原理及其代码实现

一、实现原理

例如有母串s[100] = {“abcababcab…”},有子串t[6] = {“abcabx”}。
前5 个子串与母串都是相等的,当比较进行到第六位时,子串与母串不相等。
常规的思路是让子串对齐母串的第二位再进行遍历。
BUT,聪明的克努特、莫里斯、普拉特采用了一种很聪明的算法,使得时间复杂度远远下降,实现原理如图(废话好多):

在这里插入图片描述
就是这么个原理,然后加上亿点点细节如下:
在这里插入图片描述
所以实现其算法的关键之一就在于找到子串的相等的前后缀,即next数组

二、基本kmp

可参见视频https://www.bilibili.com/read/cv8013121
讲的比较详细

1、next数组

首先吐槽大话数据结构这本书,讲的kmp中t[0]用来存长度,不符合我们C语言学习者的习惯。
所以下面采用更通俗易懂的正常的方法,由前面的基础可知next数组里j值的大小完全取决于当前字符之前的相似度。
计算next[j]的方法:

  • 当j = 0时,next[j] = -1;//沙比大话数据结构j = 0时令为0.
  • 当j>0时,next[j]的值为:模式串的位置从0到j-1构成的串中所出现的首位相同的的字串的最大长度。
  • 当没有首位相同的子串时next[j]的值为0。
    如图:
    在这里插入图片描述
    接下来把他变成代码
#include <stdio.h>
#include <string.h>
void get_next(char *t,int *next)
{
    int i = 0,j = -1;
    int len;
    next[0] = -1;
	len = strlen(t);
	i = 1;//后缀
    j = 0;//前缀
    next[1] = 0;
    while(i<len)
    {
    	if(j == -1|| t[i] == t[j])//当j回溯到-1时,j从头开始,i继续加一
    	{
    		++j;
    		++i;
    		next[i] = j;//最精彩的地方来了 下面会讲
		}
		else
		{
			j = next[j];//女少口啊 下面会讲
		}
	}
	/*for(int i = 0;i<len;i++)
	{
		printf("%d",next[i]);
	}测试用*/
	
}

对于这一语句的理解是
next[i] = j;
next[i]在i位之前的最长公共缀值 = j,j的值表示相同前缀的字符个数。(沙比大话里面j初始化为0,此时j表示为前缀的长度+1)

大的要来了

else//t[i]!=t[j]
{
    j = next[j]//回溯
}

next[j]是一个已知的值,以为都已经算到next[i]了,而i是比j要大的(我当时为什么要纠结这个,好蠢)。

这一语句表示什么意呢?就是j要回退到曾经找过的公共缀位置,继续比较。

那为什么next里面下标是j呢?因为j为前缀,此时前缀字符与后缀字符不等,故需要回溯到第j位之前的最长公共缀值,再从那个位置重新开始匹配。(真的佩服这三位大佬,怎么想到的
在这里插入图片描述

2、全部代码实现

直接上代码

#include <stdio.h>
#include <string.h>
int next[1000];
void get_next(char *t,int *next)
{
    int i = 0,j = -1;
    int len;
    next[0] = -1;
	len = strlen(t);
	i = 1;//后缀
    j = 0;//前缀
    next[1] = 0;
    while(i<len)
    {
    	if(j == -1|| t[i] == t[j])//当j回溯到-1时,j从头开始,i继续加一
    	{
    		++j;
    		++i;
    		next[i] = j;//最精彩的地方来了 下面会讲
		}
		else
		{
			j = next[j];//女少口啊 下面会讲
		}
	}
	/*for(int i = 0;i<len;i++)
	{
		printf("%d",next[i]);
	}测试用*/

}
int kmp(char *s,char *t)
{
    int i = 0,j = 0;
    int len_s,len_t;
    len_s = strlen(s);
    len_t = strlen(t);
    while(i<len_s&&j<len_t)
    {
        if(j == -1||s[i] == t[j])//若j==-1则从头再来
        {
            i++;
            j++;
        }
        else
        {
            j = next[j];//精彩的部分
        }
    }
    if(j == len_t)
    {
        return (i-j);//返回子串t在母串第几个字符之后的位置
    }
    else
    {
        return -1;//没有则返回-1
    }

}
int main()
{
	char s[1000],t[1000];
	int res;
	scanf("%s",s);
	getchar();
	scanf("%s",t);
	get_next(t,next);
	res = kmp(s,t);
	printf("%d",res);
	return 0;
}

对于j = next[j];//是不是有些眼熟?形式与next函数中的一样,其功能也差不多
就是将j向右滑动到next[j]的位置
在这里插入图片描述
运行截图
在这里插入图片描述

三、加强版kmp

1、 为什么会有加强版??

因为1.0版本不够强。

进入正文,当s = ”aaaabcde“,子串t = “aaaaax”,此时就会出现很多重复的比较,如图(图源《大话数据结构》由于大话里j的初始值不一样,且对数组的定义也不一样,所以看懂意思就行,下标什么的不必纠结)
在这里插入图片描述
就是当子串中有许多相同的字符时会产生很多重复的判断

nextval数组及完整代码

就真的只是加了一点点细节。

#include <stdio.h>
#include <string.h>
int nextval[1000];
void get_nextval(char *t,int *next)
{
    int i = 0,j = -1;
    int len;
    next[0] = -1;
	len = strlen(t);
	i = 1;//后缀
    j = 0;//前缀
    next[1] = 0;
    while(i<len)
    {
    	if(j == -1|| t[i] == t[j])//当j回溯到-1时,j从头开始,i继续加一
    	{
    		++j;
    		++i;
            if(t[i]!=t[j])
            {
                nextval[i] = j;
            }
            else
            {
                nextval[i] = nextval[j];//如果与前缀字符相同,则将前缀字符的nextval值赋给nextval在i位置的值
            }
    		
		}
		else
		{
			j = nextval[j];
		}
	}
	/*for(int i = 0;i<len;i++)
	{
		printf("%d",next[i]);
	}测试用*/

}
int kmp(char *s,char *t)
{
    int i = 0,j = 0;
    int len_s,len_t;
    len_s = strlen(s);
    len_t = strlen(t);
    while(i<len_s&&j<len_t)
    {
        if(j == -1||s[i] == t[j])//若j==-1则从头再来
        {
            i++;
            j++;
        }
        else
        {
            j = next[j];//精彩的部分
        }
    }
    if(j == len_t)
    {
        return (i-j);//返回子串t在母串第几个字符之后的位置
    }
    else
    {
        return -1;//没有则返回-1
    }

}
int main()
{
	char s[1000],t[1000];
	int res;
	scanf("%s",s);
	getchar();
	scanf("%s",t);
	get_nextval(t,next);
	res = kmp(s,t);
	printf("%d",res);
	return 0;
}

其实我感觉也没加强多少

四、 扩展版kmp

扩展版kmp的用处

当子串与母串不完全匹配时,我们也想知道母串的每个位置最多有几个字符与子串相等,或者我们想知道字串在母串中出现了几次,就需要用到此法
我们用extend[i]来表示s[i...|s|]与t的最长公共前缀的长度

i0123456
sababaca
tabac
extent[i]3014101

实现思路

在这里插入图片描述

假设当前遍历到s串位置i,extend[i]的值已经得到,l和r,r代表以l为起始位置的字符匹配成功的右边界,r = 最后一个匹配成功的位置+1,即s[l…r]等于t[0…r-l]。
现在我们还需要一个辅助数组next[i],定义为表示t和t[i…|t|]的最长公共前缀,其实就是extend[i]数组的子串和母串都是t的情况
如下表

i012345678
tabcabcabd
next800500200

①当i+next[i-l]<r
在这里插入图片描述
可以看出next[i-r]即为从i-r开始的t的最长前缀,图中用圆圈表示。所以此时extend[i] = next[i-r]
②当i+next[i-l]=r
在这里插入图片描述

处肯定不等于r-i+1(B)处,所以next数组才定在了r-l处。
又因为s中r+1(C处不等于r-l+1(B)处,但是不能判断C等不等于(A)。
故接下来就从s[r]和t[r-l]处开始继续往后遍历。
③当i+next[i-1]>r
在这里插入图片描述
可以肯定的是A部分==B部分。
又因为a部分!=b部分所以C部分!=B部分。
故C部分不等于A部分。
此时extend[i] = r-i。

代码实现

直接上代码不多bb



#include <stdio.h>
#include <string.h>

void GetNext(char *T, int m, int *next)//注释参考后面的
{
    int l = 0, r = 0;
    next[0] = m;//下标为0时next值就等于数组长度

    for (int i = 1; i < m; i++)
    {
        if (i >= r || i + next[i - l] >= r)// i >= r 的作用:举个典型例子,T和 T 无一字符相同
        {
            if (i >= r)
                r = i;

            while (r < m && T[r] == T[r - i])
                r++;

            next[i] = r - i;
            l = i;
        }
        else
            next[i] = next[i - l];
    }
}

/* 求解 extend[] */
void GetExtend(char *S, int n, char *T, int m, int *extend, int *next)
{
    int l = 0, r = 0;
    GetNext(T, m, next);

    for (int i = 0; i < n; i++)
    {
        if (i >= r || i + next[i - l] >= r) // i >= r 的作用:举个典型例子,S 和 T 无一字符相同
        {
            if (i >= r)//i>r时,r落后要跟上i的步伐
                r = i;

            while (r < n && r - i < m && S[r] == T[r - i])//匹配则r加一
                r++;

            extend[i] = r - i;
            l = i;//结合图片
        }
        else
            extend[i] = next[i - l];
    }
}

int main()
{
    int next[100];
    int extend[100];
   char S[100], T[100];
    int n, m;
    gets(S);
    gets(T);
	n = strlen(S);
	m = strlen(T);
	GetExtend(S, n, T, m, extend, next);
	for(int i = 0;i<m;i++)
	{
		printf("%d",next[i]);
	}
	printf("\n");
	for(int j = 0;j<n;j++)
	{
		printf("%d",extend[j]);
	}
    return 0;
    }

完工

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值