算法笔记-KMP算法

整理了一下据说由于过于晦涩难懂而导致某系统程序猿直接在实现字符串匹配的时候直接用暴力算法代替的KMP算法,初看之时确实觉得难以理解,不过经过塞得威客大大一节课的讲解之后,我好像开始明白了。

大神镇帖

其实在真实的应用中当字母表很大重复字符不多模式串很短(模式串一直都很短吧)的时候,KMP算法并不一定比暴力算法快,但是KMP算法不回退文本指针的特性使得它可以用来处理字符流中的匹配问题。而且如果像0101010111000这样的字母表就是0,1的字符串的话KMP的算法的效率优势就会体现出来。

这门课中的KMP算法是用有限状态自动机(DFA)来实现的,代码很短,但是非常的赞(高德纳大神的智慧熠熠生辉),首先来看一下暴力字符串匹配法:

public class BruteForce {

    public static void main(String[] args){
        String txt = "ABACCABCFT";
        String pat = "FT";
        int a = search(pat, txt);
        if (a == txt.length()){
            System.out.println("Not Found!");
        }else{
            System.out.println("Found: " + a);
        }
    }

    private static int search(String pat, String txt){
        int M = pat.length();
        int N = txt.length();

        for (int i = 0; i <= N - M; i++){
            int j;
            for (j = 0; j < M; j++){
                if (txt.charAt(i+j) != pat.charAt(j)){
                    break;
                }
            }
            if (j == M) return i;//Found
        }
        return N;//Not Found
    }
}

蛮力匹配法非常的简单,稍微有编程基础的人就能够看懂。
然后是用有限状态自动机实现的KMP算法:

public class KMP {
    private final int R;       // the radix
    private int[][] dfa;       // the KMP automoton

    private String pat;        // the pattern string

    public static void main(String[] args) {
        String pat = args[0];
        String txt = args[1];

        KMP kmp1 = new KMP(pat);
        int offset1 = kmp1.search(txt);

        // print results
        System.out.println("text:    " + txt);

        System.out.print("pattern: ");
        for (int i = 0; i < offset1; i++)
            System.out.print(" ");
        System.out.println(pat);

    }

    public int search(String txt) {

        // simulate operation of DFA on text
        int m = pat.length();
        int n = txt.length();
        int i, j;
        for (i = 0, j = 0; i < n && j < m; i++) {
            j = dfa[txt.charAt(i)][j];
        }
        if (j == m) return i - m;    // found
        return n;                    // not found
    }

    public KMP(String pat) {
        this.R = 256;
        this.pat = pat;

        // build DFA from pattern
        int m = pat.length();
        dfa = new int[R][m];
        dfa[pat.charAt(0)][0] = 1;
        for (int x = 0, j = 1; j < m; j++) {
            for (int c = 0; c < R; c++)
                dfa[c][j] = dfa[c][x];     // Copy mismatch cases.
            dfa[pat.charAt(j)][j] = j+1;   // Set match case.
            x = dfa[pat.charAt(j)][x];     // Update restart state.
        }
    }
}

可以看到search方法是几乎一样的,只不过是加了一个DFA,现在来看看DFA是怎么运作的:
在这个实现中,默认字母表是ANSSI字符,所以DFA中有256个小数组,ANSSI表中就256个字符,这也暴露了这种实现的一个缺点:如果是中文字母表或者是其他字特别多的表,那么需要的空间就太多了!
具体讲解这个算法非常麻烦,而且塞得威客大大讲的非常的好,英语好并且爱听课的同学可以去看看他的公开课:
Coursera Algorithm Part II
里相关的视频。
比较喜欢看文字资料的同学可以看一下我啰啰嗦嗦的讲解。

KMP算法是怎么做的

我们知道,在蛮力算法中,在匹配的过程中出现了失配的话,就将模式串的指针重置为0,然后将模式串向前移动一位:

蛮力算法的匹配过程

仔细观察这张图就能发现,这样非常低效,我们能够直观地看到模式串除了第一个是B之外其他的位置上的字母都是A,在位置六失配,所以前面一段必然全是A,我们直接把模式串的起始端移动到六位置就可以了,前面的一步一步移动的操作是可以跳过的,基于这一现象,深入思考,我们可以发现,根据模式串的情况,我们可以推断出来当在某个位置出现失配的时候我们应该怎么去移动模式串,为什么呢?因为当在六位置失配的时候,我们已经知道了第六位之前的五位是匹配的,当我们将模式串向前移动一位再进行匹配的时候,相当于让模式串跟去掉第一个字符并左移一位的自己进行匹配(严格来说不是自己,是自己的一个去掉第一个字符并左移一位的自己的前缀),失配后再重复这个过程。既然是跟自己的一部分匹配,那我们可以在模式串自身进行这个过程并记录下来各种情况出现的时候该怎么移动,那么,我们就需要构造一个有限状态自动机了。

有限状态自动机

有限状态自动机是一个很简单的概念,先简单看一下它长什么样子:

有限状态自动机的两种表示
先不论代码是怎么写的,说一说背后的思想。

