浅谈KMP算法

学习KMP之前有一点要知道,我们通常所知道的也容易想的BF算法不是一无是处的,虽然BF算法的时间复杂度是O(n*m),但是在一般情况下,其实际的执行时间近似于O(n+m),因此至今仍被采用。KMP算法仅当模式和主串之间存在许多“部分匹配”的情况,才显得比BF算法快得多。对于ACM竞赛而言,数据会出的很大,在时间上故意卡主BF算法,这是KMP算法就派上用场了。下面分别介绍这两种算法:

1.1. BF算法

例如现在有一个字符串“BBC ABCDAB ABCDABCDABDE”,问它是否包含子串“ABCDABD”。如果用BF算法匹配如下:

     1.

文本串与模式串的第一个字符进行比较,因为B与A不匹配,所以文本串后移一位,模式串不变。

2.

因为B与A还不匹配,所以文本串继续后移,模式串一直不变。

3.

就这样,知道文本串有一个字符跟模式串第一个字符相同为止。

4.

接着比较文本串和模式串的下一个字符,还是相同。

5.

直到文本串有一个字符跟模式串对应的字符不同为止。

6.

BF算法的核心思想来了,文本串回溯到最开始比较的下一位,模式串回溯到第一个字符,继续重新比较。这样做效率比较差,相当于把文本串中已经比较过的字符,再比较一遍。

7.(过渡)

此时K用MP的思想看就是,当空格与D不匹配,不移动文本串的位置指针(即文本串指针不回溯),而是把模式串向右移动(模式串指针回溯)。


       

-------------------------------------------------------------------- 华------丽-----的------分------割------线-------------------------------------------------------------------------------------------------------------------


1.2 KMP算法

1.

那么如何能做到以上这点呢,就要用到上面给出的《部分匹配值表》,表的产生后面再说,这里只要先会用就好。

2.

当发现空格和D不匹配时,可以发现模式串中“ABCDAB”是匹配的。表可知,最后一个匹配字符B所对应的部分匹配值为2,因此按照下面的公式推出向后移动的位数:

模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的部分匹配值

因为6 - 2 = 4 ,所以将模式串向后移动4位。

3.

因为空格与C不匹配。这是以匹配的字符串是“AB”,最后一个匹配字符B所对应的部分匹配值为0,所以移动位数 = 2 - 0 = 2,将模式串向后移动两位。

4.

因为空格与A不匹配,此时失配字符A前面没有已经匹配好的串,所以文本串和模式串皆后移一位。

5.

逐位比较,发现C和D不匹配,于是移动位数 = 6 - 2 = 4,所以将模式串向后移动4位。

6.

直到模式串的最后一位和文本串也匹配上了,说明文本串中存在模式串,匹配结束。如果还要继续匹配(即找出全部匹配数),那么移动为数 = 7 - 0 = 7,将模式串向后移动7位。

7.

下面讲下《部分匹配值表》是如何产生的。首先要明白前后缀的概念,顾名思义,前缀是除了最后一个字符所构成的所有子串的集合,后缀是除了第一个字符所构成的所有字串的集合

8.


部分匹配值就是最长的前后缀共有元素的长度,以“ABCDABD”为例:

- "A"的前缀和后缀都为空集,共有元素的长度为0;

- "AB"的前缀为[A],后缀为[B],共有元素的长度为0;

- "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;

- "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;

- "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;

- "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;

- "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。


9.

如果看过一些大牛的代码后,你会发现他们的部分匹配值和我们给的部分匹配值表不一样,难道说我们的部分匹配值表错了吗?不,最后我们完成了匹配,说明我们的部分匹配值表是正确的,那二者为什么会不一样呢?

还记得上面我们给出的移动公式吗?

模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的部分匹配值

通过公式我们可以看出,我们真正关心的是失配字符上一位字符所对应的部分匹配值。当发生失配时,我们没必要考虑当前失配的字符,而要的是当前失配字符的上一位。

这是我们把原来已经求得的部分匹配值表整体右移一位,初始位赋为-1,这样我们就得到了另一个求移动为数的公式:

模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值


其实两个公式是等价的(很明显),请读者自行证明,不过多赘述。


