算法筆記-KMP算法

整理了一下?lián)f由于過于晦澀難懂而導致某系統(tǒng)程序猿直接在實現(xiàn)字符串匹配的時候直接用暴力算法代替的KMP算法沼侣,初看之時確實覺得難以理解恨憎,不過經過塞得威客大大一節(jié)課的講解之后谍失,我好像開始明白了拌滋。

大神鎮(zhèn)帖

其實在真實的應用中當字母表很大重復字符不多模式串很短(模式串一直都很短吧)的時候哥谷,KMP算法并不一定比暴力算法快棺榔,但是KMP算法不回退文本指針的特性使得它可以用來處理字符流中的匹配問題瓶堕。而且如果像0101010111000這樣的字母表就是0,1的字符串的話KMP的算法的效率優(yōu)勢就會體現(xiàn)出來症歇。

這門課中的KMP算法是用有限狀態(tài)自動機(DFA)來實現(xiàn)的郎笆,代碼很短,但是非常的贊(高德納大神的智慧熠熠生輝)忘晤,首先來看一下暴力字符串匹配法:

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
    }
}

蠻力匹配法非常的簡單宛蚓,稍微有編程基礎的人就能夠看懂。
然后是用有限狀態(tài)自動機實現(xiàn)的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凄吏,現(xiàn)在來看看DFA是怎么運作的:
在這個實現(xiàn)中,默認字母表是ANSSI字符闰蛔,所以DFA中有256個小數組痕钢,ANSSI表中就256個字符,這也暴露了這種實現(xiàn)的一個缺點:如果是中文字母表或者是其他字特別多的表序六,那么需要的空間就太多了任连!
具體講解這個算法非常麻煩,而且塞得威客大大講的非常的好例诀,英語好并且愛聽課的同學可以去看看他的公開課:
Coursera Algorithm Part II
里相關的視頻随抠。
比較喜歡看文字資料的同學可以看一下我啰啰嗦嗦的講解裁着。

KMP算法是怎么做的

我們知道,在蠻力算法中拱她,在匹配的過程中出現(xiàn)了失配的話二驰,就將模式串的指針重置為0,然后將模式串向前移動一位:

蠻力算法的匹配過程

仔細觀察這張圖就能發(fā)現(xiàn)椭懊,這樣非常低效诸蚕,我們能夠直觀地看到模式串除了第一個是B之外其他的位置上的字母都是A,在位置六失配氧猬,所以前面一段必然全是A背犯,我們直接把模式串的起始端移動到六位置就可以了,前面的一步一步移動的操作是可以跳過的盅抚,基于這一現(xiàn)象漠魏,深入思考,我們可以發(fā)現(xiàn)妄均,根據模式串的情況柱锹,我們可以推斷出來當在某個位置出現(xiàn)失配的時候我們應該怎么去移動模式串,為什么呢?因為當在六位置失配的時候丰包,我們已經知道了第六位之前的五位是匹配的禁熏,當我們將模式串向前移動一位再進行匹配的時候,相當于讓模式串跟去掉第一個字符并左移一位的自己進行匹配(嚴格來說不是自己邑彪,是自己的一個去掉第一個字符并左移一位的自己的前綴)瞧毙,失配后再重復這個過程。既然是跟自己的一部分匹配寄症,那我們可以在模式串自身進行這個過程并記錄下來各種情況出現(xiàn)的時候該怎么移動宙彪,那么,我們就需要構造一個有限狀態(tài)自動機了有巧。

有限狀態(tài)自動機

有限狀態(tài)自動機是一個很簡單的概念释漆,先簡單看一下它長什么樣子:

有限狀態(tài)自動機的兩種表示
先不論代碼是怎么寫的,說一說背后的思想篮迎。

