字串匹配演算法
字串匹配一直是一個熱門的演算法。本文主要講兩種,普通方法(回溯)+高階方法(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 值的位置……以此類推
- 這樣一直進行下去,直到出現兩種情況——
- j 退到某個 next 值時字元比較相等,則兩指標各自增1,繼續比較下去
- 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
}