算法日记(六)之串的模式匹配(二)

在前一期文章博主发表了串的模式匹配——BF算法,相信大家很清楚明白。今天我们来看看另一种匹配算法,另一块难啃的骨头——KMP算法。KMP算法相信大家都有多少了解过。博主刚开始学也是很模模糊糊,记得自从大一寒假学完数据结构到现在,KMP算法至今琢磨过三四遍,才初步弄懂原理。如果你是第一次接触KMP算法,我想劝你不要放弃,每隔一段时间多去琢磨琢磨,随着次数增加,理解程度也会更深。这种算法其实讲起来也不是很好讲,更多要靠自己去想,自己去悟。下面博主尽量用易懂的语言来讲讲吧。

首先,先引入一个概念:一个字符串最长相等前缀和后缀。

百度上的公式是这样的:

(看不懂,也没关系,我们继续往下看)我给大家个例子。字符串 abcdab
前缀的集合:{a,ab,abc,abcd,abcda}
后缀的集合:{b,ab,dab,cdab,bcdab}
那么最长相等前后缀不就是ab啦,这理解了,那么子串回溯到哪个字符就有着落了。下面继续往下看。

这个图代表主串"abcabeabcabcmn"和子串"abcabcmn"。

 可以看到c的左右边都有红色的ab吧。据KMP的思想我们要将子串向后移动,现在解决要移动多少的问题。之前提到的最长相等前后缀的概念有用处了。因为红色部分也会有最长相等前后缀。

 灰色部分就是红色部分字符串的最长相等前后缀,我们子串移动的结果就是让子串的红色部分最长相等前缀和主串红色部分最长相等后缀对齐。

 灰色部分就是红色部分字符串的最长相等前后缀,我们子串移动的结果就是让子串的红色部分最长相等前缀和主串红色部分最长相等后缀对齐。

这一步弄懂了,KMP算法的精髓就差不多掌握了。接下来的流程就是一个循环过程了。事实上,每一个字符前的字符串都有最长相等前后缀,而且最长相等前后缀的长度是我们移位的关键,所以我们单独用一个next数组存储子串的最长相等前后缀的长度。而且next数组的数值只与子串本身有关。所以next[i]=j,含义是:下标为i 的字符前的字符串最长相等前后缀的长度为j。
我们可以算出,子串t= "abcabcmn"的next数组为next[0]=-1(前面没有字符串单独处理)
next[1]=0;next[2]=0;next[3]=0;next[4]=1;next[5]=2;next[6]=3;next[7]=0;(注意数组是下标是从0开始的)。

借用一下《大话数据结构》的内容加强理解一下,hhh


 

 接下来继续讲,我们把子串移动,也就是让s[5]与t[5]前面字符串的最长相等前缀后一个字符再比较,而该字符的位置就是t[?],很明显这里的?是2,就是不匹配的字符前的字符串 最长相等前后缀的长度。

 也是不匹配的字符处的next数组next[5]应该保存的值,也是子串回溯后应该对应的字符的下标。 所以?=next[5]=2。接下来就是比对是s[5]和t[next[5]]的字符。这里也是最奇妙的地方,也是为什么KMP算法的代码可以那么简洁优雅的关键。
我们可以总结一下,next数组作用有两个:
一是之前提到的,next[i]的值表示下标为i的字符前的字符串最长相等前后缀的长度。

二是表示该处字符不匹配时应该回溯到的字符的下标

next有这两个作用的源头是:之前提到的字符串的最长相等前后缀
想一想是不是觉得这个算法好厉害,从而不得不由衷佩服KMP算法的创始人们。

下面是求next数组的代码:

typedef struct
{    
    char data[MaxSize];
    int length;            //串长
} SqString;
//SqString 是串的数据结构
//typedef重命名结构体变量,可以用SqString t定义一个结构体。
void GetNext(SqString t,int next[])        //由模式串t求出next值
{
    int j,k;
    j=0;k=-1;
    next[0]=-1;//第一个字符前无字符串,给值-1
    while (j<t.length-1) 
    //因为next数组中j最大为t.length-1,而每一步next数组赋值都是在j++之后
    //所以最后一次经过while循环时j为t.length-2
    {    
        if (k==-1 || t.data[j]==t.data[k])     //k为-1或比较的字符相等时
        {    
            j++;k++;
            next[j]=k;
            //对应字符匹配情况下,s与t指向同步后移
            //通过字符串"aaaaab"求next数组过程想一下这一步的意义
            //printf("(1) j=%d,k=%d,next[%d]=%d\n",j,k,j,k);
           }
           else
        {
            k=next[k];
            **//我们现在知道next[k]的值代表的是下标为k的字符前面的字符串最长相等前后缀的长度
            //也表示该处字符不匹配时应该回溯到的字符的下标
            //这个值给k后又进行while循环判断,此时t.data[k]即指最长相等前缀后一个字符**
            //为什么要回退此处进行比较,我们往下接着看。其实原理和上面介绍的KMP原理差不多
            //printf("(2) k=%d\n",k);
        }
    }
}
KMP算法解释:

int KMPIndex(SqString s,SqString t)  //KMP算法
{

    int next[MaxSize],i=0,j=0;
    GetNext(t,next);
    while (i<s.length && j<t.length) 
    {
        if (j==-1 || s.data[i]==t.data[j]) 
        {
            i++;j++;              //i,j各增1
        }
        else j=next[j];         //i不变,j后退,现在知道为什么这样让子串回退了吧
    }
    if (j>=t.length)
        return(i-t.length);      //返回匹配模式串的首字符下标
    else  
        return(-1);                //返回不匹配标志
}
关于KMP算法后面还有改进方法,由于博主水平还不高,很难讲懂,这里就不多说了,关于KMP算法的分享就到这啦。大家花个时间自己思考整理一下思路吧

本贴为博主亲手整理。如有错误,请评论区指出,一起进步。谢谢大家的浏览.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值