字符串匹配-KMP算法

目录

一、朴素模式匹配

二、KMP算法

(1)基本思想

(2)各种情况讨论

(3)初步结论(第(2)小点中各种情况的总结)​编辑

(4)求取next数组

三、KMP算法优化


一、朴素模式匹配

        主串S长度为n,子串T长度为m。

        具体代码没啥可说,可以用数组实现,依次比较,不对就后移,直接暴力破解,时间复杂度最差为O((n-m+1) * m),即O(nm)。

二、KMP算法

(1)基本思想

        基于朴素模式匹配进行改进。

        朴素模式匹配是一旦发现这个子串中某个字符不匹配,就只能从头开始转而匹配下一个子串,指针只移动一个位置。图中是如果发现子串(2-7)不匹配时,将会移动到子串(3-8),继续匹配。

图 1

         此处的缺点是即使匹配到第6个字符时才发现不匹配,即前5个字符都匹配时,仍然要回到最开始进行下一个子串(3-8)和模式串的匹配,从而对已知的信息利用不充分。

图 2     

         因此,提出一个观点,不匹配的字符之前,一定是和模式串一致的

         如图2所示,图中进行第一轮匹配时,已经发现前5个字符是匹配的,也已知了主串S的前5个个字符,因此,如果利用好着些信息,我们可以得到如下结论:

        ① 不用从2、3位置开始匹配,从2开始,前两个字符是ba,与模式串不匹配,3开始是aa,也显然不匹配

        ② 也不用从4、5位置开始匹配,因为已知4、5位置的字符ab和模式串前两个是匹配的(在第一次匹配时获得的信息),无需再次比较。

        ③ 以4为子串的开始位置,直接从6的位置开始比较,因为4、5位置匹配正确,而6位置不知道主串中的字符是什么,需要继续跟模式串进行匹配。

        如图3所示

 图 3        

        图中结论对模式串具有通用性,与子串在主串中的位置无关,只要主串和模式串匹配的情况相同,即可直接直接进行同样的操作,即令主串指针i不变,模式串指针j=3。

        如上即为KMP算法的基本思想。

(2)各种情况讨论

情况 a(第6个元素失配):

        abaab? 与abaabc匹配,匹配失败后令主串指针i不变,模式串指针j=3

情况 b(第5个元素失配):

        abaa?与abaabc匹配,匹配失败后i不变,j=2

 

 情况 c(第4个元素失配):

        aba?与abaabc匹配,匹配失败后i不变,j=2

 

  情况 d(第3个元素失配):

        ab?与abaabc匹配,匹配失败后i不变,j=1

 

  情况 e(第2个元素失配):

        a?与abaabc匹配,匹配失败后i不变,j=1

 

  情况 f(第1个元素失配):

        ?与abaabc匹配,直接匹配下一个子串

 

(3)初步结论(第(2)小点中各种情况的总结)

         可见,针对最初的例子,可以直接进入第一种情况,直接跳到i=5,j=3进行匹配。经过对朴素模式匹配优化后,主串指针将不再回溯,即充分的利用了上一次匹配的所有信息。

        注意:以上结论是针对模式串T = ‘abaabc’的,KMP算法就是对对应的模式串进行处理,得到如上的类似结论,从而优化了字符串模式匹配的效率。

        从而,我们可以将结论简化为一个数组来表示,

        对应的代码为:

if(S[i] != T[j]){
    j = next[i];
    if(j == 0){
        i++;
        j++;
    }
}

         因此,我们将问题提炼为:根据模式串T,求取next数组,从而完成字符串匹配,从而匹配流程如下所示:

         对应的,匹配的全部代码如下所示:

int Index_KMP(SString S, SString T, int next[]){
    int i = 1, j = 1;
    while(S <= i.length && j <= T.length){
        if(j == 0 || S.ch[i] == T.ch[j]){
            i++;
            j++;               //继续比较后继字符
        }else{
            j = next[j];       //模式串指针移动
        }
    }
    if(j > T.length)
        return i -T.length;    //匹配成功
 
    else
        return 0;
}

        因此,最坏时间复杂度O(m+n),其中,求解next数组的时间复杂度为O(m),匹配的时间辅助度为O(n)。

(4)求取next数组

        (考研中会手动模拟即可,一般不考代码实现求next数组)

        首先,我们显然可以知道,对于任何模式串,第一个字符不匹配时,只能匹配下一个子串,因此,next[1]恒为 0;

        同理,我们也可以知道,第二个字符不匹配时,下一次应尝试匹配模式串的第一个字符,即从头开始匹配,故next[2] = 1;

        如图,在不匹配的位置前,画一个分界线, 让模式串一步一步后退,直到分界线前面(左边)能对上,或者模式串完全跨过分界线,此时 j 指向哪儿,next数组的值就是多少(注意,在移动模式串时,j 的位置不变,变的只是模式串的位置)。

        从而依次完成next数组的填写。

        值得注意的是,此处只需关注不匹配的是第几个位置的元素,无需在意用来匹配的主串的内容,只需不匹配的是第n个元素时,前n-1个都匹配即可。如当需要计算第4个位置不匹配时next数组的值,即next[4]的值时,前三个位置的主串中子串与模式串前三个位置相同即可。

        图中对于google这个字符串,根据以上规则,对应的next数组的值应为:

三、KMP算法优化

(1)例一

        如图可知 ,当使用传统KMP时,当abaabc模式串进行匹配,第三个位置匹配不上时,根据next数组,j 将指向1,即从主串的3位置和模式串的1位置开始匹配。

        但我们可以知道,因为第三个位置匹配不上时,显然S[3]的值不等于T[3],即不为a,此时,如果继续跟 j = 1位置的模式串进行匹配,那显然也会失败,因为模式串1位置的值也为a,而这些都是我们已知的信息,因此这种情况下,KMP做了一次没有必要的比较。所以,这就是可以优化的点。

        这种情况下,当匹配失败时,下一次匹配为第一个位置匹配失败,next数组中next[1]为0,故我们可以让next[3]的值直接变为0,跳过一次无必要的比较,即直接进行下一个子串的比较。

(2)例二

 

         同例一,我们同样可以看到,此时第5个位置不匹配,根据next数组,j = 2 ,跳到该位置进行匹配,但此时我们看到T[2]的值同样为不匹配的T[5]的值,即b,故此次匹配必然失败。

        同理,匹配失败后,下一次匹配为第2个位置匹配失败,需要让 j = 1,故可以直接将next[5]的值写为next[2]的值,即1。当第5个位置匹配失败,直接 j = 1,继续匹配。 

(3)结论

        需要确定next数组对应位置失配时,它指向的 j 位置,对应的字符是否与失配的字符相等,如果相等,则可以直接将next[n](n为失配的位置)的值,等于next[next[j]],如例二中,5失配,对应的next[5] = 2,next[next[5]] = next[2] = 1,即让next[5] = 1。否则,不进行修改。

        总结可知,优化KMP算法,实质上就是优化next数组。

        可以自行比较一下aaaab的KMP优化前后优化后的效果加深记忆。

 (4)优化next数组代码

nextval[1] = 0;  //优化后的数组nextval
for(int j = 2; j <= T.length; j++){
    if(T.ch[next[j]] == T.ch[j]){
        nextval[j] = nextval[next[j]];
    }else{
        nextval[j] = next[j];
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值