附求next数组的代码:

void GetNext(char* p,int next[])  
{  
    int pLen = strlen(p);  
    next[0] = -1;  
    int k = -1;  
    int j = 0;  
    while (j < pLen - 1)  
    {  
        //p[k]表示前缀,p[j]表示后缀  
        if (k == -1 || p[j] == p[k])   
        {  
            ++k;  
            ++j;  
            next[j] = k;  
        }  
        else   
        {  
            k = next[k];  
        }  
    }  
}  

1.3 next数组的优化

为了便于读者理解,此处只给出方法,具体数学证明请自行百度,不做赘述。

1.

由于c和b失配,所以模式串右移 j - next[ j ] = 3 - 1 = 2位.

2.


右移两位后,发现c又和b发生失配。这种失配是必然的,为什么这样说呢?因为当p[ j ] 与 s[ i ]失配时,如果此时p[ j ] == p[ next[ j ] ],你还用与p[ j ] 值等同的p[ next[ j ] ]去和s[ i ]进行匹配,那么必然失配。如何可以避免这种情况呢?答案是当p[ j ] == p[ next[ j ] ]时,令next[ j ] = next[ next[ j ] ]。

3.


只要出现了p[next[j]] = p[j]的情况,则把next[j]的值再次递归。例如在求模式串“abab”的第2anext值时,如果是未优化的next值的话,第2a对应的next值为0,相当于第2a失配时,下一步匹配模式串会用p[0]处的a再次跟文本串匹配,必然失配。所以求第2anext值时,需要再次递归:next[2] = next[ next[2] ] = next[0] = -1此后,根据优化后的新next值可知2a失配时,执行“如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++j++,继续匹配下一个字符,同理,第2b对应的next值为0


附代码:

//优化过后的next 数组求法  
void GetNextval(char* p, int next[])  
{  
    int pLen = strlen(p);  
    next[0] = -1;  
    int k = -1;  
    int j = 0;  
    while (j < pLen - 1)  
    {  
        //p[k]表示前缀,p[j]表示后缀    
        if (k == -1 || p[j] == p[k])  
        {  
            ++j;  
            ++k;  
            //较之前next数组求法,改动在下面4行  
            if (p[j] != p[k])  
                next[j] = k;   //之前只有这一行  
            else  
                //因为不能出现p[j] = p[ next[j ]],所以当出现时需要继续递归,k = next[k] = next[next[k]]  
                next[j] = next[k];  
        }  
        else  
        {  
            k = next[k];  
        }  
    }  
}  


1.4 扩展 Sunday算法

  • Sunday算法是从前往后匹配,在匹配失败时关注的是文本串中参加匹配的最末位字符的下一位字符。
    • 如果该字符没有在模式串中出现则直接跳过,即移动位数 = 匹配串长度 + 1;
    • 否则,其移动位数 = 模式串中最右端的该字符到末尾的距离+1。

下面举个例子说明下Sunday算法。假定现在要在文本串"substring searching algorithm"中查找模式串"search"。

    1. 刚开始时,把模式串与文本串左边对齐:
substring searching algorithm
search
^
    2. 结果发现在第2个字符处发现不匹配,不匹配时关注文本串中参加匹配的最末位字符的下一位字符,即标粗的字符 i,因为模式串search中并不存在i,所以模式串直接跳过一大片,向右移动位数 = 匹配串长度 + 1 = 6 + 1 = 7,从 i 之后的那个字符(即字符n)开始下一步的匹配,如下图:

substring searching algorithm
    search
    ^
    3. 结果第一个字符就不匹配,再看文本串中参加匹配的最末位字符的下一位字符,是'r',它出现在模式串中的倒数第3位,于是把模式串向右移动3位(r 到模式串末尾的距离 + 1 = 2 + 1 =3),使两个'r'对齐,如下:
substring searching algorithm
      search
       ^

    4. 匹配成功。

    回顾整个过程,我们只移动了两次模式串就找到了匹配位置,缘于Sunday算法每一步的移动量都比较大,效率很高。





(完)

再次向本文援引的各位大牛博客内容致敬!

                                                                                                    


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值