算法大法好之KMP

1,103 阅读8分钟

KMP算法的主要应用场景:

在一段(主字符串)中查找是否包含某一段(子串)

举例朴素模式:

在了解KMP之前,可以想象使用最简单的方法就是,子串与主串挨个对比,当子串某一位字符对比主串字符失败后,

主串将对比的位置回溯到上一次(开始的位置+1)。

子串将对比的位置回溯到0

一直到匹配成功,或者遍历完所有字符。

对于正常的字符串模式匹配,主串长度为m,子串为n,时间复杂度会到达O(m*n)。

图例 :

    S串:abcdefgab

    T串:abcdex

     



分析:

     对于要匹配的子串T来说,"abcdex" 首字母a与后面的任意一个字符都是不相等的,既然首字母a不与后面的任意字符相等 (并且我们通过对比图中,已知主串的2-5位与子串的2-5位是判断相等的),那么也就意味着首字母a不可能与主串2-5的位置相等,那么朴素算法的2️⃣3️⃣4️⃣5️⃣判断对比都是多余的。


示例二:

另一种情况,如果T串后面含有与首字母"a"的字符

S = "abcabcabc"

T=  "abcabx"    

使用朴素算法进行对比


分析:

1,根据示例一的分析T串的首字母"a"与自己后面的第二位,第三位字符都不相等,所以2️⃣3️⃣判定为多余的对比。

2,然后发现T的首字母"a"与自己第四位字符"a"相等,第二位"b"与第五位"b"相等,观察1️⃣发现,T串的第四位"a",第五位"b" 与 S主串中相应的位置比较过为相等。因此可以判断出T的首字符"a",第二位字符"b" 与 主串S的第四位,第五位也是相等的,所以该步骤也可以忽略。

3,也就是说,T的子串中有,与首字母相等的字符,也可以省略一部分不必要的对比判断。

4,对比以上两个例子,发现朴素匹配法主串的(角标i值),在不断的回溯,而通过对比的规律发现这些回溯是可以避免发生的。

5,通过两个示例分析发现,S串,与T串对比时,只需要关注子串(下标值K的变化)

通过示例二分析发现,可以根据T串的结构中是否有重复字符的出现,从而决定K值的变化



KMP算法能将时间复杂度降低到O(m+n)

KMP算法的核心之一(next[]数组)

next数组的作用:是将指引你下一次探深对比的位置,

搞懂next数组的第一步,必须先要知道 前缀,后缀这两个概念


示例一:

在"abc"中,

前缀就是"ab",除去最后一个字符"c"的剩余字符串。

后缀就是"bc",除去第一个字符"a"后的面全部的字符串。


示例二:

如果子串是"abcabcdabc"

第一次拆分,前缀: abcabcdab 后缀: bcabcdabc,发现前后缀并不是一样的,那么我们需要按照前面拆分前后缀的规则继续拆分

p: abcabcda , s:cabcdabc 

p: abcabcd , s:abcdabc  

p: abcabc , s:bcdabc 

p: abcab , s:cdabc 

p: abca , s:dabc 

p: abc , s:abc   成功匹配出前后缀中,最长相同子串

这里有一点要注意,前缀必须要从头开始算,后缀要从最后一个数开始算,中间截一段相同字符串是不行的。


next数组推导

根据前后缀的拆分的规则,算出当前下标k,到0,最长的重复子串长度

示例:

 A B A B A C A

k = 0 及 next[k] = 0 ,因为首字符长度唯一,无法拆分出前后缀,所以= 0

k = 1 及 next[k] = 0 ,AB ,前后缀无相同所以 = 0

k= 2 及 next[k] = 1,ABA ,前缀AB ,后缀BA,继续-> (F: A ,S:A),所以 = 1

k = 3及 next[k] = 2 ,ABAB ,前缀ABA 后缀BAB ,继续-> (F: AB ,S:AB) 所以 = 2

k = 4 及 next[k] = 3 ,ABABA  (F: ABAB , S: BABA) -> (F: ABA , S: ABA) = 3

k = 5 及 next[k] = 0 ,ABABAC  (F: ABABA , S: BABAC) -> (F: ABAB , S: ABAC) 

-> (F: ABA , S: BAC)-> (F: AB , S: AC)-> (F: A , S: C) = 0

k = 6 及 next[k] = 1 ,ABABACA    (F: ABABAC , S: BABACA) -> (F: ABABA , S: ABACA) ->(F: ABAB , S: BACA)->(F: ABA , S: ACA)->(F: AB , S: CA)->(F: A , S: A) = 1

