1. 程式人生 > 其它 >字串匹配演算法

字串匹配演算法

字串匹配一直是一個熱門的演算法。本文主要講兩種,普通方法(回溯)+高階方法(KMP)。

一、回溯法求子串位置(普通方法)

  演算法思想:

  給定兩個指標:i、j,分別指向主串的第pos個位置和子串的第一個位置。比較兩個指標所指的字元:如果相等,則繼續比較後續字元;若不等,則 i 指標跑到pos的下一個位置上,j 指標繼續從第1個位置開始比較……直到找到對應的子串。
  我們發現每次失配時,指標 i 都會重新回到之前位置的下一個位置上,這邊是“回溯”的體現。

  給定兩個指標:i、j,分別指向主串的第pos個位置和子串的第一個位置。比較兩個指標所指的字元:如果相等,則繼續比較後續字元;若不等,則 i 指標跑到pos的下一個位置上,j 指標繼續從第1個位置開始比較……直到找到對應的子串。


  我們發現每次失配時,指標 i 都會重新回到之前位置的下一個位置上(有點繞,多想想),這邊是“回溯”的體現。

int Index(SString *S, SString *T, int pos) {
    int i = pos;    int j = 1;
    while(S[i] != '\0' && T[j] != '\0') {    //只要有一個串走完了,那麼就退出迴圈
        if(S[i] == T[j]) {    //如果相等,則繼續比較後續字元
            i++;    j++;
        }else {
            i = i-j+2;    //i指標跑到隔壁位置上————回溯法的體現
            j = 1;    //重置j指標
        }
    }
    if(T[j] == '\0')    return i-j;    //返回子串在主串的第一個匹配字元的位置
    else return 0;    //不匹配,返回0
}

  演算法分析:

  若設n和m分別是主串和子串的長度,則一般情況下,該演算法的時間複雜度為O(n + m)。但是在最壞情況下,它的複雜度為O(n * m)【主串和子串的前面字元都匹配,只有最後一個字元不一樣】於是我們想找一種更好、更穩定的演算法

二、KMP演算法(非回溯法)

  對第一種方法的改進 

  在第一種方法下,我們每次失配都要重新把 i 指標放到其隔壁的位置上,而經過實踐,我們發現有時候這樣做不高效,如果能直接移動模式串就好了。
於是我們提出了改進方法:在新的演算法模式下,我們不需要對 i 指標進行回溯,我們只用向右移動模式串就行了。
  但是這樣我們需要解決一個問題:模式串要移動多遠,即主串中第 i 個字元應與模式的第幾個字元比較?

  演算法思想:
  假設主串為“s1, s2, s3 …… sn”,模式串為“p1, p2, p3 …… pm”

  1.若設失配後主串第 i 項應該與模式中第 k 個字元繼續比較,則模式中前 k-1 個字元的子串必須滿足以下公式——“p1, p2, p3 …… pk-1” == “si-k+1, si-k+2, si-k+3 …… si-1”  

  2.而已經得到“部分匹配”(j 指標前的所有字元均和主串對應字元匹配)的結果是——“pj-k+1, pj-k+2, pj-k+3 …… pj-1” == “si-k+1, si-k+2, si-k+3 …… si-1”  

  3.由以上兩式,可推得下面的式子——“p1, p2, p3 …… pk-1” == “pj-k+1, pj-k+2, pj-k+3 …… pj-1”

  4.由此——當匹配過程中主串第 i 個字元和模式第 j 個字元不匹配時,只需把模式移動至它的第 k 項和主串第 i 個字元對齊就行了!  

  next[ ]函式的構造

  既然模式的每一項都有不匹配的可能,那麼我們用一個數組next[ ],來寫出每一項不匹配的時候所對應的 k 值**(next[ j ] = k)**。
那麼如何求出每一項的k值呢?規則如下圖——

  

   由上圖可知,next[ ]函式值僅取決於模式串本身,而與其相匹配的主串無關
因此,我們求每一項next值的方法如下——

  1.規定:next[1] = 0;
    設:next[j+1] = k;

  2.比較第 k 和第 j 個字元是否匹配——
    2.1. 如果匹配(T[ k ] == T[ j ]),則next[ j + 1 ] = next[ j ] + 1;
    2.2. 如果不匹配(T[ k ] != T[ j ])再看k的next[ k ]
    2.2.1. 若next[ k ] == T[ j ],則next[ j + 1 ] = next[ k ] + 1;
    2.2.2. 若next[ k ] != T[ j ],那麼繼續找next[ k ]這一項的 k 值,再比較和第 j 個字元是否匹配,如果不匹配就一直找下去……

  3.最後,如果你幸運找到了匹配項,則next[ j + 1 ]就是你找到的那一項的項數 + 1
  如果不幸一直沒找到,那麼next[ j + 1 ]就等於1。
  我們來看一下獲取next( )函式的程式碼——

//獲取next[]陣列的函式
void get_next(SString T, int next[]) {
    int i = 1;    //用於遍歷模式串
    next[1] = 0;    //規定
    int j = 0;    //next數組裡面的數
    while(i < T(0)) {
        if(j == 0 || T[i] = T[j]) {    //一旦第k項和第j項匹配,那麼next陣列的j+1項的值就是你找到的那一項的項數 + 1
            i++;    j++;
            next[i] = j;
        }else {
            j = next[j];    //如果不匹配,就找第j項對應的k值
        }
    }
}

  現在,我們就有了模式串每一項所對應的k值了!
  例如,對於模式串“a b a a b c a c”,我們可以得到如下的next函式值:

 

  匹配步驟:

  在求得模式的next[ ]函式之後,匹配可如下進行:
  假設以製造 i 和 j 分別指示主串和模式中正待比較的字元,令 i 的初值為 pos, j 的初值為1(模式的第一個字元)——

  • 若在匹配過程中,si = pj,則 i 和 j 分別增加1;
  • 否則(si = pj),i 不變,j 退到 next[ j ] 的位置上再比較:若相等,則 i 和 j 分別增加1;不等則繼續比較下一個 next 值的位置……以此類推
  • 這樣一直進行下去,直到出現兩種情況——
    1. j 退到某個 next 值時字元比較相等,則兩指標各自增1,繼續比較下去
    2. j 退到其值為0(即模式串的第一個字元就失配),則此時需要把整個模式串向右滑動一位,即從主串的 s+1 位置起和模式重新進行匹配

  最後,就是KMP的程式碼實現,實際上它和回溯法的程式碼差不多。

int Index(SString S, SString T, int pos) {
    int i = pos;    int j = 1;
    while(i <= S[0] && j <= T[0]) {
        if(next[j] == 0 || S[i] == T[j]) {    //匹配,則比較下一個字元;要麼就是模式串的第一個字元就失配,直接從主串的 s+1 位置起和模式重新進行匹配
            i++;    j++;
        }else {
            j = next[j];    //不匹配,移動模式串到下一個k處,再比較是否匹配
        }
    }
    if(j >= T(0))    return i-T(0);    //返回子串在主串的第一個匹配字元的位置
    else return 0;    //不匹配,返回0
}