字符串KMP算法详解

串是由零个或多个字符组成的有限序列,又名叫字符串

朴素的模式匹配算法

串经常用来匹配子串再主串中是否存在,假设两个串存在数组中,数组下标0的位置存了串的长度。


    /**
     * 串的朴素模式匹配算法
     * 返回子串T在子串S中第pos个字符之后的位置。若不存在,则返回0
     * T非空, 1 <= pos <= strlength(S)
     */
    function Index(string $S, string $T, int $pos) {
        $i = $pos;  //i为主串中当前位置下标 若pos不为1,则从pos位置开始匹配
        $j = 1;   //j用于子串中当前位置下标
        //判断长度
        while ($i <= $S[0] && $j <= $T[0]) {
            if ($S[$i] == $T[$j]) {
                //如果第一个字母相等则对比下一个字母
                $i++;
                $j++;
            }else {
                //把i回到上一个匹配的字符的下一个
                $i = $i - $j + 2;
                //j退回首位
                $j = 1;
            }
        }
        if ($j > $T[0]) {
            //匹配上了,返回下标
            return $i - $T[0];
        } else {
            //没匹配上, 返回0
            return 0;
        }
    }

想一下这个算法的时间复杂度,最好的情况肯定是直接匹配到也就是O(1),那么最坏的情况呢,如果主串为“0000000000000000000000000001”,而子串为“000000000001”,设n是主串的长度,m是子串的长度,那么当i = 1的时候,要匹配m次发现匹配失败,当i=2时候还是m次,一直匹配到n - m + 1的位置,那么总共匹配了(n - m + 1) * m 次,它最坏的时间复杂度也就是O((n - m + 1) * m)。

KMP模式匹配算法

既然上面那个算法时间复杂度太差,那么有没有更好的呢,肯定是有的,也就是KMP算法,KMP算法由D.E.Knuth、J.H.Morris 和 V.R.Pratt(其中Knuth和Pratt共同研究,Morris独立研究)他们三个研究发表,可以大大避免重复遍历的情况,我们把它称为克努特-莫里斯-普拉特算法,简称KMP算法。

KMP算法匹配原理

主串S=“abcdefgab”,子串T=“abcdex”,那么如果用前面的朴素算法的话,前5个字母,两个串完全相等,直到第6个字母,'f’与’x’不等。

如果我们知道T串中首字符’a’与T中后面的字符均不相等。而T串中的第二位的’b’与S串中第二位的’b’相等,那么也就意味着,T串中首字符’a’与S串中的第二位’b’是不需要判断也知道它们是不可能相等了。

同样道理,在我们知道T串中首字符’a’与T中后面的字符均不相等的前提下,T串的’a’与S串后面的’c’,‘d’,'e’也都可以确定是不相等的。

再看下一个例子

假设S=‘abcabcabc’,T=‘abcabx’。对于开始的判断,前5个字符完全相等,第6个不等,此时,根据上一个列子的经验,T的首字符’a’与T的第二位’b’,第三位’c’均不等,所以不需要做判断

因为T的首位’a’与第四位’a’相等,第二位的’b’与第五位的’b’相等。而在第一步时,第四位的’a’与第五位的’b’已经与主串中的相应位置比较过了,是相等的,因此,T的首字符’a’第二位的’b’与主串中的第四位和第五位也不需要比较了,肯定是相等的–之前比较过了,还判断什么。

对比这两个例子,可以发现,在第一步时候,都是当i=6时发现两个字符不相等,而剩下的2,3,4,5步时,i=2,3,4,5。其实都可以省略

即我们在朴素模式匹配中,主串的i值是不断的回溯来完成的。而我们的分析发现,这种回溯其实可以不需要,KMP模式匹配算法就是为了让这没必要的回溯不发生。

既然i值不回溯,也就是不可以变小,那么要考虑的变化就是j值了。根据我们前面说的,很显然j值得变化和自身有关,j值得多少取决于当前字符之前的串的前后缀的相似度。比如第一个例子,'abcdex'j值就是1,那第二个例子'abcabx'j值是3,因为ab = ab.

我们把T串各个位置的j值得变化定义为一个数组next,我们现在要求出这个next数组

next数组值推导

在子串中查找有没有和首位字符相等的字符,如果没有,next[j] = 1,如果有,那么n个字符相等,next[j] = n + 1,比如abcabx,next[1] = 0, 2 = 1, 3 = 1, 4 = 1, 5 = 2,因为next[5]的时候,选取的字符是 abca 从j = 1到 j = 4的字符,而abca中前后的a相等,那么一个字符相等,next[5] = 1 + 1= 2,我们选取字符时候是选择从首字符到j - 1个字符的比较。那么next[6] = 3,因为j = 6的时候比较字符abcab,前后的ab两个字符相等,那么next[6] = n +1 = 2+1=3。这个例子的next数组就是[0,1,1,1,2,3]。

看下一个例子,找出 T = 'ababaaaba’的next数组

  • j = 1, next[1] = 0
  • j = 2, 取字符a, next[2] = 1
  • j = 3, 取字符ab, next[3] = 1
  • j = 4, 取字符aba, 因为 a = a, next[4] = n + 1 = 1+1= 2
  • j = 5, 取字符abab, 因为ab = ab, next[5] = n + 1 = 2 + 1 =3
  • j = 6, 取字符ababa, 因为aba = aba, next[6] = n + 1 = 3+1= 4
  • j = 7,取字符ababaa,因为a = a, next[7] = 2
  • j = 8,取字符ababaaa, 同上,next[8] = 2
  • j = 9,取字符ababaaab,因为ab = ab, next[9] = 2 + 1=3
  • 最终next = [0,1,1,2,3,4,2,2,3]

再来一个例子, 找出T='aaaaaaaab’的next数组

  • j = 1,next[1] = 0
  • j = 2,next[2] = 1
  • j = 3,取aa, a = a, next[3] = 1+1=2
  • j = 4,aaa, aa = aa, next[4] = 2 + 1=3
  • j=5,aaaa, aaa = aaa, next[5] = 3+1=4
  • j=6,aaaaa, aaaa = aaaa,next[6] = 4+1=5
  • j=7,aaaaaa, aaaaa = aaaaa,next[7] = 5+1=6
  • j=8,aaaaaaa, aaaaaa = aaaaaa,next[8] = 6+1=7
  • j=9,aaaaaaaa, aaaaaaa = aaaaaaa,next[9] = 7+1=8
  • next数组 = [0,1,2,3,4,5,6,7,8]

可以看到,其实,next[1] 都是0,next[2]都是1,从next[3]开始才有变化,有人问了,aaa里面为啥是aa = aa,而不是 aaa = aaa,是因为比较的时候取前缀字符时不能包含最后的字符,取后缀字符时候同理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值