串的模式匹配算法(简单模式匹配、KMP、改进的KMP)

一、简单模式匹配

  思想:从主串的第一个位置起和模式串的第一个字符进行匹配,如果相等则继续匹配,否则从主串的第二个字符开始逐一比较。以此类推,知道全部匹配完成。

如:str: ababc  str1:abc

第一轮匹配:匹配失败,下次从str的第二个元素继续进行匹配

      ababc

      abc

第二轮匹配:匹配失败,下次从str的第三个元素继续进行匹配

     ababc

       abc

第三轮匹配:匹配成功

     ababc

         abc

Coding

int find(Str str,Str substr)
{
	int i,j,k;
	i=0;j=0;k=i;
	while(i<str.length&&j<substr.length)  
	{
		if(str.ch[i]==substr.ch[j])
		{
			++i;
			++j;
		}
		else
		{
			j=0;
			i=++k; //k储存着每次 i 开始匹配的位置,每一次都从i上次开始位置的后一位置开始匹配 
		}
	}
	if(j==substr.length) //全部匹配完子串。找到位置了 
	{
		return k; //返回子串在主串的位置 
	}
	return 0;
}

时间复杂度:O(m*n)

空间复杂度:O(1) 

二、KMP模式

建议看这个博客:https://blog.csdn.net/starstar1992/article/details/54913261

我也是看这上面的,大概看懂了,但是有些代码还是解释不清楚。我只做代码记录,用于以后复习。

重点:next数组确定,KMP对next数组的使用

#include<stdio.h>
#include<malloc.h>
typedef struct{
    char *ch;//指向动态分配储存区首地址的字符指针
    int length;//串的长度
}Str;

//存储发生不匹配情况时,元素下标需要移动的距离。
int next[20]={-1};
 
/*计算子串需要向后移动空间,实际上子串是不会移动,是模拟移动的过程,修改子串匹配元素的下标,比如当
前元素下标是3,发生不匹配情况,比如前面下标为0的元素与3前面那个元素(下标为2的元素)相同,则,修
改下标为当前下标为0+1=1*/
void cal_next(Str str)
{
    next[0] = -1;//next[0]初始化为-1,-1表示不存在相同的最大前缀和最大后缀
    int k = -1;//k初始化为-1
    for (int q = 1; q < str.length-1; q++) //str最后一个字符是“\0 ” 
    {
        while (k > -1 && str.ch[k + 1] != str.ch[q])//如果下一个不同,那么k就变成next[k],注意next[k]是小于k的,无论k取任何值。
        {
            k = next[k];//往前回溯
        }
        if (str.ch[k + 1] == str.ch[q])//如果相同,k++
        {
            k = k + 1;
        }
        next[q] = k;//这个是把算的k的值(就是相同的最大前缀和最大后缀长)赋给next[q]
        printf("%d ",next[q]);
    }
}

//KMP匹配
int KMP(Str str,Str subStr)
{
	cal_next(subStr);//先生成next数组
	int i,j,k;
	k=-1;
	for(i = 0;i<str.length-1;i++)
	{
		while(k>-1&&str.ch[i]!=subStr.ch[k+1]) //这里逻辑比较难懂,
		{
			k=next[k]; //匹配串下标前移,
		}
		if(str.ch[i]==subStr.ch[k+1])//两个元素匹配,则向后移继续去匹配下一个元素
			k=k+1;
		if(k==subStr.length-2)//说明K已经移动到了最末端
		{
			return i-subStr.length+2;//为什么减2,因为字符串末尾是“\0 ”不会进行比较。 
		} 
	}
	return -1; 
}



int main(){
	Str str;
	str.ch=(char*)malloc(sizeof(char)*(38));//主串
    char a[38]="bacbababadababacambabacaddababacasdsd";//主串字符
    char *b="ababaca"; //待匹配得到子串字符
	for(int i=0;i<37;i++)
	{
		str.ch[i]=a[i];
	}
	str.ch[37]='\0';
	str.length=38;
	
	Str subStr; //子串
	subStr.ch=(char*)malloc(sizeof(char)*(8));
	for(int i=0;i<7;i++)
	{
		subStr.ch[i]=b[i];
	}
	subStr.ch[7]='\0';
	subStr.length=8;
	printf("%d",KMP(str,subStr)); //KMP查找
}

