串的模式匹配

【注】致力于讲知识讲明白!不懂请留言!

定义

串的模式匹配是指子串的定位操作,也就是求子串(模式串)在主串中的位置;

1.暴力模式匹配(BF)算法

初始化两个数组下标,循环比较两个字符串的字符是否相等,相等则下标同时进一,比较下个元素直到匹配成功;不相等则使匹配的串下标回退到 0,被匹配的串下标回退到比较的初始位置+1,循环往复。

暴力模式匹配(BF)算法如下:

int index(SString S,SString T)			//SString是存储字符串的结构
{                                    
    int i=1,j=1;							//i 和 j 分别为主串和模式串的下标指示器
    while(i <= S[0]&& j <= T[0])	//两个字符串比较的范围即不超过主串长度
    									//且j不超过模式串长度
    {
        if(S.ch[i] == T.ch[j])	//主串和模式串的对应赐福相同,则继续比较下一个字符长度
        {
            ++i;
			++j;        //继续比较后面的字符
        }
        else
        {
            i=i-j+2;     //如果主串和模式串发现字符不同,则需要回退
            j=1;
        }
    }
    if(j>T[0])
         return i-T[0];//匹配结束后如果j大于模式串长度,说明整个模式串都匹配完毕
         				//也就是返回主串中匹配的初始位置,即减去模式串的长度
    else
        return 0;  //没有匹配到,返回0
}

算法运行过程:(图中,i 和 j 表示匹配字符在数组中的位置下标):
在这里插入图片描述
【注】最坏的情况下的时间复杂度为O(nm);
举个例子分析下:假设n,m分别为主串和模式串的长度,最坏情况下,主串中的每一个位置,都要对模式串完整的比较一次,完整一次的比较运算次数为O(m),主串有n个位置,需要比较n轮,所以最坏情况下的时间复杂度O(n*m);
在这里插入图片描述

暴力模式匹配(BF)算法在进行模式匹配时,从主串的第一个字符开始,每次失败都是模式后移一位再从头开始比较,而某趟已经匹配相等的字符是模式的某个前缀,这种频繁的重复比较相当于模式串不断的进行自我比较,这就是其效率低的根源;

2.KMP算法

KMP算法是在BF算法的基础上改进得到的,利用已经部分匹配这个有效信息,保持 i 指针不回溯,通过修改 j 指针,让模式串尽量地移动到有效的位置,重点就在于当某一个字符与主串不匹配时,我们应该知道j指针要移动到哪里。即跳过了很多不需要比较的情况。如下图的分析:
第一种情况来说,在第一次比较失败的时候,之前的每次比较都是成功的,也就是说主串中这一部分和模式串的这一部分完全匹配的;
在这里插入图片描述
那既然前面的部分是完全匹配,那么暴力算法的接下来几次匹配肯定是成立的,也就是说这些比较其实是不需要的;
在这里插入图片描述
另外一种情况就是,在第一次比较失败的时候,之前的每次比较都是不成功的,也就是说主串中这一部分和模式串的这一部分不完全匹配的;
在这里插入图片描述
模式串前面都匹配的abcd部分由于a后面是bcd,这三个字符显然和a不匹配,所以暴力算法接下来的每次都后移一个位置显然是都不可能匹配的;
在这里插入图片描述

所以在这种情况下,我们前面这几次比较就是需要跳过的,那我们该一次性跳过几个字符呢?能否利用第一次得到的信息来预知后面移动的位置呢?
我们可以通过next数组(即记录模式串中每个位置发生匹配失败时,下一次和主串对应位置对比的字符的下标)来解决这个问题,那如何来求next数组呢? 下面先看一下进行匹配的过程:
在这里插入图片描述
在这个图中,第一行是主串,第二行是我们需要匹配的模式串(在竖线之前是完全匹配的),最后一行是在发生不匹配时,模式串需要进行后移(t 之前是不需要匹配的,即P[1, t] = P[j-t, j]),然后就是模式串中 t 位置重新和主串中 i 的比较,不过需要注意的是我们的这个t可能不至一个,如下图:
在这里插入图片描述
在这个图里面呢,我们可以取第一个 a 和最后一个 a,当然也可以取前面的 aba 和后面的 aba,但是我们的next数组一定是一个固定的值,这里我们需要选择的值是最大的 t,就是 j - t 最小,即移动距离最小的,(从安全角度考虑的),根据下面的图进行理解:
在这里插入图片描述
根据上面的分析,我们大概就理解了next数组,下面看一下求next数组的代码