最终得出字符串对应的next[]数组:



翻译成java代码

private int[] getNextArray(String chs) {

    int i;//字符数组的下标指示器  

    int k;//前一个字符处的最大公共(相等)前、后缀子串的长度  

    int[] next = new int[chs.length()];

    for (i = 1, k = 0; i < chs.length(); i++) {

        while (k > 0 && chs.charAt(i) != chs.charAt(k)) { 
            k = next[k - 1]; // 这一部分是在回溯 “前缀” 的位置???
        }

        if (chs.charAt(i) == chs.charAt(k)) {
            k++;
        }
        next[i] = k;
    }

    return next;

}


next数组的实际应用:

当我们求出一段子串的next数组后,那么在匹配主串的时候,我们可以通过下标,使得我们知道将子串定位到主串 (下一趟探深比较的位置),而没有回退过主串指针。我们希望这个相等的前缀和后缀的长度越大越好(这也是为什么我们next数组中是求相等的前缀和后缀中最长的那个相等串的值),显然这样就可以匹配更多相同的元素。

根据next对应的(下标)推算下一次右移的位数:

如果子串当前匹配失败的位置 为起始点 k = 0 ,默认右移一位,

否则

下一次需要移动的位数 = (已经匹配成功的个数 - next[k-1])


KMP示例:

主串 S: deabcabcdabcabdh

子串 T:  abcabd

推算出子串的next数组值 如下:



1第一次对比:


根据next数组计算下一次探深对比的位置 :

失败的位置 为子串起始点 k = 0 ,默认右移一位, moveRight = 1


第二次对比:


失败的位置 为子串起始点 k = 0 ,默认右移一位, moveRight = 1

第三次对比:


moveRight = 5 - next[k - 1]  , 及 5 - 2 moveRight = 3


第四次对比:


moveRight = 3 - next[k - 1] , 及 3 - 0 moveRight = 3


第五次对比:


失败的位置 为子串起始点 k = 0 ,默认右移一位, moveRight = 1

第六次匹配:


完成匹配。


KMP增强版本 -  nextVal数组

在某些特殊的情况下,next是存在着缺陷的

例如 主串为:aaaabcde ,子串为:aaaaax

先按照next数组推导子串


和主串进行匹配

step1:


发现下标5位置匹配失败:根据公式计算子串下一次右移的范围

moveRight = 5 - next[k - 1] , 及 5 - 4 moveRight = 1

step2:


右移后,我们发现这时的主串下标发生回溯 (i=5 变成i=1),这就和朴素模式没有区别了,不划算。

那么我们换个方式,尝试推导nextVal数组


nextVal推导过程

这里的next数组和上面的next数组推导有一些区别,下标是从1开始,这里的next数组计算的前后缀是截取k=1 到 当前k-1的位置。根据next数组推导,还有一种不基于next的方法还没搞懂):

next[ k = 1]  :默认为0,没得比,单身狗不解释。

next[ k = 2]  :   当前位置的next值为1,  拿当前a 和next值1对应的a相比 相等 结果为0

next[ k = 3] :    当前位置的next值为2, 拿当前a 和next值2对应的a相比 相等 结果为0

next[ k = 4] : 当前位置的next值为3, 拿当前a 和next值3对应的a相比 相等 结果为0

next[ k = 5] : 当前位置的next值为4, 拿当前a 和next值4对应的a相比 相等 结果为0

next[ k = 6] : 当前位置的next值为5, 拿当前x 和next值5对应的a相比 不通 结果为5


小结:在计算出当前位置的next值的时候,如果当前为的a字符与它的值指向的位置字符b相等,那么当前位置a字符的nextVal值就指向b字符位置的next值,如果不等,该位置a字符的nextVal 的值就是该字符a当前的next值。

通过nextVal数组值匹配

第一位匹配失败时,右移默认一位(如果你想说,子串a的后面有相同字符,而且和主串对应的位置刚好匹配,也可以视为多余的比较,但是我想的是,第一位匹配失败后,是没有必要继续当前i++对比的,因为第一位都失败了,再去比较的意义就不大了)

其他位置失败,通过该位置的nextVal值,对应吧该值指向的字符位移到当前失败的位置即可。完美~!


总结:KMP算法,在较少字符串的情况下进行匹配,相比朴素算法是毫无优势可言的,因为next数组推算,也是需要增加计算成本的。