时间复杂度:O(m+n)

三、KMP实现二(next数组算法)

(天勤数据结构书上的理解,我应该是理解懂了,比上面的代码更懂些,如果读者想要懂的话,建议认真读完下面的话,读完理解了,肯定懂,如果不能坚持,建议去看视频或者其他教程),直接用书上的图把。

1.引子:首先形象的解释一下上面简单模式匹配:设主串为s1s2....sn,模式串为p1p2...pm,i j分别为主串和模式串中当前参与比较的两个字符的下标。我们假设在sipj 匹配。si-j+1 ~ si-1的元素与p1 ~ pj-1的元素都分别匹配。每当出现这种情况的时候,简单模式匹配的做法是:一律将i赋值为i-j+2j赋值为1相当于把模式串整体后移一个单位,让i-j+2与p1进行匹配,重新开始比较。再次出现这个情况时,再次后移。

 2.进步状态:如果在模式串后移的过程中又出现了模式串前部某子串p1p2...与主串尾部某子串....si-2 si-1相匹配的状态,这就是一个进步状态,为什么,因为出现这种状态时,意味着在原始位置Si 和 Pj不匹配处,存在新的匹配机会,且与Si匹配的模式串下标会变的小于<j,至于小多少,就看存在多少前部某子串p1p2...与主串中某子串....si-2 si-1相匹配的元素(假设元素个数为x,模式串下标从0开始)j下标就修改为x, 减小j-x, x是>=1的,这句话大家应该能懂把(上图中,Si与Pj不匹配,则P字符串就要往后移动,pj就与Si后面的元素匹配,与Si匹配的元素就变成了pj前面的元素,对吧)

 3.进步状态,如何进步?

   例:主串:aabababcbb   模式串:ababc

   第1次匹配 : 位置2不匹配

      aabababcbb

      ababc

    第2次匹配,元素发生不匹配处前面只有一个元素,进步状态不存在,只能像简单匹配一样,模式串整体往后移一个单位,即pj下标变为0,原本是模式串的第二个元素(下标1)

       aabababcbb

         ababc

   第3次匹配,上轮结果可知,模式串前面4个元素都匹配,我们分析一下进步状态,s3-s4p0-p1刚好完全相等,这里为什么不是S0~s4与P0~P3呢,同志们注意看上面进步状态描述,模式串的前部与主串子串的尾部。这能说明什么问题呢,说明s3-s4p0-p1匹配,则模式串是不是可以整体后移两个单位,让s3-s4p0-p1去匹配,p2及后面的元素继续匹配主串S4后面的元素,及修改pj下标4为2,直接从主串i下标处开始匹配,不用匹配前面的元素了。这样是不是就比简单匹配模式更高效了。同时注意我们始终以与Si处匹配的模式串下标进行研究。

这里引出最长前缀最长后缀,进步的距离就是模式串中当前不匹配元素前的子串,该子串的最长前缀与最长后缀相同,他们元素的个数。所以进步的关键就是找发生不匹配元素前的子串的相同最长前缀和最长后缀元素个数

在这里:abab就是模式串不匹配元素前的子串,最长后缀与最长前缀相同的子串:ab,修改当前pj的下标4为2,j减少了两个,所以进步2.

小例子:aaabbbc  最长前缀:aaabbb及其的所有子串,最长后缀:aabbbc及其所有子串,相等的最长前后缀为空。

   aabababcbb

         ababc

    匹配完成,

问:现在有的同学可能会怀疑这种匹配(进步)过程中出现遗漏,导致中间存在匹配却没有匹配上。

