KMP算法小结

今天又把KMP算法看了一遍,特此小结。

扯淡的话:

KMP算法主要用来模式匹配。正如Implement strStr() 中形容的一样,“大海捞针”,当时看到题中变量如此命名,真的感觉实在是再贴切不过了。

在介绍KMP算法之前,先介绍一下BF算法,叫的这么暧昧(who is GF?),其实就是最low的暴力算法。这个男票略暴力。

事实上,JDK1.7中String的contains的源码用的就是BF算法。(1.7中调用了indexOf,我记得1.6中是直接写的contains接口来着)

截取了一部分,并稍稍改动一下下

public boolean contains(String p){
 char[] source ;
 char[] target = p.toCharArray();
 char fist = p.charAt(0);
 int end = p.length() ;
 int max = s.length() - 1;
 for (int i = 0; i <= max;  i++) {
            /* Look for first character. */
            if (source[i] != first) {
                while (++i <= max && source[i] != first);
            }
            /* Found first character, now look at the rest of v2 */
            if (i <= max) {
                int j = i + 1;
                int end = j + targetCount - 1;
                for (int k = 1; j < end && source[j] == target[k]; j++, k++);
                if (j == end) {
                    /* Found whole string. */
                    return true;
                }
            }
        }
        return false;
    }
View Code

 

 

可能网上很多实现不太一样,但是总体的算法思路都相同。看不懂也不要紧,因为看懂也不会有人问你这个。模式匹配算法是非常实用的算法之一,例如论文查重,字符串的模式匹配就是其中一种实现,虽然现在已经有更高大上的“树匹配” 和 “图匹配” 了,但是文本匹配也是一个非常重要的应用。BF算法是模式匹配中最low的算法时间之一,复杂度是O(m*n),这是非常恐怖的,试想,论文查重将你的论文与数据库中所有论文进行匹配.....准备延毕吧。因此BF算法貌似并没有太大的应用价值。

 

下面来看KMP算法,这是一个线性算法,复杂度是O(n + m)【一般情况下,n >> m,因此你也可以说是O(n)】,同AVL树一样,KMP算法的命名是由Knuth、Morris、Pratt三位作者的首字母组成的。不再多说。

 

在开讲之前,首先要来了解该算法中应用的几个概念:前缀 & 后缀

 

其实看到前缀、后缀字眼大家就能猜到是啥意思,但是对于KMP来讲,这里的前缀和后缀指的是绝对前缀绝对后缀。不能包含其自身。

还是看图说话吧。

这里我就不讲了,大家先仔细看图说话

这里发现了不一致,BF算法是酱紫的:

但是仔细观察会发现,其实不必要,可以这样:

再对比一下这两种情况:(上下为一组)

--------------------------------------↑为BF算法,↓为KMP算法-------------------------------------

 

你会发现:BF回溯了                                  而KMP,相对于字符串s而言,是一路向前的,因此,从这里粗略来看,时间复杂度是O(n)。

这里我们开始编码,即:遇到不同的字符,s的指针不变,p向右移动x个位置,这个x到底是多少,这里先不管,一会儿再讲。

        while(i < s.length()){
            if(p.charAt(j) == s.charAt(i)){
                i++;
                j++;
            }else{
                j = next[j];//这里这个next[j]表示上面说的x个位置
            }
            if(j == p.length()) return i - p.length();
        }

 

具体的代码框架应该是这样的,当两个字符相同时,继续比较s和p的下一个位置,如果不同,i不变,j向右移动next[j]步。当j == p.length 的时候,即表示在s中找到了一个子串为p。因此返回其实下标。

 

好了,回到问题的核心,这个x,具体是多少。还记得上面讲的前缀后缀吗?没错,这里要用到。

再来看这个图:

图中在上面虚线框处(当前s的下标是10)发生了失配,然后我们把p移动了x个位置(本例中,x == 4),到了下图的虚线框,现在有没有意识到为什么要移动x个位置,而不是x + 1或者x - 1个位置了吧?注意实线框。

还不明白?

KMP算法比BF算法高效的一点就是,s只需要一路向前,不能回溯,当发生失配时,为保证s的下标指针 i 不回溯,那么就要保证 i 之前的元素要么与p匹配,或者为空。因此这里移动了x个位置,使得虚线框之前的子串与p匹配。如果还不明白,那就去看july的博客吧,上面讲的炒鸡详细。

再看p

 

第一次匹配的是我用黑圈围起来的AB,移动之后匹配的是红圈围起来的AB,这两个AB对 “D之前的子串” 来讲是什么东东?bingo:前缀 & 后缀 啊!准确来讲,是 最长前缀 和 最长后缀 。

有点明白了吧?