在KMP的字符串匹配的過程中男图,其實就是一個狀態(tài)機的狀態(tài)轉換問題,我們以圖上的狀態(tài)機為例柑潦。
模式串有多長享言,就有多少個狀態(tài),首先我們是在0狀態(tài)渗鬼,然后開始匹配第一個字符览露,匹配的話,狀態(tài)機進入第二個狀態(tài)譬胎,開始匹配第二個字符差牛,不匹配的話命锄,根據自動機里存儲的轉換方式來轉換狀態(tài)。比如在0狀態(tài)時偏化,被匹配的長字符串的當前字母是A脐恩,那我們就根據dfa[A][0]得到我們應該轉換到1狀態(tài),然后指向被匹配的字符串的指針進1侦讨。如果被匹配的長字符串的當前字母是B驶冒,那我們就根據dfa[B][0]得到我們應該呆在狀態(tài)0,然后指向被匹配的字符串的指針進1韵卤,然后一步一步地重復這個過程骗污。如果我們前面一直匹配成功的話,我們會成功的從狀態(tài)0一直轉換到狀態(tài)6沈条,當我們的狀態(tài)成功達到6的時候需忿,就說明找到匹配了(注意這個過程中指向被匹配字符串的指針并沒有發(fā)生過回退,我們是通過狀態(tài)的轉換來決定接下來應該從模式字符串的哪一個字符開始與被匹配串的下一個字符進行匹配)蜡歹,匹配結束屋厘。
這個過程非常的簡單直觀,問題在于月而,我們怎樣才能構造出這樣的一個狀態(tài)機汗洒。
我們在這個應用中使用了一個二維數組來代表這個狀態(tài)機,數組的第一個索引是我們在被匹配字符串中所遇到的字符父款,第二個索引是當前自動機所在的狀態(tài)仲翎,在ANSSI字母表中,字符的總數共256個铛漓,所以共有256個子數組,在我們看到的這個案例中鲫构,模式串的長度是6浓恶,所以共有六個狀態(tài),所以每個子數組的長度是六结笨,代表從0到5六個狀態(tài)包晰。

該怎么構造出這個自動機呢?

過程也非常直觀炕吸,首先我們一眼就能看出來伐憾,當處于零狀態(tài)的時候,如果我們遇到字母A赫模,說明匹配成功树肃,狀態(tài)機的狀態(tài)應該轉向狀態(tài)1,在狀態(tài)1的時候瀑罗,如果遇到了字母B胸嘴,說明匹配再次成功了雏掠,狀態(tài)機的狀態(tài)應該轉換為狀態(tài)2,按照這個思路劣像,我們可以得到在完全匹配情況下的狀態(tài)轉換情況乡话,于是可以在二維數組中寫下這些值。
接下來就是失配狀態(tài)了耳奕,當在狀態(tài)0的時候绑青,如果發(fā)生失配,模式字符串就應該繼續(xù)停留在狀態(tài)0屋群,然后將第0個字符跟被匹配串的下一個字符進行比較闸婴,所以我們可以將dfa[][0]的未匹配位置都初始化為0。
在狀態(tài)1以及以后的狀態(tài)里谓晌,情況就會稍微復雜一些掠拳,但我們可以知道這樣一件事情:如果在第五個字符失配,那我們起碼知道下一次要輸入自動狀態(tài)機的四個字符是啥纸肉,有了這個溺欧,就可以不用回退文本指針了。
我們現(xiàn)在要做的事情是這樣的柏肪,首先用一個指針x指向0狀態(tài)姐刁,然后我們可以直接把0狀態(tài)里的數值直接拷貝到1狀態(tài),因為當在一狀態(tài)的時候發(fā)生不匹配的時候烦味,我們會用已匹配串的去掉首字母前綴進行匹配聂使,結果是空的,因為此時就一個字母匹配谬俄,然后我們再用不匹配的那個字母跟模式字符串的第一個字母去匹配柏靶,得到模式應該處于的狀態(tài),所以可以直接將第一個字母的對應的狀態(tài)機的這一列拷貝過來溃论,直接拿到結果屎蜓。然后這個過程是迭代的,也就是說钥勋,當我們在模式串的第三個字符失配的時候楞抡,我們實際上是在拿模式串的第二個字符(因為模式串這時肯定要向前進一)去跟模式串的0狀態(tài)匹配煤裙,然后得到我們用來進行匹配的下一個狀態(tài)喷橙,我們已經通過x = dfa[pat.charAt(j)][x]得到了輸入模式串的第二個字符后自動機的狀態(tài)锣笨,也就是說x現(xiàn)在指向的那一列狀態(tài)就是當前待匹配字符輸入之后應該轉換到的狀態(tài)了(也就是說,在當前位置失配了菲驴,當前位置的文本串應該去跟x位置的模式串字符去做匹配)荐吵,所以我們可以把x指向的那一列直接抄過來。
匹配進狀態(tài)進一,不匹配把去掉首字母已匹配串的的前綴輸入狀態(tài)機(這個操作在構造狀態(tài)機的過程中迭代進行捍靠,沒有重復操作)沐旨,這樣一個狀態(tài)機就構造出來了。
有了這樣一個狀態(tài)機榨婆,在進行模式匹配的時候磁携,我們就可以用j = dfa[txt.charAt(i)][j]; 變換自動機的狀態(tài),當j等于最后一個狀態(tài)的時候良风,就說明匹配成功了谊迄。

