KMP字符串匹配

KMP字符串查找

问题如下:

给定一个字符串 S S S,以及一个模式串 P P P,所有字符串中只包含大小写英文字母以及阿拉伯数字。模式串 P P P在字符串 S S S中多次作为子串出现。求出模式串 P P P在字符串 S S S中所有出现的位置的起始下标。

这个问题的 b r u t a l − f o r c e brutal-force brutalforce解法是显然的,只要一次次将模式串与原串比对,比对完成则返回第一个下标,比对失败则将模式串向右移动一位,继续进行比对,直至模式串右端达到原串的结尾。

问题是这个算法的效率巨特么低。若S的长度为M,P的长度为N,则时间复杂度是 O ( M N ) O(MN) O(MN)。在M为 1 0 5 10^5 105,N为 1 0 6 10^6 106的情况下,运算次数就会直接爆炸( 1 0 11 10^{11} 1011)。

考虑对 b r u t a l − f o r c e brutal-force brutalforce解法进行优化。这个解法的时间复杂度取决于两方面,一方面是比对操作,一方面是移动操作。原解法的比对操作每次都是从模式串的开头开始比对,且移动操作每次都只移动一位,这是两个使时间复杂度爆炸的重要因素。

每一次比对失败并不是一开始就失败的,若是能将已经比对成功的部分利用起来,想必能够减少时间复杂度。

综上,我们应该去寻找一种能够继承上一次比对成果的算法。

优化

关于下标的问题

我们考虑在匹配过程中对S串使用从0开始的下标,在录入P串时对P串使用从1开始的下标, n e ne ne数组表示相等的前后缀真子串的最大长度。具体原因之后再说。

具体优化过程

基本思路

考虑如图的情况。

在这里插入图片描述

现在有 i i i j j j两个指针,现寻找一种算法,使 i i i指针的移动是单调的,通过移动 j j j指针实现对上一次匹配结果的继承。

为了结果的简便,我们对比 P [ j + 1 ] P[j+1] P[j+1] S [ i ] S[i] S[i]

若当模式串P上的指针指到 j j j,S串指到i时,比对失败,则直到 j j j都已经比对成功。我们有 S [ i − j + 1.. i ] = P [ 1.. j ] S[i-j+1..i]=P[1..j] S[ij+1..i]=P[1..j]。此时我们要移动P串。但是,我们不需要只移动1位——我们要移动更多的位数,这个位数将是由已知信息推出的第一个可能使匹配成功的位数,移动的位数比这个数字小将必定匹配失败,比这个数字大将可能漏解

下面证明我们应将 j j j移动至 n e [ j ] ne[j] ne[j],其中 n e ( i ) ne(i) ne(i)满足:
n e ( i ) = m a x ( { l    ∣   P [ 1.. l ] = P ( i + 1 − l . . i ) } ) ne(i) = max(\{l\;|\, P[1..l] = P(i +1- l..i)\}) ne(i)=max({lP[1..l]=P(i+1l..i)})
注意:此时P的下标从1开始!!

即,我们考察 P [ 1.. i ] P[1..i] P[1..i]中所有内容相同的的前缀字符串和后缀字符串,这些字符串中的最大长度即为 r e ( i ) re(i) re(i)

移动的长度小于这个数字,如图,移动浅蓝色的距离。

在这里插入图片描述

若留在已匹配部分的P串能够匹配成功,则此时浅蓝色部分显然满足 P [ 1.. l ] = P ( i + 1 − l . . i ) P[1..l] = P(i + 1 - l..i) P[1..l]=P(i+1l..i),这个长度显然大于 n e ( i ) ne(i) ne(i),与定义矛盾!故而必然匹配失败。移动这个位数将可能匹配成功(移动后的P串留在已匹配部分的内容全部比对成功,之后的内容还未经过比对),故比这个数字大的移动位数将可能漏解。

循环至可能匹配成功

之后若匹配依旧不成功( P [ j + 1 ] ≠ S [ i ] P[j+1] \ne S[i] P[j+1]=S[i]),则我们将继续移动j指针。最坏情况是将j指针移动至0,重新开始匹配。

和原串匹配