移动的距离其实是与 最长匹配前后缀 的长度有关的。

例如上图中ABCDABD中的D,其最长相等前后缀是AB,长度为2,因此将p.charAt(2)移动到虚线框的位置,并从这里往后比较。

因此这里就可以看出上段代码中的 next[j] 实际上只与字符串p中的 j 相关,与s并无关系。

然而,next数组,该如何得到呢?

首先结合上面的图片来确认一下next数组表达的含义:

next[j]的值表示p[0 ~ j - 1]中最长匹配前后缀的长度。(所谓匹配前后缀是指:前缀.equals(后缀))

如上图中的

    ABCDAB D

next                  2

即表示:p[0 ~ j - 1]即ABCDAB:其前缀有:A 、AB、ABC、ABCD、ABCDA

                其后缀有: B、AB、DAB、CDAB、BCDAB

很容易看出:最长匹配前后缀是AB,长度为2,因此next[6] = 2;

特别的,约定next[0] = -1;

这样看来,next数组直接求解是很容易的,只需要对每一个子串的所有前后缀进行检查即可。然而,这个复杂度最小也是O(m^2),可以优化:

递推版本:由next[0 ~ i - 1],求出next[i]

算法思想:假设next[j] = k,即:P[0..k - 1] = P[j - k .. j - 1](前k个,后k个相等)

根据定义next[0] = - 1。(下面是摘自海子版的递推公式)

1) 如P[j] = P[k],因此P[0 ~ k] = P[j - k ~ j]:即有next[j + 1] = next[j] + 1 = k + 1;

2) 如P[j] != P[k],则可以把其看做模式匹配问题,即匹配失败的时候,k值如何移动,显然k = next[k]。

我智商比较拙计,怎么“显然”:而且P[j] = P[k] => next[j + 1] = next[j] + 1 = k + 1,是如何保证P[0..k - 1] = P[j - k .. j - 1] 的?

这里,我简单的将二者给解释一下:

关于第一点:

 如P[j] = P[k],因此P[0 ~ k] = P[j - k ~ j]:即有next[j + 1] = next[j] + 1 = k + 1;

在求next[j]时,已经能确保此时k的值是 k = next[j - 1];不明白?请点击下面展开

当你求next[j]的时候,是不是上一步刚刚求过next[j - 1] ? 
即next[j - 1] = k
这说明什么?
是不是说明p[0 ~ k - 1] = p[j - k, j - 1]?
View Code

 

这里有一种特殊情况,即当k = -1时,next[j] = 0(注意:k = -1 时,并不一定以为着仅仅在求next[1]时用到,下面就会讲到)

第二点:

如P[j] != P[k],则可以把其看做模式匹配问题,即匹配失败的时候,k值如何移动,显然k = next[k]。

“显然” 这两个字用的感觉有点推卸责任,但是没关系,自己剖析一下:

来看这个栗子:当前状态为:

            k      j

        ↓      ↓

    a  b  a  b  a  a  b  e

next    -1 0  0  1  2  3

此时已知的条件有,红色字体标记的前缀绿色阴影标记的后缀是最长匹配的,k= 3,j = 5,现在求b的next值

如果这里p[j] == p[k],那么很好办了,b直接next[j + 1] = next[j] + 1 = k + 1 = 4;这里可以很直观的看出,为什么next[j + 1] = next[j] + 1;

可惜这里p[j] != p[k],肿么办?按照上面的算法:k = next[k],即k = 1;然后再比较p[1] ? p[j],如果还不相等,继续k = next[k];

为什么?当你没有思路的时候,回到起点吧,定义是普适的。

首先俩看下next[k]表示什么意义?k之前的子串的最长匹配,即表示b之前的aba子串的最长匹配。

因此这里的next[k] = 1表示的是aba中的最长匹配前缀(后缀)长度为1,即next[k]表示红色字体标记的前缀 的最长前缀

而我们在不断的k = next[k],是为了寻找什么?是为了找b之前的最长前缀啊,找到了又怎样?还要跟后缀匹配啊!

进一步剖析一下b之前的子串 a b a b a a(将其命名为sub)

我们要找这个子串的最长匹配前后缀,首先我们已经确定了,后缀的最一个字母是a,先不看这个a,继续往前看, a  b  a。如果sub的后缀长度 > 1的话,倒数第2个字符一定是a,倒数第3个一定是b,倒数第4个一定是a,有没有倒数第5个? 不可能!!因为如果有倒数第5个字符的话,那么a b a b a 的最长匹配前后缀的长度至少是4,先把这个理解了再往下看吧。

即next[j + 1] <= next[j] + 1。

再来看sub : a b a b a a

继续不看最后的a,看前面的 a b a b a 我们要找这里面的最长匹配前后缀,只有在这个子串中匹配了,才可能在sub中匹配

