KMP子字符串查找算法分析與實現

      原創博客,轉載請註明http://my.oschina.net/BreathL/blog/137916  java

      子字符串查找,是程序設計的一個基本且廣泛的問題。一般狀況下子字符串查找不須要特別的設計,一是因爲執行的次數很少,二是查找字符串通常也較短,因此不會造成性能的瓶頸;但若是你的程序裏有大量的查找或長字符串的子串查找,也許就須要考慮特別的設計已保證程序的效率。 c++

暴力查找算法:

      大部分語言默認的子字符串查找都是使用的暴力查找算法,思路很簡單,就是逐個取出查找的字符串的字節,而後依次和被包含的子字符串的字節比較,從以第一個開始,若相等,則各自取下一個繼續比較;若不相等,則查找的字符串回退回去到起始比較處的下一個字節,而子字符串從頭開始取,而後以此循環的比較。 算法

能夠優化:

      它的算法複雜度是 N*M 這個量級的,但有個問題是:當匹配失敗後,查找的字符串回退回去到起始比較處的下一個字節,而子字符串從頭開始取時,緊接着的幾步比較多是多餘的計算。由於前X已經匹配上了,說明臨近X個字節已知了,那就能夠根據已知的狀況去掉一些重複的比較,這就KMP子字符串查找算法的優化原理。這麼說可能有些模糊,舉個例子: 數組

帶查找字符串: F Y Y Y Y U H N Z Y Y Y Y        目標字符串: F Y Y Y Y M 性能

      第1次比較,到M是發現不匹配了,這時咱們已經知道了帶查找字符串前5個字符是什麼,因此經過必定的規律咱們能夠直接產生第7次比較,去掉第2~第6次的多餘的計算,由此提升效率。 優化

Knuth-Morris-Pratt(KMP)子字符串查找算法:

     前面已經提到KMP優化的查找的關鍵點是在,當匹配失敗後,根據失敗前所匹配的一些信息,回退到最適當的位置進行後續的匹配。KMP算法給出的方案是:

      1. 待查找字符串,不回退,繼續取Next進行匹配。[暴力算法:回退起始匹配的Next] this

      2. 包含的字符串,根據當前這個不匹配字節,來肯定,回退帶那個字節。[暴力算法:回退到開始字節] spa

    爲了更好的實現上述優化邏輯,KMP子字符串算法的整個設計思路以下: .net

      1. 經過包含字符串,構造出一個二維的字典,兩個key對應一個值,這兩個key:1是對應全部的字節c,2是對應包含字符串字節位置(index);而值是下一個要去比較的包含字符串中字節的位置。 設計

      2. 查找字符串,逐個取出字節,放到上述構建的那個二維字典中;取出的字節對應到第一個key,第二個key是包含字符串的index,初始時是0。這樣就獲得下一個包含字符串中須要比較的字節的位置;用這個位置更新第二個Key,同時取出查找字符串的Next 更新第一個Key;不斷循環。

      我記得小時候有玩過一種遊戲(記不清名字了),跟這個很像:首選,遊戲者選擇一堆本身以爲對遊戲有利的條件;來到第一個格子,格子裏寫着:[若是知足XXX條件請直接到第六格、若是知足YYY條件請原地不動,若是知足MMM條件繼續前進一格],知足MMM條件的我顛兒顛兒的來到第二格,第二個格子一樣寫着不一樣的條件,到哪到哪。跟KMP的思路真是一模一樣;二維字典中的這第一個Key就是那些條件,第二個Key就是你當前所處的格子,值就是要去的下一個格子。我猜算法就是設計者玩這遊戲時想出的,絕對是,嗯。

核心-二維字典的構建:

      由上述的KMP子字符串查找算法,咱們能夠清晰的看出只要這個二維字典一旦構造好,其餘的邏輯只是經過這個字典去查找,邏輯就比較簡單了。構造二維字典的目的也已經明確,存儲的是下一次比較的位置。說白了就是一次跳轉,這裏細說下這個跳轉,可分爲3種類別,分別實現這3種類別的邏輯,其實也就實現了二維字典的構造邏輯,這樣看代碼也會很清晰。

      1 . 首選,匹配對了,即第一個Key的字符與第二個Key(即包含字符串的index)所對應的字符同樣,這時的跳轉就是下一個(index=index+1)。

      2 . 匹配失敗,且無前綴,跳轉至起始位置,即index=0。

      3 . 匹配失敗,有前綴,跳轉至前綴結束位置的下一個。

      這裏所指的前綴,是前綴匹配,定義很簡單,這裏不細講了,舉個例子吧。

       好比上圖中,匹配到第五個位置了,即紅色箭頭的比對,發現不匹配了,因而跳轉;因爲存在前綴,藍色方框對,因而 包含字符串 跳轉至 藍框結束後面的那個B 開始下一個的比較(查找字符串始終是往下一個跳轉),右圖紅框是匹配失敗後的起始的比較。

       二維字典,通常用一個二維數組表示,dfa[][],它構造出來大體能夠想象成這麼個樣子,行是第一個Key,列是第二個Key:

實現:

      完整的程序以下(代碼來自《算法》第四版)

package com.kmp;

public class KMP {

    private String pat;
    private int[][] dfa; //二維字典
    
    public KMP(String pat){
        this.pat = pat;
        int M = pat.length();
        int R = 256;
        //二維字典的構造
        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];
            dfa[pat.charAt(j)][j] = j + 1; //第j個字節,恰好在第j個位置上,說明匹配成功
            X = dfa[pat.charAt(j)][X]; //更新參照列的位置
        }
    }
    
    public int find(String content){
        // 查找邏輯,相似遊戲
        int i, j;
        int N = content.length();
        int M = pat.length();
        for (i = 0, j = 0; i<N && j<M; i++){
            j = dfa[pat.charAt(i)][j];
        }
        if (j == M) return i - M; //找到
        else return M;
    }

}

      在二維字典的構造邏輯裏面,有個參照列的思路,它的原理是,一列即第二個Key對應全部字節的跳轉值是一種狀態,這種狀態是能夠保存並傳遞給下面某一列;具體的邏輯能夠簡單理解爲,重複了(即在中間出現了前綴),那麼程序裏的X,就會產生變化,從而是參照列改變。最開始都是參照第0列的;最後若是再也不繼續重複(即前綴中斷),X又會變回成0,從新用第0列做爲參照。

相關文章
相關標籤/搜索