答:这个是肯定不会遗漏的,为什么,因为是拿最长前部子串,与发生不匹配处之前的元素进行匹配,这就相当于你拿着模式串,(刚才的例子,第三轮)原本挪2两步的,只是你一眼看出,2个位置后的主串与这个模式串开头的两个元素是相匹配的,直接一次性移动了两步,减少了比较次数。我们可以一眼看出,那计算机怎么一眼看出,嗯,继续上个例子,在第2轮匹配后出现s3-s4p2-p3匹配,在S5与p4发生不匹配,人眼观察知道p2-p3与p0-p1相同对吧,才有了进步,这问题就到了计算机如何求最长前缀和最长后缀的问题了。

4. 字符串的最长前缀和最长后缀:

  借上面博客链接的例子说明前后缀问题

**注意最长前缀:是说以第一个字符开始,但是不包含最后一个字符。 比如aaaa相同的最长前缀和最长后缀是aaa。
** 对于目标字符串ptr,ababaca,长度是7,
 a,ab,aba,abab,ababa,ababac,ababaca的相同的最长前缀和最长后缀的长度。
由于a,ab,aba,abab,ababa,ababac,ababaca的相同的最长前缀和最长后是是“”,“”,“a”,“ab”,“aba”,“”,“a”

      对于ababc这个模式串,先分解,因为在任意位置,都可能出现不匹配的情况,所以我们在开始匹配前,就求出所有的进步可能。用next数组存储。next取值规则如下

      特殊情况1(不存在相同的最长前缀和最长后缀)即模式串直接从主串不匹配位置处模式串第一个元素进行比较,将j下标改为0,next值赋0,即模式串整体向后移动 j-1个位置。

     特殊情况2:模式串中的第一个字符与主串 i 位置不匹配,模式串整体往后移动一个位置开始匹配,即简单模式匹配。标记next为-1

模式串       a   b   a  b   c

对应下标:0   1   2  3   4

1)位置0发生不匹配时,即第一个字符发生不匹配,next[0]= -1;(特殊情况2)

2)位置1发生不匹配时,前面只有一个元素a,即特殊情况1,next[1]=0 ;

3)   位置2发生不匹配时,前面“ab”没有相同的最长前后缀,即特殊情况1,  next[2]= 0 ;

4)    位置3发生不匹配时,前面“aba”有一个相同的最长前后缀"a",next[3]=1;

5)    位置4发生不匹配时,前面“abab”有两个相同的最长前后缀"ab",next[4]=2;

大家可以看出,在任意位置的相同的最长前后缀的值有( next[i] < i )

coding:看不懂代码,看下面的例子理解。代码来自《天勤数据结构》,稍作修改

void getNext(Str str)
{
	int i,k=-1;
	int j=0; 
	next[0]=-1; //第一种特殊情况
	i=0;
	while(i<str.length-1) //str最后一个位置是“\0”,我也计算了长度的,所以是length-1
	{
		if(k == -1 || str.ch[i] == str.ch[k]) //当k为-1,表示第二种特殊情况,i直接进1,k直接+1变成0,模式串匹配位置下标设为0。
		{
			++i;  //注意这里是唯一改变i值的地方
			++k;
			next[i]=k;
		}
		else
		{
			k=next[k];
		 } 
	}
        //输出next数组
	for( i=0;i<str.length-1;i++)
	{
		printf("%d ",next[i]);
	}
 } 

注:next数组的求值过程,下一步的求next值,用到了上一步、上上步、及之前步骤结果。

例子:比如ababc

由上知:next[0]= -1,next[1]=0,next[2]=0,next[3]=1,next[4]=2;

我们分析next[4],前提回顾,我们要求next[4],我们就去算0~3下标的最长前后缀长度。

1.首先进入while循环,这里i=3,k=1。(当前状态)

2.我们使用if语句,查看条件是否符合,k> -1,  str.ch[3]==str[1]?   明显不等,这里我说一个逻辑