void get_next(char T[], int next[]){ //基于递推思想
	i = 1;
	next[1] = 0;
	j = 0;
	while(i <= T[0]){
		if(j == 0 || T[i] == T[j])
		++i;
		++j;
		next[j] = i;
	}
	else
		j = next[j];
}

看到这个代码呢,肯定是一脸懵逼的(大佬除外),下面根据一个例子来分析下代码:
先看两个特殊的情况:
next[j] 的值表示当S[i] != T[j] 时,j 指针(模式串指针)的下一步移动位置;
在这里插入图片描述
如果第一个位置就不匹配了,指针 j 已经无法再左移到之前匹配的位置,所以解决办法就是右移指针 i (主串指针);
在这里插入图片描述
如果第二个位置就不匹配了,指针 j 只能右移到第一个位置;
在这里插入图片描述
然后再考虑一般的情况1:
如果6号位置不匹配了,那根据KMP算法,此时前后缀匹配最长为 ab,长度为2,所以指针 j 左移到2+1=3号位置,故next[6] = 3;
在这里插入图片描述
如果7号位置不匹配了,那根据KMP算法,此时前后缀匹配最长为 abc,长度为3,所以指针 j 左移到3+1=4号位置,故next[7] = 4;
在这里插入图片描述
这就是递推的一大类情况,那它在代码中的体现就是:

		if(j == 0 || T[i] == T[j])  //j==0先不考虑
		++i;
		++j;
		next[j] = i;

再考虑一般的情况2:
这时候 i 所指的 b 和 j 所指的 c 不相等,所以我们前后缀相同的最大长度不可能为4(abax),只能寻找更短一点的相同前后缀(比如ab) ;
在这里插入图片描述
如下图:
在这里插入图片描述
向前循环找更短一点的相同前后缀
在这里插入图片描述
那它在代码中的体现就是:

else
	j = next[j];

最后再单独分析下 j == 0的情况:
T[2] = b != T[1] = a 说明前后缀不等,进入 else语句:

else
	j = next[j];

如果不考虑这种情况,那么循环到这里就走不下去了,因为T[0]不存在或者说这时主串没有字符,更不会有所谓的前后缀了;我们知道当
j = 0 时,next值直接为1(因为前后缀相同的子串长度为0,所以移动到0+1=1的位置,所以next值就为1),所以可以正好并入到 if 的这种情况中;
在这里插入图片描述
经过当面的分析,我们再重新看一下求next数组的代码:

void get_next(char T[], int next[]){ //基于递推思想
	i = 1;
	next[1] = 0;		//第一个字符不匹配,next值设为0
	j = 0;
	while(i <= T[0]){	//递推字符串各个位置
		if(j == 0 || T[i] == T[j])
		++i;
		++j;
		next[j] = i;	//如果i和j位置元素相同,j加1后再赋值给next的下一个位置
	}
	else
		j = next[j];//如果i和j位置元素不同,则去寻找更短的相同前后缀
}

知道next数组的求法后,KMP算法就比较容易理解了,下面看一下KMP算法的代码:

int KMP(char S[], char T[], int next[])			
{                                    
    int i=1,j=1;							//i 和 j 分别为主串和模式串的下标指示器
    while(i <= S[0]&& j <= T[0])	//两个字符串比较的范围即不超过主串长度且j不超过模式串长度
    {
        if(j == 0 || T[i] == T[j])
        {
            ++i;
			++j;        //继续比较后面的字符
        }
        else
			j = next[j];
    }
    if(j>T[0])
         return i-T[0];//匹配结束后如果j大于模式串长度,说明整个模式串都匹配完毕
         				//也就是返回主串中匹配的初始位置,即减去模式串的长度
    else
        return 0;  //没有匹配到,返回0
}

最后进行KMP算法的效率分析:
根据上面的代码,我们可以设变量K = 2i - j;
每次循环迭代,如果时满足 if 的情况, i 的值加1,j 的值加 1,K 的值也会加1;
如果时满足else的情况,由于next[j]的值肯定比 j 小,所以 j 至少减去1,那么K的值至少增加1。
总之,无论那种情况,K的值都是随着算法的不断迭代,总是不断增大的;
算法开始,i = 1, j = 1,所以K初值为1;
算法结束时,i 至多遍历到主串末尾,即 i 最大为主串长n,j 最小是0;所以K = 2
i - j <= O(n),KMP算法还需要得到next数组,计算next数组的方式和KMP算法是类似的过程,所以不难计算next数组的时间复杂度,O(m)(m是模式串的长度),所以KMP算法的时间复杂度O(m+n)
【注】相对于暴力算法直接降为线性的了!

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Faith_xzc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值