【演算法】KMP
@目錄
一. 暴力匹配
字串匹配的最直接的方法就是暴力匹配,而KMP演算法也是基於暴力演算法進行改進。暴力匹配的思想如下:
- 對於文字串T和模式串P,從模式串P的第 0 號位置、文字串第 \(i_0\) 號位置開始逐一比對;
- 比對到中間某個時刻,若\(T[i ] == P[j]\),則比對繼續進行,\(i++, j++\)
- 如果比對失敗,則從模式串第0位和文字串的第\(i_0 + 1\)位繼續進行
但是\(T[i_0 , i)\)和\(P[0, j)\)比對成功意味著\(T[i_0 , i)\)和\(P[0, j)\)
例1
在圖中的比對過程中,主串中的 x 和模式串中的 y 失配,根據模式串中 y 以前的內容可以獲知主串對應部分的內容。如果在下一步的比對過程中直接將主串中的 x 和模式串中的 e 進行比對,可以省去 6 次比對
二.KMP的基本思想
在對暴力破解的演算法的分析中發現,對比在某個位置失敗意味著在這之前的比對完全成功,主串中失配字元前的一段內容已經完全獲知。利用這一點,對暴力演算法可以進行兩個方面的優化:
- 避免主串的回溯。暴力匹配當比對失敗後,文字串的第\(i_0 + 1\)
- 模式串快速移動。基於和上面相同的原因,模式串的新的比對位置不需要從 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;
}