1. 程式人生 > 實用技巧 >【演算法】KMP

【演算法】KMP

@目錄

一. 暴力匹配

字串匹配的最直接的方法就是暴力匹配,而KMP演算法也是基於暴力演算法進行改進。暴力匹配的思想如下:

  1. 對於文字串T和模式串P,從模式串P的第 0 號位置、文字串第 \(i_0\) 號位置開始逐一比對;
  2. 比對到中間某個時刻,若\(T[i ] == P[j]\),則比對繼續進行,\(i++, j++\)
  3. 如果比對失敗,則從模式串第0位和文字串的第\(i_0 + 1\)位繼續進行

但是\(T[i_0 , i)\)\(P[0, j)\)比對成功意味著\(T[i_0 , i)\)\(P[0, j)\)

完全相同的,掌握了\(P[0, j)\)那麼也就意味著掌握了\(T[i_0 , i)\),下一輪的比對方案完全可以提前預知。

例1

在圖中的比對過程中,主串中的 x 和模式串中的 y 失配,根據模式串中 y 以前的內容可以獲知主串對應部分的內容。如果在下一步的比對過程中直接將主串中的 x 和模式串中的 e 進行比對,可以省去 6 次比對

二.KMP的基本思想

在對暴力破解的演算法的分析中發現,對比在某個位置失敗意味著在這之前的比對完全成功,主串中失配字元前的一段內容已經完全獲知。利用這一點,對暴力演算法可以進行兩個方面的優化:

  1. 避免主串的回溯。暴力匹配當比對失敗後,文字串的第\(i_0 + 1\)
    位、\(i_0 + 2\)位、……、\(i-1\)位的對比結果完全可以推匯出來,沒有必要再進行比對嘗試;
  2. 模式串快速移動。基於和上面相同的原因,模式串的新的比對位置不需要從 0 開始。如例一中的模式串,字元y和字元e的前面都包含了"abc"這一部分,因此y前面的部分能和主串匹配成功,那麼e前面的也一定能匹配成功。

因此對於模式串中的每個位置 j ,都能提前找到一個替代位置。

例2

模式串"abababca",對字元c而言,2號位的 a 和4號位的 a 都是能夠在字元 c 發生失配時的一個可選擇的位置

在諸多可選的繼任位置中,位置下標越大,意味著已經成功匹配的長度越長,剩下需要比對的位置也就越少,因此 j 的繼任位置\(next[j]\)

定義為:

\[next[j] = \max(k | p_0 p_ 1...p_{k - 1} = p_{j - k}p_{j - k + 1}...p_{j - 1}) \]

通常定義\(next[0] = -1\)或者\(next[1] = 0\)(當字串的下標從1開始時),這種規定是假想在模式串的起始位置的前一個有一個通配哨兵。

三.next[]的求法

1. 暴力求解

根據\(next[j]\)的定義,從逐一列舉字元P[j]的真字首和真字尾,找出相等的真字首和真字尾的長度,取長度的最大值即為\(next[j]\)

2. 遞推求解

假設已經求得\(next[0, ... , j]\),遞推求解\(next[j + 1]\)

\(next[j]\)已知意味著\(P[0, 1, ..., next[j] - 1]\)\(P[j - next[j], ... , j - 1]\)是相等的,並且這個相等的部分是最大的,求取\(next[j + 1]\)時,只需要考察\(P[next[j]] == P[j]\)是否成立。如果成立,\(next[j + 1] = next[j] +1\) ,如果不成立,再考察\(P[next[next[j]]] == P[j]\)是否成立,依次類推,最終會收斂於\(next[0] + 1 = 0\)

插圖來自視訊

void buildNext(string str, int nt[]){
    int len = str.size();
    nt[0] = -1;
    int t = nt[0], j = 0;
    while(j < len - 1){
        if(t < 0 || str[j] == str[t]){
            nt[++j] = ++t;
        }else{
            t = nt[t];
        }
    }
}

四.KMP演算法

在求解了next陣列之後,kmp演算法變得非常簡單了。

int kmp(string str1, string str2){
    int nt[str2.size()];
    buildNext(str2, nt);//構建next表
    int i = 0, j = 0;
    while(i < str1.size() && j < str2.size()){//逐步比對
        if(j < 0 || str1[i] == str2[j]){//比對成功時,前進一位,j < 0表示和萬用字元比對成功
            i++; j++;
        }else{//對比失敗,找到新的位置比對
            j = nt[j];
        }
    }
    return i - j;
}