演算法(2)KMP演算法

hard_man發表於2019-04-08

1.0 問題描述

實現KMP演算法查詢字串。

2.0 問題分析

  1. “KMP演算法”是對字串查詢“簡單演算法”的優化。
  2. 字串查詢“簡單演算法”是源字串每個字元分別使用匹配串進行匹配,一旦失配,模式串下標歸0,源字串下標加1。
  3. 可以很容易計算字串查詢“簡單演算法”的時間複雜度為O(m*n),其中n表示源字串長度,m表示匹配串長度。
  4. KMP演算法的匹配方式同簡單演算法的匹配方式相同,只不過在失配的時候,模式串下標不歸零,反而會根據模式串自身的重複資訊,迴歸到一個大於0的下標。從而減少匹配次數。
  5. 那麼失配時候,模式串下標迴歸的位置需要提前計算好。計算的方法是,匹配串中,每個位置的字首字串“頭部”和“尾部”的重複字元數即為失配下標迴歸位置。
    • 假設匹配串是:ababc
    • 下標0的字元是“a”,它的字首是空字串,所以迴歸位置為0
    • 下標1的字元是“b”,它的字首是“a”,數量不足2個,迴歸位置也是0
    • 下標2的字元是“a”,他的字首是“ab”,沒有重複的頭部和尾部,迴歸位置是0
    • 下標3的字元是“b”,它的字首是“aba”,有重複的頭部和尾部,重複子串是“a”,長度為1,迴歸位置是1
    • 下標3的字元是“c”,它的字首是“abab”,有重複的頭部和尾部,重複子串是“ab”,長度為2,迴歸位置是2
  6. 根據上一條,我們可以計算出一個next陣列用於儲存失配時模式串下標應該回到哪裡的下標序列。
  7. 計算好next陣列後,就可以使用“簡單演算法”的邏輯進行匹配了,只不過不同的是,一旦失配,匹配串下標不是迴歸到0,而是根據next陣列決定。

3.0 程式碼實現

3.1使用swift實現

///簡單查詢
func simpleSearch(_ src: String, _ mode: String, _ start: Int) -> Int {
    var i = start;
    while i < src.count {
        var j = 0;
        while j < mode.count {
            if(i + j >= src.count || src[i + j] != mode[j]){
                break;
            }
            j += 1;
        }
        if j == mode.count{
            return i;
        }
        i += 1;
    }
    return -1;
}

///kmp字串查詢
func kmpSearch(_ src: String, _ mode: String, _ start: Int) -> Int {
    //計算next
    var next: [Int] = [0, 0];
    //自身匹配,i表示主下標,j表示匹配下標
    var i = 2, j = 0;
    while i < mode.count {
        if(mode[i - 1] == mode[j]){
            next[i] = j + 1;
            i += 1;
            j += 1;
        }else{
            if(j == 0){
                i += 1;
            }
            //已經匹配的部分有可能會有首尾相同的情況
            j = next[j];
        }
    }
    
    //演算法同自身匹配
    i = start;
    j = 0;
    while i < src.count {
        if(src[i] == mode[j]){
            i += 1;
            j += 1;
            
            if(j == mode.count){
                return i - mode.count;
            }
        }else{
            if(j == 0){
                i += 1;
            }
            j = next[j];
        }
    }
    
    return -1;
}
複製程式碼

3.2使用js實現

function simpleSearch(src, mode, start){
    for(let i = start; i < src.length; i++){
        let miss = false;
        for(let j = 0; j < mode.length; j++){
            if(i + j >= src.length){
                miss = true;
                break;
            }else if(src.charAt(i + j) != mode.charAt(j)){
                miss = true;
                break;
            }
        }
        if(!miss){
            return i;
        }
    }
    return -1;
}

function kmpSearch(src, mode, start){
    let next = [0, 0];
    let i = 2; 
    let j = 0;
    while (i < mode.length) {
        if(mode.charAt(i - 1) == mode.charAt(j)){
            j++;
            i++;
            next[i-1] = j;
        }else{
            if(j == 0){
                i++;
            }
            j = next[j];
        }
    }

    i = start;
    j = 0;
    let found = false;
    while (i <= src.length) {
        if(src.charAt(i) == mode.charAt(j)){
            i++;
            j++;
            if(j == mode.length){
                found = true;
                break;
            }
        }else{
            if(j == 0){
                i++;
            }
            j = next[j];
        }
    }

    if(found){
        return i - mode.length;
    }else{
        return -1;
    }
}
複製程式碼

4.0 複雜度分析

  1. 我們選取複雜度最大的一種模型來分析,即:模式串所有字元都相同,源串和模式串總是在最後一位失配。
  2. 令源串為 “aaaabaaaabaaaab”,匹配串為 “aaaaa”。
  3. 匹配串的next陣列為:[0,0,1,2,3]
  4. 首次匹配會在第5位失配,比較次數為5。
  5. 模式串回到3,進行一次比較即會失配,比較次數為1。
  6. 模式串回到1,進行一次比較即會失配,比較次數為1。
  7. 模式串回到0,同時源串下標加1。此時源串下標為6,匹配串下標為0。根據源串特點,此時會不斷重複4-7的過程。
  8. 根據上述分析,我們推到一般情況,長度為n的源串,長度為m的匹配串,會形成一個週期性匹配,週期次數為n/m。
  9. 很容易看到一個週期內的複雜度小於O(2m),所以整體複雜度小於 O(2m*n/m)=O(2n),即複雜度為O(n)。
  10. 計算next陣列的演算法和匹配演算法相同,因此複雜度為O(m)。
  11. 所以KMP演算法整體複雜度為O(m+n)。

相關文章