KMP算法就這么煉成了

理解了有限狀態(tài)自動機,KMP算法也就可以很容易地寫出來了烟央,為了練習一下统诺,筆者做了一下leetcode里的編程練習:
字符串匹配
有興趣的同學可以去練習一下加深理解。

后記:反復review加跟各種人解釋疑俭,我特么的把這段代碼背下來了

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末粮呢,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子钞艇,更是在濱河造成了極大的恐慌啄寡,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哩照,死亡現(xiàn)場離奇詭異挺物,居然都是意外死亡,警方通過查閱死者的電腦和手機飘弧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門识藤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人次伶,你說我怎么就攤上這事痴昧。” “怎么了冠王?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵剪个,是天一觀的道長。 經常有香客問我版确,道長,這世上最難降的妖魔是什么乎折? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任绒疗,我火速辦了婚禮,結果婚禮上骂澄,老公的妹妹穿的比我還像新娘吓蘑。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布磨镶。 她就那樣靜靜地躺著溃蔫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪琳猫。 梳的紋絲不亂的頭發(fā)上伟叛,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天,我揣著相機與錄音脐嫂,去河邊找鬼统刮。 笑死,一個胖子當著我的面吹牛账千,可吹牛的內容都是我干的侥蒙。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼匀奏,長吁一口氣:“原來是場噩夢啊……” “哼鞭衩!你這毒婦竟也來了?” 一聲冷哼從身側響起娃善,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤论衍,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后会放,有當地人在樹林里發(fā)現(xiàn)了一具尸體饲齐,經...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年咧最,在試婚紗的時候發(fā)現(xiàn)自己被綠了捂人。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡矢沿,死狀恐怖滥搭,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情捣鲸,我是刑警寧澤瑟匆,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站栽惶,受9級特大地震影響愁溜,放射性物質發(fā)生泄漏。R本人自食惡果不足惜外厂,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一冕象、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧汁蝶,春花似錦渐扮、人聲如沸论悴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽膀估。三九已至,卻和暖如春耻讽,著一層夾襖步出監(jiān)牢的瞬間察纯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工齐饮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留捐寥,地道東北人。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓祖驱,卻偏偏與公主長得像握恳,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子捺僻,可洞房花燭夜當晚...
    茶點故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內容

  • 第5章 引用類型(返回首頁) 本章內容 使用對象 創(chuàng)建并操作數組 理解基本的JavaScript類型 使用基本類型...
    大學一百閱讀 3,216評論 0 4
  • 初衷:看了很多視頻乡洼、文章,最后卻通通忘記了匕坯,別人的知識依舊是別人的束昵,自己卻什么都沒獲得。此系列文章旨在加深自己的印...
    DCbryant閱讀 3,993評論 0 20
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理葛峻,服務發(fā)現(xiàn)锹雏,斷路器,智...
    卡卡羅2017閱讀 134,629評論 18 139
  • 冰J冰閱讀 629評論 0 7
  • 164days 臭叉术奖。 不吃人奶奶吧…餓了吧礁遵! 一肚子氣,好想拉出來~ 信懿好嗎采记?如果我和aba的關系好點就好了佣耐,...
    sueva閱讀 251評論 0 0