字串匹配原理及實現(C++版)
字串匹配原理及實現(C++版)
1. 字串匹配概念
在查詢操作中,我們用到很重要的概念就是字串匹配,所謂字串匹配就是在文字串中搜索模式串是否存在及其存在的位置。下面介紹幾種字串匹配的方法。
2. BF
2.1 原理
BF(暴力法)是一種最簡單的字串匹配演算法,匹配過程如下:
文字串中的 I 和模式串中的 II 實現了匹配。
如果 I 和 II 下一個都是 A ,那麼匹配長度增長,I 變成 III ,II 變成 IV 。
如果 III 的下一個是 A ,IV 的下一個是 B ,那麼匹配失敗,模式串向後移動一個字元,重新開始字串匹配。
BF 的特點:
1.模式串與文字串的匹配是自左向右的進行。
2.一旦模式串與文字串失配,模式串只能向右移動一個字元。
2.2 程式碼實現
/*
* 暴力法:用於字串匹配
* string t:文字串
* string p:模式串
* 返回值:返回首次匹配(完全匹配)位置(失敗返回-1)
*/
int BruteForce( string t, string p){
int lenT = t.size();
int lenP = p.size();
int i, j;
for (i = 0; i <= lenT - lenP; ++i){
for (j = 0; j < lenP; ++j){
if (t[i + j] != p[j]){
break;
}
}
if (j == lenP){
return i;
}
}
return -1;
}
3. KMP
3.1 原理
可以看出,BF 中的匹配過程如下:
1.每次:多次成功,一次失敗。
2.總體:多次失敗,一次成功。
因為每次比對,都經歷了多次成功,而經歷了一次失敗,然後需要模式串移動一個字元。顯然,這種方法沒有吸取 先前匹配成功的經驗
我們考慮在第一個文字串和模式串對齊方式中,I 和 II 是匹配的,那麼,模式串能夠從第一個對齊位置移動到下一個對齊位置的條件是 III 和 IV 是匹配的。
由此我們可以總結:
1.移動對齊方式只由文字串與模式串失配位置決定。
2.而與文字串與模式串失配位置的文字串字元無關。
3.也就是說,移動對齊方式只與模式串有關。
那麼,我們完全可以根據模式串預先算出一張表,由此得到在不同的位置上失配可以移動模式串的字元距離。
在第一個對齊方式中,I 和 II 是匹配的,匹配長度是 7 個字元,那麼我們可以在表中記錄數字 7,即該表儲存的是當前字元前面的字串 頭 和 尾 匹配的長度。
推導表格的方法我們採用遞推的方法,假設已經有了第一個對齊位置的匹配,即 I 和 II 是匹配的,匹配長度是 7。
如果 I 和 II 的下一個字元都是 A,那麼 I 變成 III ,II 變成 IV,即匹配長度 + 1,在表中記錄數字 8。
如果 III 的下一個字元是 A ,IV 的下一個字元是 B,那麼問題就不再那麼簡單了。
首先,細分 III 字串,可以看到 V 和 VI 是匹配的,同理,VII 和 VIII 是匹配的。此時剛好 V 的下一個字元是 B,那麼就實現了匹配, V 變成 IX,VIII 變成 X。在表格中記錄數字 3。
如果 V 的下一個字元依舊不是 B,我們就可以將 V 繼續細分,方法與上類似。如果細分到最後還是找不到字元 B,那麼就只能將模式串移動一個字元,即只能在表中記錄數字 0,表示當前字元前面的字串 頭 和 尾 匹配的長度是 0。
建立 next 表的程式碼如下:
/*
* 建立next表
* string p:模式串
* int next[]:next表
*/
void CreatNext(string p, int next[]){
int lenP = p.size();
int i = 0, j = -1;
next[0] = -1;
while (i < lenP - 1){
if (j < 0 || p[i] == p[j]){
i++;
j++;
next[i] = j; //此句程式碼還可以改進
}
else{
j = next[j];
}
}
}
其實,next 表的建立方法還可以改進。
首先 I 和 II 是匹配的,III 和 IV 是匹配的,按第一種建立方式, II 和 IV 的下一個字元對應的表格都應該記錄數字 3。但是實際上,如果 IV 的下一個字元發生了失配,而 IV 和 III 的下一個字元都是 A 的話,即使將 III 移動到 IV 的位置上,結局依然是失配,而我們可以通過改進 next 表的建立方式來避免這種不必要的操作。
/*
* 建立next表改進版
* string p:模式串
* int next[]:next表
*/
void CreatNext_E(string p, int next[]){
int lenP = p.size();
int i = 0, j = -1;
next[0] = -1;
while (i < lenP - 1){
if (j < 0 || p[i] == p[j]){
i++;
j++;
next[i] = p[i] == p[j] ? next[j] : j; //此句程式碼進行了改進
}
else{
j = next[j];
}
}
}
KMP 的特點:
1.模式串與文字串的匹配是自左向右的進行。
2.一旦模式串與文字串失配,模式串依靠 next 表向右移動若干個字元。
3.2 程式碼實現
next 表的建立程式碼不再贅述。
/*
* KMP法:用於字串匹配
* string t:文字串
* string p:模式串
* 返回值:返回首次匹配(完全匹配)位置(失敗返回-1)
*/
int KnuthMorrisPratt(string t, string p){
int lenT = t.size();
int lenP = p.size();
int *next = new int[lenP];
//CreatNext(p, next);
CreatNext_E(p, next);
int i, j;
for (i = 0; i <= lenT - lenP; ){
for (j = 0; j < lenP; ++j){
if (t[i + j] != p[j]){
i += j - next[j];
break;
}
}
if (j == lenP){
return i;
}
}
return -1;
}
4. BM
4.1 壞字元
在 KMP 演算法中,總結起來就是:
1.每次:多次成功,一次失敗。
2.總體:多次失敗,一次成功。
可以看出來,除了成功匹配的那次對比,其餘的各次都是因為一次失配引起的。但是,在一般情況下,失敗的概率與成功的概率相比,簡直是微乎其微。所以,與其說是尋找匹配,不如說是加速失敗。這裡的壞字元說的就是加速失敗。
在 壞字元 策略中,有這樣的情況,這裡 I 和 II 已經成功匹配。而 I 前面的一個字元是 A,II 的前面一個字元是 B,發生了失配。那麼,接下來,我們在模式串中找到字元 A,然後將兩者相應對齊。那麼,會有以下幾種情況:
1.A(一個或多個)在 B 的前面:那麼這時我們為了 加速匹配 程序而又 避免遺漏,可以把(最右邊的 A)移動到文字串的 A 位置,與之對齊。
2.A(無或有)在 B 的後面:如果模式串中沒有字元 A,那麼直接將模式串向右移動一個字元。而如果 A 在 B 的後面,那麼就不能把 A 和文字串的 A 對齊,因為這樣會引起字串匹配的回溯,是沒有意義的。這時依舊是將模式串向右移動一個字元。
因為我們需要知道的是某個字元在模式串中的有無以及最右邊的位置,所以我們可以構建一個 bc 表,用來記錄這些資訊,方便我們查詢。顯然,bc 表要能夠涵蓋整個文字串與模式串中包含的字元集合。
下面給出 bc 表的演示(沒有的記作 - 1):
下面給出實現程式碼:
/*
* 建立bc表
* string p:模式串
* int bc[]:bc表
*/
void CreatBc(string p, int bc[]){
int lenP = p.size();
int i;
for (i = 0; i < 256; bc[i++] = -1);
for (i = 0; i < lenP; ++i){
bc[p[i]] = i;
}
}
BC 的特點:
1.模式串與文字串的匹配是自右向左的進行。
2.一旦模式串與文字串失配,模式串依靠 bc 表向右移動若干個字元。
4.2 好字尾
這裡 I 和 II 以及成功匹配,A 和 B 發生失配,那麼這時模式串移動到下一位置的條件就是 III 和 IV 是匹配的。
讓我們把視野放到模式串上,如果 I 和 II 匹配,當 II 前面一個字元發生失配,那麼模式串對應需要向右移動 12 個字元。然後在 gs 表中記錄數字 12。
但是,直接得到 gs 表十分困難,我們需要一個 ss 表作為中間轉換。這裡的 7 表示 I 和 II 的匹配長度是 7。ss 表的建立過程如下:
/*
* 建立ss表
* string p:模式串
* int ss[]:ss表
*/
void CreatSs(string p, int ss[]){
int lenP = p.size();
int i, hi, lo;
ss[lenP - 1] = lenP;
for (hi = lo = lenP - 1, i = lo - 1;i >= 0; --i){
if (i > lo && ss[lenP - 1 - hi + i] <= i - lo){
ss[i] = ss[lenP - 1 - hi + i];
}
else{
hi = i;
lo = __min(hi, lo);
while (lo >= 0 && p[lo] == p[lenP - 1 - hi + lo]){
lo--;
}
ss[i] = hi - lo;
}
}
}
/*
* 建立gs表
* string p:模式串
* int gs[]:gs表
*/
void CreatGs(string p, int gs[]){
int lenP = p.size();
int *ss = new int[lenP];
CreatSs(p, ss);
int i, j;
for (i = 0; i < lenP; ++i){
gs[i] = lenP;
}
for (i = 0, j = lenP - 1; j >= 0; --j){
if (ss[j] == j + 1){
while (i < lenP - 1 - j){
gs[i++] = lenP - 1 - j;
}
}
}
for (i = 0; i < lenP - 1; ++i){
gs[lenP - 1 - ss[i]] = lenP - 1 - i;
}
delete[] ss;
}
GS 的特點:
1.模式串與文字串的匹配是自右向左的進行。
2.一旦模式串與文字串失配,模式串依靠 gs 表向右移動若干個字元。
BM 的特點:
1.模式串與文字串的匹配是自右向左的進行。
2.一旦模式串與文字串失配,模式串依靠 bc 表和 gs 表向右移動若干個字元。(取 bc 表和 gs 表的較大值)
4.3 程式碼實現
/*
* BM法:用於字串匹配
* string t:文字串
* string p:模式串
* 返回值:返回首次匹配(完全匹配)位置(失敗返回-1)
*/
int BoyerMoore(string t, string p){
int lenT = t.size();
int lenP = p.size();
int *bc = new int[256];
CreatBc(p, bc);
int i, j;
/*for (i = 0; i <= lenT - lenP;){
for (j = lenP - 1; j >= 0; --j){
if (t[i + j] != p[j]){
i += (j - bc[t[i + j]] > 0) ? j - bc[t[i + j]] : 1;
break;
}
}
if (j == -1){
return i;
}
}*/
int *gs = new int[lenP];
CreatGs(p, gs);
for (i = 0; i <= lenT - lenP;){
for (j = lenP - 1; j >= 0; --j){
if (t[i + j] != p[j]){
int max = __max(j - bc[t[i + j]], gs[j]);
i += max;
break;
}
}
if (j < 0){
return i;
}
}
return -1;
}