資料結構-KMP模式演算法

Override0330發表於2019-04-12

最近很久沒有看Java的知識了,都在看看資料結構,一連看了一週,資料結構理解不難,但是真正的演算法理解還是比較困難的,所以開一個坑。接下來會繼續更新其他演算法,線性表貌似只涉及這一個演算法233333

參考書籍《大話資料結構》

1. 演算法原理

KMP模式演算法,用來解決查詢最小子串的問題,相對於暴力演算法來說,避免了重複比較的過程。

圖1-重複比較的示意圖(圖源《大話資料結構》)
從圖中可以看到,我們可以知道②③④⑤⑥是沒有必要的操作,因為我們已經遍歷過了這些資料,為什麼不能直接跳過他們呢? 我們分析我們的暴力求解方法:

//C語言實現,平均時間複雜度(n+m)/2
int getIndex(String s, String t, int pose){
    int i = pose;//母串下標值
    int j = 1;//表示當前子串的下標值,在這裡string下標為0的資料為字串的長度
    while (i <= s[0] && j<= t[0]) {//若i小於s長度且j小於t的長度的時候j迴圈,j>t[0]時表示有解
        if (s[i] == t[i]) {
            i++;
            j++;
        }else{
            i = i-j+2;//將母串的下標值回溯
            j = 1;
        }
    }
    if (j > t[0])//大於則表示有解
        return i - t[0];
    else
        return 0;
}
複製程式碼

從上圖可以看到,我們演算法要實現的就是避免i的回溯,以節省掉一些不必要的判斷,既然禁止了i的回溯,那麼我們就看看j是如何變化的。 根據圖1,我們可以發現,除去我們需要丟掉的,j從6變回了1,需要被查詢的子串abcdex的首字母a和bcdex中的任何一個字元不同,然後我們來看下面這個例子。

圖2-例子
我們發現,由於子串中的開頭的ab和後面的第四個字元開始的ab相等,所以j變成了3。故我們得出規律: j值的多少取決於當前字元之前的串的前字尾的相似度。 我們可以根據這個值給這個子串做一個特徵陣列(next陣列),陣列長度和子串相等,專門用來描述這一特性。

1.1 next陣列值推導

next陣列有如下推導原則:

  • 當j = 1時 這個下標下的特徵陣列值為0
  • 當1~j-1的串中有兩個字元相等的時候,值為相等的字首字元最後一個字元的下標+1
  • 其他情況時,值為1

例子:

  • 子串為“abcdex”
  1. 當j = 1時,next[1] = 0;
  2. 當j = 2時,b的前面就一個字元,屬於其他情況,next[2] = 1
  3. 當j = 3時,c的字首字元是b,和前面沒有相等的字元,所以屬於其他情況,next[3] = 1;
  4. 以此類推,最終的next陣列為:011111。
  • 子串為“abcabx”
  1. 當j = 1時,next[1] = 0;
  2. 當j = 2時,next[2] = 1;
  3. j = 3,4時同理,為1;
  4. 當j = 5時,b前面的字串為"abca",最後的a前面的首字元a相同,a的下標是1,所以next[5] = 1+1 = 2;
  5. 當j = 6時,x前面的字串為"abcab",字尾的ab和字首的ab是相同的,b的下標是2,所以next[6] = 2+1 = 3;
  6. 所以最終的next陣列為:011123
  • 子串為"ababaaaba"
  1. j = 1, next[1] = 0;
  2. j = 2, next[2] = 1;
  3. j = 3, next[3] = 1;
  4. j = 4, next[4] = 2;因為b前的字串"aba"中字首字元a和字尾字元a相等,1+1 = 2;
  5. j = 5, next[5] = 3;"***ab***ab" ab = ab 2+1 = 3;
  6. j = 6, next[6] = 4;"ababa"中,字首字串"aba"和字尾字串"aba"相等,即使中間的a是共享的;
  7. j = 7, next[7] = 2;"ababaa"中,字首字元a和字尾字元a相等,所以1+1 = 2;
  8. 以此類推,next:011234223;

接下來我們就要運用這個next陣列來實現我們的KMP模式演算法了

2. 演算法實現

首先是獲得next陣列的函式

void get_next(string t, int *next){
    int i, j;
    i = 1;
    j = 0;
    next[1] = 0;
    while (i<next[0]) {//next[0]是next陣列的長度
        if (j == 0 || t[i] == t[j] ) {
            i++;
            j++;
            next[i] = j;
        }else{
            j = next[j];
        }
    }
}
複製程式碼

然後是使用這個next特徵陣列進行查詢子串:

int Index_KMP(string s, string t, int pos){
    int i = pos;
    int j = 1;
    int next [255];
    get_next(t, next);//獲得next陣列
    while (i <= s[0] && j <= t[0]) {
        if (j==0 || s[i] == t[j]) {//對比暴力方法多加入了一個j的判斷,減少了遍歷次數
            i++;
            j++;
        }else{
            j = next[j];//對比暴力方法多加入了一個j回退到合適的位置,而不是直接退回到1,而i不變
        }
    }
    if (j > t[0])//有解
        return i=t[0];
    else
        return 0;//無解
}
複製程式碼

KMP模式其實還有能夠優化的地方,今晚到點熄燈了,下次再說

3. 總結

KMP模式演算法對比暴力破解法,其實改變了回溯的機制,使得遍歷的次數少了,從而做到了相對高效。但是這種演算法只有在有子串中有很多干擾項的時候才能對比出優越性。

相關文章