在KMP的字符串匹配的过程中,其实就是一个状态机的状态转换问题,我们以图上的状态机为例。
模式串有多长,就有多少个状态,首先我们是在0状态,然后开始匹配第一个字符,匹配的话,状态机进入第二个状态,开始匹配第二个字符,不匹配的话,根据自动机里存储的转换方式来转换状态。比如在0状态时,被匹配的长字符串的当前字母是A,那我们就根据dfa[A][0]得到我们应该转换到1状态,然后指向被匹配的字符串的指针进1。如果被匹配的长字符串的当前字母是B,那我们就根据dfa[B][0]得到我们应该呆在状态0,然后指向被匹配的字符串的指针进1,然后一步一步地重复这个过程。如果我们前面一直匹配成功的话,我们会成功的从状态0一直转换到状态6,当我们的状态成功达到6的时候,就说明找到匹配了(注意这个过程中指向被匹配字符串的指针并没有发生过回退,我们是通过状态的转换来决定接下来应该从模式字符串的哪一个字符开始与被匹配串的下一个字符进行匹配),匹配结束。
这个过程非常的简单直观,问题在于,我们怎样才能构造出这样的一个状态机。
我们在这个应用中使用了一个二维数组来代表这个状态机,数组的第一个索引是我们在被匹配字符串中所遇到的字符,第二个索引是当前自动机所在的状态,在ANSSI字母表中,字符的总数共256个,所以共有256个子数组,在我们看到的这个案例中,模式串的长度是6,所以共有六个状态,所以每个子数组的长度是六,代表从0到5六个状态。

该怎么构造出这个自动机呢?

过程也非常直观,首先我们一眼就能看出来,当处于零状态的时候,如果我们遇到字母A,说明匹配成功,状态机的状态应该转向状态1,在状态1的时候,如果遇到了字母B,说明匹配再次成功了,状态机的状态应该转换为状态2,按照这个思路,我们可以得到在完全匹配情况下的状态转换情况,于是可以在二维数组中写下这些值。
接下来就是失配状态了,当在状态0的时候,如果发生失配,模式字符串就应该继续停留在状态0,然后将第0个字符跟被匹配串的下一个字符进行比较,所以我们可以将dfa[][0]的未匹配位置都初始化为0。
在状态1以及以后的状态里,情况就会稍微复杂一些,但我们可以知道这样一件事情:如果在第五个字符失配,那我们起码知道下一次要输入自动状态机的四个字符是啥,有了这个,就可以不用回退文本指针了。
我们现在要做的事情是这样的,首先用一个指针x指向0状态,然后我们可以直接把0状态里的数值直接拷贝到1状态,因为当在一状态的时候发生不匹配的时候,我们会用已匹配串的去掉首字母前缀进行匹配,结果是空的,因为此时就一个字母匹配,然后我们再用不匹配的那个字母跟模式字符串的第一个字母去匹配,得到模式应该处于的状态,所以可以直接将第一个字母的对应的状态机的这一列拷贝过来,直接拿到结果。然后这个过程是迭代的,也就是说,当我们在模式串的第三个字符失配的时候,我们实际上是在拿模式串的第二个字符(因为模式串这时肯定要向前进一)去跟模式串的0状态匹配,然后得到我们用来进行匹配的下一个状态,我们已经通过x = dfa[pat.charAt(j)][x]得到了输入模式串的第二个字符后自动机的状态,也就是说x现在指向的那一列状态就是当前待匹配字符输入之后应该转换到的状态了(也就是说,在当前位置失配了,当前位置的文本串应该去跟x位置的模式串字符去做匹配),所以我们可以把x指向的那一列直接抄过来。
匹配进状态进一,不匹配把去掉首字母已匹配串的的前缀输入状态机(这个操作在构造状态机的过程中迭代进行,没有重复操作),这样一个状态机就构造出来了。
有了这样一个状态机,在进行模式匹配的时候,我们就可以用j = dfa[txt.charAt(i)][j]; 变换自动机的状态,当j等于最后一个状态的时候,就说明匹配成功了。

KMP算法就这么炼成了

理解了有限状态自动机,KMP算法也就可以很容易地写出来了,为了练习一下,笔者做了一下leetcode里的编程练习:
字符串匹配
有兴趣的同学可以去练习一下加深理解。

后记:反复review加跟各种人解释,我特么的把这段代码背下来了

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,978评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,954评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,623评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,324评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,390评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,741评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,892评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,655评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,104评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,569评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,254评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,834评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,725评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,950评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,260评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,446评论 2 348

推荐阅读更多精彩内容

  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,216评论 0 4
  • 初衷:看了很多视频、文章,最后却通通忘记了,别人的知识依旧是别人的,自己却什么都没获得。此系列文章旨在加深自己的印...
    DCbryant阅读 3,993评论 0 20
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,628评论 18 139
  • 冰J冰阅读 629评论 0 7
  • 164days 臭叉。 不吃人奶奶吧…饿了吧! 一肚子气,好想拉出来~ 信懿好吗?如果我和aba的关系好点就好了,...
    sueva阅读 251评论 0 0