字符串的模式匹配问题(朴素算法和kmp算法)

说白了就是处理char数组的问题。
有一个子串,就是一个串的一部分。
涉及串的操作也不多,插入删除,查找子串。

存储结构的话,其实用链表也行,但是会很繁琐(链表可以考虑一个数据域存储多个字母,即使用char数组,记得用一个‘\0’即可)

模式匹配

什么叫模式匹配呢,就是在一个串中匹配给定子串的起点下标,匹配不成功返回-1。

回溯法
看名字就能猜到含义,就是匹配的过程中如果和子串出现不同,就要在将子串回溯到起点,主串回溯到第一个匹配成功的点的下一个。

举个例子
abcdefg
bce
第二个匹配成功了,直到主串的d和子串的e匹配失败了,此时主串要回溯到第一个匹配成功点的下一个,就是c,子串应该从头开始继续匹配。

int Brute_Force(char *a,char *b,int length_a,int length_b)//a主串,b子串
{
	int i=j=0;
	while(i<length_a && j<length_b)
	{
		if(a[i]==b[j])
		{
			i++;
			j++;
		}
		else 
		{
			i=i-j+1;//回溯
			j=0;
		}
	}
	if(j>length_b)//说明子串完全被匹配了
		return j-length_b+1;
	else 
		return -1;
}

简单粗暴,考虑一下平均时间复杂度
最好情况
每一次要么是第一位匹配失败,要么是直接成功,能成功的位置为0~length_a - length_b,假设失败的次数为i,则 i 的范围也应为0到length_a - length_b。
所以最好情况的时间复杂度:(ab分别为ab的长度简写)
在这里插入图片描述
o(a + b);

最坏情况
每一个点都是匹配到最后一个元素,发现自己被耍了,然后回溯,重新匹配,直到匹配成功。
所以匹配次数应该为(i+1) * length_b,i为匹配的趟数。
时间复杂度:
在这里插入图片描述
o(a * b);

可以看出来,在情况不如意的时候,该算法还是不太合理,所以一种改进的算法出现了。

kmp算法

之前的暴力匹配算法之所以能达到乘积的时间复杂度,主要原因就在回溯上,所有的情况都一概而论,直接回溯到最开始,但如果有一些情况并不需要回溯这么多,时间上就会省不少。

那应该回溯多少呢?接下来提出next函数(该函数对应的是j回溯的量,此时i不回溯)

  1. 子串的第一位是-1;//注意了,next函数是针对子串的
  2. 和子串第一个字母相同,下面两个条件满足一个,为-1;
    取k∈[1.j),如果a[0]到a[k-1]和a[j-k]到a[j-1]没有一个k能使他们相同
    或者有k保证其相同的,但是a[k]和a[j]相同
  3. 取k∈[1.j),如果a[0]到a[k-1]和a[j-k]到a[j-1]有k保证其相同且a[k] != a[j],为k
  4. 剩余情况为0

这么讲正常人都懵,那就来一些例子

在这里插入图片描述
先自己想一下,然后下拉看答案吧

在这里插入图片描述
这里面感觉bc属于第四条的可能是有一点不太好弄(反正我是翻车了)

下一个:
在这里插入图片描述

答案:
在这里插入图片描述
注意一下6,虽然a和a[0]一样,但是应该是用3来判断的。因为3不限制当前元素是否和a[0]一样。

下一个:
在这里插入图片描述

答案:
在这里插入图片描述
这里小心一下最后一个吧,因为a[3]既在前面的[0,k-1]又在[k-j,k-1]

next的应用

当你千辛万苦看到这里,并熟练的掌握了next函数,恭喜你,kmp算法小课堂开始了(狗头保命)

next函数在这样用的:(当前为a[i])

  1. next=-1,说明下一次a[i+1]和b[0]//本意是说明间接比较过a[i]和b[0]了
  2. next=0 ,说明下一次a[i]和b[0]
  3. next=k ,说明下一次a[i]和b[k]
    这个没啥,毕竟求next的目的就是在于找出到底回溯多少是合适的。

代码:

int KMP(char* s,char* t,int next[])
{
    int index=1,i=0,j=0;
    if(!s||!t)
        return -1;
    else if(s[0]=='\0'||t[0]=='\0')
        return -1;
    while(s[i]!='\0'&&t[j]!='\0')
    {
        if(s[i]==t[j])//匹配成功
        {
            i++;
            j++;
        }
        else
        {
            index+=j-next[j];//失配,回溯返回值,并按情况讨论
            if(next[j]!=-1)//情况二、三在一起
                j=next[j];
            else//情况一
            {
                j=0;
                i++;
            }
        }
    }
    if(t[j]=='\0')
        return index;
    else
        return -1;
}
void Next(char* t,int next[])
{
    int j=0,k=-1;
    next[0]=-1;//第一条
    while(t[j]!='\0')
    {
        if(k==-1||t[j]==t[k])
        {
            k++;
            j++;
            if(t[j]!=t[k])
                next[j]=k;//第四条的直接失配,或者是第三条
            else
                next[j]=next[k];//第二条,和第四条中最后一位相同导致的失配
        }
        else
            k=next[k];//倒退k
    }
}

next函数代码和上述的思路还是有一点出入的,但是功能相同。

在这里插入图片描述

首先第一个为-1,此时k为-1,j为0,进入循环;
满足第一个if,k=0,j=1,且两个下标对应值不等,第四种情况为0;
满足第一个else,k回退为-1;
进入第一个if,同上一个,next[2]=0;
k再次回退为-1;
t[3]和首字母相等,next[3]=next[0]=-1;第二条
此时不需要回退,j、k加一后不等,符合第三条

在这里插入图片描述
next[0] = -1;
next[1&2]和上面相同,都是回退k为-1后进入内层的if语句,为0;
next[3]同上,在回退k为-1进入内层的else语句,取next[0]=-1;
next[4]因为是在最后一个位置相同了,也就是t[k]=t[j],符合第四条,进入内层的else语句。
next[5]同上;(但此时k没有发生回退而是一直加一)
next[6]也为内层的else语句,此时k为3;
next[7]时,因为最后一个元素不等,所以next函数取k值,为4,内层if语句。

通过这两个例子我们对这个函数的体会能更加深刻一些。
第二条的实现是在内部的else语句实现的,此时的k为0,next[0]=-1;
第三条的匹配成功且最后一个不同,是在内层的if语句实现的;
至于第四条,大致分两部分,一个是直接匹配失败(如next[1&2])和匹配成功但是最后一个元素相同,第一种是在内层if实现的,此时的k为0;第二种则是在内层else语句中实现的。
至于外层的else语句,则是实现了k的倒退功能,一般为-1(当然有不同的情况,多数是k为0,也就是第四条的直接失配)

kmp总结

kmp的算法中有一个求next步骤是之前没有的,是一个o(m),因为一般来说m<<n,所以可以不用心疼这么一点时间,而且还是将乘积的时间复杂度降为线性阶。

但是呢,因为我们之前得到的乘积阶时间复杂度是在最不理想的情况下,正常情况kmp是简单一些,但是不一定是绝对需要的,(毕竟还占了一个数组),而且kmp算法很麻烦(其实是麻烦的一批),所以在实际中也不是一定要用的,但是还是要会的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值