又回到了原点:a b a是这里的一个前缀,a b a ,此时k 指向了前缀的下一个字符b,后缀的下一个字符是a,不匹配,再继续寻找,怎么找?k = next[k]

注意,next[k]表示啥?上面说过了,表示红色字体标记的 字符串的 最长前缀。同时。也表示:红色字体标记的字符串的最长匹配后缀。

再观察一下发现:红色字体跟绿色阴影是完全相等的,那么红色字体标记的前缀,岂不是就等于绿色阴影的后缀了吗?

因此每一次next[k]其实是在红色字体中 寻找 与 绿色阴影后缀 匹配的 前缀啊!比较拗口,理解了再往下看。

每查找一下,k都指向前缀后面的字符,然后将其与a作比较,如果相等,则next[j + 1] = k + 1;否则要继续往前找合适的前缀,最极端的情况就是:直到p[0],发现p[0] != p[j],next[j] = 0。

理解到这里,再看代码,将next[0]初始化为-1 ,并与p[j] = p[k]的处理合并到一起,实在是太巧妙了。。

分析到这里,就可以看代码了

 

public void setNext(String m,int[] next){
        char[] s = m.toCharArray();
        int length = m.length(), j = 0, k = -1;
        next[0] = -1;
        while(j < length - 1){//注意这里是length - 1,原因自己理解
            if(k == - 1 || s[j] == s[k]){//将k = -1与s[j] = s[k]放一起处理了,看起来好优美
                next[++j] = ++k;
            }else{
                k = next[k];
            }
        }
    }

 

 

 

这里就得到了next数组

再来看怎么用next数组。

刚才的图:

上面的setNext运行过后,匹配到D时候,发生失配,此时next[6] = 2,因此我们将i不变,j = next[j],让空格与C继续匹配。

 

    ABCDAB D

 

next                  2

 

代码如下:

 

    public int match(String s,String p){
        int[] next = new int[p.length()];
        int i = 0 , j = 0;
        setNext(p, next);
        while(i < s.length()){
            if(j == -1 || p.charAt(j) == s.charAt(i)){//这里j = -1 的处理其实跟上面setNext有异曲同工之妙的,你体会一下
                i++;
                j++;
            }else{//失配时,j = next[j]
                j = next[j];
            }
            if(j == p.length()) return i - p.length();
        }
        return -1;
    }

 

 

 

合并之后的代码:附test case

 1 package Algorithme;
 2 
 3 import java.util.ArrayList;
 4 import java.util.List;
 5 
 6 public class KMP {
 7     public void setNext(String m,int[] next){
 8         char[] s = m.toCharArray();
 9         int length = m.length(), j = 0, k = -1;
10         next[0] = -1;
11         while(j < length - 1){
12             if(k == - 1 || s[j] == s[k]){
13                 next[++j] = ++k;
14             }else{
15                 k = next[k];
16             }
17         }
18     }
19     public int match(String s,String p){
20         int[] next = new int[p.length()];
21         int i = 0 , j = 0;
22         setNext(p, next);
23         while(i < s.length()){
24             if(j == -1 || p.charAt(j) == s.charAt(i)){
25                 i++;
26                 j++;
27             }else{
28                 j = next[j];
29             }
30             if(j == p.length()) return i - p.length();
31         }
32         return -1;
33     }
34     
35     public static void main(String[] args) {
36         KMP test = new KMP();
37         List<String> list = new ArrayList<String>();
38         String s1 = "i love qiqi";
39         String s2 = "qiqi is my girl friend";
40         String s3 = "we have been together for five years";
41         list.add(s1);
42         list.add(s2);
43         list.add(s3);
44         String p = "qiqi";
45         for(String s : list)
46             System.out.println(test.match(s, p));
47     }
48 }
View Code

 

 

截止到这里,KMP算法基本算解释完了,如果没听懂,欢迎留言讨论。

 

 

参考资料:

July版:http://blog.csdn.net/v_july_v/article/details/7041827(优点,很详细。缺点:太啰嗦;吐槽:他的书肯定很厚,很贵 ̄へ ̄)

海子版:http://www.cnblogs.com/dolphin0520/archive/2011/08/24/2151846.html?ADUIN=313359714&ADSESSION=1410660113&ADTAG=CLIENT.QQ.5353_.0&ADPUBNO=26381(优点:很精练,缺点:有的地方一笔带过,太糙)

OI版:http://www.matrix67.com/blog/archives/115(以一种非主流的方式讲了KMP,很厉害,据说是作者当时是一名高中生)

posted on 2014-09-14 22:45  喵星人与汪星人  阅读(1021)  评论(0编辑  收藏  举报