1. 引言
前一篇中介紹了字符串KMP算法,其利用失配時已匹配的字符信息,以確定下一次匹配時模式串的起始位置。本文所要介紹的Boyer-Moore算法是一種比KMP更快的字符串匹配算法,它到底是怎么快的呢?且聽下面分解。
不同於KMP在匹配過程中從左至右與主串字符做比較,Boyer-Moore算法是從模式串的尾字符開始從右至左做比較。下面討論的一些遞推式都與BM算法的這個特性有關。
思想
首先,我們一般化匹配失敗的情況,設主串\(y\)、模式串\(x\)的失配位置為i+j
與i
,且主串、模式串的長度各為\(n\)與\(m\),如下圖:
已匹配上的字符結構:
失配后下一次匹配時,模式串應如何對齊於主串呢?從上圖中看出,我們可以利用兩方面的信息:
- 已經匹配上的字符結構,
- 主串失配位置的字符
前一篇中的KMP算法只利用第一條信息,而Boyer-Moore算法則是將這兩方面的信息都利用到了,故模式串的移動更為高效。同時,根據這兩方面信息(已匹配信息與失配信息),Boyer-Moore算法引申出來兩條移動規則:好后綴移動(good-suffix shift)與壞字符移動(bad-character shift)。
實例
Moore教授在這里給出BM算法一個實例,比如主串=HERE IS A SIMPLE EXAMPLE
,模式串=EXAMPLE
。第一次匹配如下圖:
在第一次匹配中,模式串在尾字符發生失配,而主串的失配字符為S
,且S
不屬於模式串的字符;因此下一次匹配時模式串指針應向右移動7
位(壞字符移動)。第二次匹配如下圖:
第二次匹配也是在模式串尾字符發生失配,但不同的是主串的失配字符為P
屬於模式串的字符;因此下一次匹配時模式串的P
(從右開始第一次出現)應對齊於主串的失配字符P
(壞字符移動)。第三次匹配如下圖:
在第三次匹配中,模式串的后綴MPLE
完全匹配上主串,主串的失配字符為I
,不屬於模式串的字符;那么下一次匹配是模式串指針應怎么移動呢(是壞字符移動,還是好后綴移動?)?BM算法采取的辦法:移動步數=\(\max\{壞字符移動步數,\ 好后綴移動步數\}\)。(具體移動步數的計算會在下面給出),這里是按好后綴移動;第四次匹配如下圖:
第四次匹配的情況與第二次類似,應按壞字符移動,第五次匹配(模式串與主串完全匹配)如下圖:
2. BM算法詳述
好后綴移動
因已匹配上的字符結構正好為模式串的后綴,故名之為好后綴
。好后綴移動一般分為兩種情況:
- 移動后,模式串有子串能完全匹配上好后綴;
- 移動后,模式串只有能部分匹配上好后綴的子串
我們用數組bmGs[i]
表示模式串的失配位置為i
時好后綴移動的步數。第一類情況如下圖:
第二類情況如下圖:
接下來的問題是應如何計算bmGs[i]
呢?我們引入suff
函數,其定義如下:
表示了模式串中末字符為x[i]
的子串能匹配模式串后綴的最大長度。其中,suff[i]=m
。
-
對於第一類情況,令
i+1=m-suff[a]
,則x[i+1..m-1]=x[m-suff[a]..m-1]
;根據suff
函數的定義,有x[m-suff[a]..m-1]=x[a-suff[a]-1..a]
;則x[i+1..m-1]=x[a-suff[a]-1..a]
,即可得到bmGs[i]=bmGs[m-suff[a]-1]=m-1-a
。 -
對於第二類情況,由字符的部分匹配可得
x[0..m-1-bmGs[i]]=x[bmGs[i]..m-1]
,即suff[m-1-bmGs[i]]=m-bmGs[i]
。令m-bmGs[i]=a
,有suff[a-1]=a
。因為是部分匹配,故bmGs[i] = m-a > i+1
,則i < m-a-1
。綜上,當i < m-a-1
且suff[a-1]=a
時,bmGs[i]=m-a
。 -
有可能上述兩種情況都沒能被匹配上,則
bmGs[i]=m
。
綜合上述三類情況,bmGs
數組計算的實現代碼(參看[2]):
void preBmGs(char *x, int m, int bmGs[]) {
int i, j, suff[XSIZE];
suffixes(x, m, suff);
// case 3, default value
for (i = 0; i < m; ++i)
bmGs[i] = m;
j = 0;
// case 2
for (i = m - 1; i >= 0; --i)
if (suff[i] == i + 1)
for (; j < m - 1 - i; ++j)
if (bmGs[j] == m)
bmGs[j] = m - 1 - i;
// case 1
for (i = 0; i <= m - 2; ++i)
bmGs[m - 1 - suff[i]] = m - 1 - i;
}
壞字符移動
壞字符移動是根據主串失配位置的字符y[i+j]
而進行的移動。同樣地,我們用數組bmBc[c]
表示主串失配位置字符為c
時壞字符移動的步數。壞字符移動一般分為兩種情況:
-
模式串
x[0..i-1]
有字符y[i+j]
且第一次出現,如下圖:
-
整個模式串都不包含該字符串,如下圖:
據此,可以將bmBc[c]
定義如下:
表示距模式串末字符最近的c
字符;若c
字符未出現在模式串中,則bmBc[c]=m
。C實現代碼:
void preBmBc(char *x, int m, int bmBc[]) {
int i;
for (i = 0; i < ASIZE; ++i)
bmBc[i] = m;
for (i = 0; i < m - 1; ++i)
bmBc[x[i]] = m - i - 1;
}
suff函數計算
bmGs[i]
的計算依賴於suff
函數;如何更為高效的計算suff
函數成為了接下來需要考慮的問題。符號標記的定義如下:
i
表示當前位置;f
記錄上一輪匹配的起始位置;g
記錄上一輪匹配的失配位置。
這里所說的匹配
指的是與模式串后綴的匹配。同樣地,一般化匹配過程,如下圖:
當g < i < f
則必有x[i]=x[m-1-(f-i)]=x[m-1-f+i]
;
- 若
suff[m-1-f+i] < i-g
,則suff[i]=suff[m-1-f+i]
; - 否則,
suff[i]
與suff[m-1-f+i]
沒有關系,要根據定義進行計算。
C實現代碼:
void suffixes(char *x, int m, int *suff) {
int f, g, i;
suff[m - 1] = m;
g = m - 1;
for (i = m - 2; i >= 0; --i) {
if (i > g && suff[i + m - 1 - f] < i - g)
suff[i] = suff[i + m - 1 - f];
else {
if (i < g)
g = i;
f = i;
while (g >= 0 && x[g] == x[g + m - 1 - f])
--g;
suff[i] = f - g;
}
}
}
復雜度分析
3. 參考資料
[1] Moore, Boyer-Moore algorithm example.
[2] Thierry Lecroq, Boyer-Moore algorithm.
[3] sealyao, Boyer-Moore算法學習.