3.为什么要用str.ch[3]去匹配str.ch[1] ? 因为next[3]==1,说明,0-2只有一个最长前后缀,他们是ch[0] 、ch[2],我们用ch[3]去匹配ch[1],如果成功,是不是说明下标01 == 下标23的子串,进入if,++i, ++k,  所以直接赋值next[4]=2。

 4.如果不成功,我们就用next[k]去匹配,k=1, next[1]=0,即用初始元素去匹配next[3],如果成功是不是说明,有一个最长前后缀,不成功k=next[0]= -1 , 然后遇到特殊情况2,执行i++,k++ 然后赋值next[3] = 0。对吧。

 

四、久违的KMP实现

代码来自《天勤数据结构》,稍作修改,有问题请留言。

int KMP2(Str str,Str subStr)
{
	getNext(subStr); //先计算next数组 
	int i=0,j=0;
	while(i < str.length-1 && j < subStr.length-1) 
	{
                /*j==-1,如果出现特殊情况1,j值为-1,即从主串的下一个元素与开始与模式串第一个元
                    素匹配。
                 *str.ch[i]==subStr.ch[j],如果匹配,则继续匹配下一个元素,j++, i++
                 *如果不匹配,j!=-1 ,即改变j的值为next数组中的值,然后继续做判断。
                */
		if(j==-1 || str.ch[i]==subStr.ch[j]) 
		{
			++j;
			++i;
		}
		else
		{
			j=next[j];
		}
	}
	if(j==subStr.length-1) //通过j的值判断,j是否遍历到最后一个位置“\n”,判断是否匹配成功。
	{
		return i-subStr.length+1;
	}
	else
	{
		return 0;
	}
}


 五、KMP改进算法实现

举个例子字符串p: AAAABB

next[0] = -1, next[1] = 0, next[2] = 1, next[3] = 2, next[4] =3, next[5] =0, 

我们求进行KMP匹配时,当p3 与 主串Si 不匹配时,按照上述KMP算法有:

 1.修改p3下标为 next[3]=2,主串si 与 p[ next[3] ] = p2 比较,即si 与 p[2]不匹配,

 2. 修改p2下标为next[2]=1,主串si 与 P[ next[ next[3] ] ] = p1比较,si 与 p[1]不匹配,

 3. 修改p1下标为next[1]=0,主串si 与 P[ next[ next[ next[3] ] ] ] = p0比较,p[3]与p[0]不匹配, 到了这里,大家就可以看出问题。

我们可以看到,字符串本身:AAAABB,前面4个是一样的,即这个KMP仍可优化,如果我们一开始就知到 p[3] = p[ next[3] ]  = P[ next[ next[3] ] P[ next[ next[ next[3] ] ] ] ,即在KMP匹配的过程中,优化这个过程,就可以减少3次无效匹配。

问题引申:在KMP算法中,若主串si 与 模式串pj不匹配,k1等于next[j] = pj , 且仍然与P[k1]不匹配,k2等于next[pk1] ,   且p[k2]等于p[k1],则si与p[next[k2]]不匹配,则一直比较下去,直到Pj 与Pkn不等(kn = next[ next[ ...next[j]...] ],嵌套了n个next),或者kn = -1时,则next[j] 重置为kn 。

问题解决:一般next数组不变,而用名为nextval的数组来保存更新后的next数组,即当pj 与 pn 不等时,nextval[j] 赋值为kn

nextVal数组求值步骤:

1) 当 j = -1 时,nextVal[j]赋值为-1,作为特殊标记 

2) 当pj不等于pk时(k等于next[j]),nextVal[j]赋值于K

3) 当pj等于pk时(k等于next[j]),nextVal[j]赋值于nextVal[k].

Coding: 代码理解,请听明晚分析

void getnextval(Str str)
{
	int i,k=-1;
	int j=0; 
	nextVal[0]=-1;
	i=0;
	while(i<str.length-1)
	{
		if(k == -1 || str.ch[i] == str.ch[k])
		{
			++i;
			++k;
			if(str.ch[i]!=str.ch[k])
				nextVal[i]=k;
			else
				nextVal[i] = nextVal[k];	
		}
		else
		{
			k=nextVal[k];
		 } 
	}
	for( i=0;i<str.length-1;i++)
	{
		printf("%d ",nextVal[i]);
	}
	printf("\n");
}

 

 

 

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值