有两个循环的条件,一是 P [ j + 1 ] ≠ S [ i ] P[j+1] \ne S[i] P[j+1]=S[i],二是 j > 0 j > 0 j>0。跳出循环时,至少有一个为假,若第一个为假,则完成循环,j向右移动一位;若第二个为假,则有 j = 0 j = 0 j=0,此时需进行判断p[j + 1] == s[i],若为真则j向右移动一位,若为假则只能移动i了。

在以上步骤进行完毕后,对j进行边界检测,若 j = n j = n j=n,则输出结果。

代码如下:

for (int i = 0, j = 0; i < m; i++){
        while (j > 0 && p[j + 1] != s[i]) j = ne[j];
        if (j < n && p[j + 1] == s[i]){
            j++;
        }
        if (j == n){
            printf("%d ", i - (n- 1));
            j = ne[j];
        }
    }

求解 n e ( i ) ne(i) ne(i)

在求解 n e ( i ) ne(i) ne(i)的过程中想到使用动态规划是自然的(或者说要使用最基本的信息推所有的信息)。考虑求 n e ( i ) ne(i) ne(i),若 P [ i ] = P [ n e ( i − 1 ) + 1 ] P[i] = P[ne(i - 1) + 1] P[i]=P[ne(i1)+1],则 n e ( i ) = n e ( i − 1 ) + 1 ne(i) = ne( i - 1) + 1 ne(i)=ne(i1)+1。若不满足条件,则必然有 n e ( i ) − 1 < n e ( i − 1 ) ne(i) - 1 < ne(i - 1) ne(i)1<ne(i1)。下面来证明这一点。

显然 n e ( i ) − 1 ≠ n e ( i − 1 ) ne(i) - 1 \ne ne(i - 1) ne(i)1=ne(i1),若 n e ( i ) − 1 > n e ( i − 1 ) ne(i) - 1 > ne(i - 1) ne(i)1>ne(i1),则由 n e ne ne数组的定义, n e ( i − 1 ) = n e ( i ) − 1 ne(i - 1) = ne(i) - 1 ne(i1)=ne(i)1,矛盾!,故而 n e ( i ) − 1 < n e ( i − 1 ) ne(i)-1<ne(i-1) ne(i)1<ne(i1)

在这里插入图片描述

接下来寻找不满足条件之后的解决方法。我们要在 n e ( i − 1 ) ne(i - 1) ne(i1)范围内找最大的前后缀相等的字符串,则根据 r e re re数组的定义,这个字符串的长度为 n e ( n e ( i − 1 ) ) ne(ne(i - 1)) ne(ne(i1))。若此时依旧没有 P [ i ] ≠ P [ n e ( n e ( i − 1 ) ) + 1 ] P[i] \ne P[ne(ne(i-1))+1] P[i]=P[ne(ne(i1))+1],则继续这个过程(在 n e ( n e ( i − 1 ) ) ne(ne(i -1)) ne(ne(i1))里找。最终结束循环的条件应有两个:一个是满足 P [ i ] = P [ n e ( n e ( i − 1 ) ) + 1 ] P[i] = P[ne(ne(i-1))+1] P[i]=P[ne(ne(i1))+1],另一个是迭代之后从 n e ne ne数组取出的值为0。如果没有第二个条件,则若一直无法满足第一个条件( P [ i ] ≠ P [ 0 + 1 ] P[i] \ne P[0 + 1] P[i]=P[0+1],这是很有可能的),我们将进入死循环(取出的值依旧为0).

跳出循环后,我们只知道两个条件中至少有一个为假。故而我们再进行一次判断(p[i] == p[end + 1])(其中end是最后从 n e ne ne数组中取出的值),若这个判断为真,则我们有 n e [ i ] = e n d + 1 ne[i] = end + 1 ne[i]=end+1,若为假,则必有 P [ i ] ≠ P [ e n d + 1 ] ∧ e n d = 0 P[i] \ne P[end + 1] \wedge end =0 P[i]=P[end+1]end=0,故而有 n e [ i ] = 0 ne[i] = 0 ne[i]=0

代码如下:

for (int i = 2; i <= n; i++){
        int end = ne[i - 1];
        while (p[i] != p[end + 1] && end > 0){
            end = ne[end];
        }
        if (p[i] == p[end + 1]) ne[i] = end + 1;
        else  ne[i] = 0;
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值