- 主串和模式串
在字符串A中查找字符串B,那字符串A就是主串,字符串B就是模式串。
我們把主串的長度記作n,模式串的長度記作m。因爲我們是在主串中查找模式串,所以n>m。
幾種單模式串匹配算法
- BF(暴力)算法
- RK算法
- BM算法
- KMP算法
1. BF(Brute Force)算法
時間複雜度O(n*m),其中n是主串長度,m是模式串長度。
缺陷:忽略了已檢測過的文本信息。
2. RK(Rabin-Karp)算法
如果模式串長度爲m,主串長度爲n,那在主串中,就會有n-m+1個長度爲m的子串。
BF算法需要對比n-m+1次,每次對比都需要依次對比m個字符。
RK算法的思路是:
- 通過哈希算法對主串中的n-m+1個子串分別求哈希值
- 然後逐個與模式串的哈希值比較大小
- 如果某個子串的哈希值與模式串相 等,那就說明對應的子串和模式串匹配了(這裏先不考慮哈希衝突的問題,後面我們會講到)。因爲哈希值是一個數字,數字之間比較是否相等是非常快速的, 所以模式串和子串比較的效率就提高了。
簡單的哈希算法,需要遍歷子串的每個字符,儘管模式串與子串比較的效率提高了,但是,算法整體的效率並沒有提高。
巧妙的設計哈希算法。假設要匹配的字符串的字符集中只包含K個字符,我們可以用一個K進制數來表示一個子串,這個K進制數轉化成十 進制數,作爲子串的哈希值。
假設字符串中只包含a~z這26個小寫字符,我們用二十六進制來表示一個字符串,對 應的哈希值就是二十六進制數轉化成十進制的結果。
這種哈希算法有一個特點,在主串中,相鄰兩個子串的哈希值的計算公式有一定關係。
相鄰兩個子串s[i-1]和s[i] (i表示子串在主串中的起始位置,子串的長度都爲m)。我們可以使用s[i-1]的哈希值很快的計算出s[i]的哈希值。
h[i] = (h[i-1] - 26^(m-1)*(s[i-1]-‘a’)) * 26 + (s[i+m-1] - ‘a’)
可以提前計算26^(m-1)這部分的值,然後通過查表的方式提高效率。
RK算法包含兩部分,計算子串哈希值和模式串哈希值與子串哈希值之間的比較。
- 第一部分,我們前面也分析了,可以通過設計特殊的哈希算法,只需要掃描一遍主串就能計算出所有子串的哈希值了,所以這部分的時間複雜度是O(n)。
- 第二部分,模式串哈希值與每個子串哈希值之間的比較的時間複雜度是O(1),總共需要比較n-m+1個子串的哈希值,所以,這部分的時間複雜度也是O(n)。
所以,RK算法整體的時間複雜度就是O(n)。
如上這種哈希算法是不會有哈希衝突的,因爲我們是基於進制來表示一個字符串的,也就是說,一個字符串與一個二十六進制數一一對應,不同的字符串的哈希值肯定不一樣。
問題:模式串很長,相應的主串中的子串也會很長,通過上面的哈希算法計算得到的哈希值就可能很大,可能會超過了計算機中整型數據可以表示的範圍。
建議:設計數值較小的哈希函數,可能會有哈希衝突。在哈希值相等時,還需再對比一下子串和模式串本身。
3. BM(Boyer-Moore)算法
我們把模式串和主串的匹配過程,看作模式串在主串中不停地往後滑動。當遇到不匹配的字符時,BF算法和RK算法的做法是,模式串往後滑動一位,然後從模式 串的第一個字符開始重新匹配。
在這個例子裏,主串中的c,在模式串中是不存在的,所以,模式串向後滑動的時候,只要c與模式串有重合,肯定無法匹配。所以,我們可以一次性把模式串往 後多滑動幾位,把模式串移動到c的後面。
字符串匹配的關鍵,就是模式串的如何移動最高效。
BM算法本質上就是尋找一種規律,使得模式串和主串匹配過程中,當遇到不字符不匹配的時候,能夠跳過一些肯定不會匹配的情況,將模式串儘量往後多滑動幾位。
BM算法包含兩部分
- 壞字符規則(bad character rule)
- 好後綴規則(good suffix shift)
BM算法模式串的匹配方式是從末尾往前倒着匹配。
壞字符規則
從模式串的末尾往前倒着匹配,當我們發現某個字符沒法匹配的時候。我們把這個沒有匹配的字符叫作壞字符(主串中的字符)。
- 壞字符c在模式串不存在,這個時候,我們可以將模式串直接往後滑動三(模式串長度)位,將模式串滑動到c後面的位置,再從模式串的末尾字符開始比較。
- 壞字符a在模式串中存在,模式串中下標是0的位置也是字符a。這種情況下,我們可以將模式串往後滑動兩位,讓兩個a上下對齊,然後再從模式串的末尾字符開 始,重新匹配。
- 規律:當發生不匹配的時候,我們把壞字符對應的模式串中的字符下標記作si。如果壞字符在模式串中存在,我們把這個壞字符在模式串中的下標記作xi。如果不存在, 我們把xi記作-1。那模式串往後移動的位數就等於si-xi。(注意,我這裏說的下標,都是字符在模式串的下標)
若xi有多個,選擇下標最大的那個,即最靠後的那個。
利用壞字符規則,BM算法在最好情況下的時間複雜度非常低,是O(n/m)。
-
比如,主串是aaabaaabaaabaaab,模式串是aaaa。每次比對,模式串都可以直接後移四位,所以,匹配具有類似特點的模式串和主串的時候,BM算法非常高效。
-
不過,單純使用壞字符規則還是不夠的。因爲根據si-xi計算出來的移動位數,有可能是負數,比如主串是aaaaaaaaaaaaaaaa,模式串是baaa。不但不會向後滑動模式串,還有可能倒退。所以,BM算法還需要用到“好後綴規則”。
好後綴規則
我們把已經匹配的bc叫作好後綴,記作{u}。我們拿它在模式串中查找,如果找到了另一個跟{u}相匹配的子串{u*},那我們就將模式串滑動到子串{u*}與主串 中{u}對齊的位置。
- 若在模式串中找不到另一個等於{u}的子串,我們就直接將模式串,滑動到主串中{u}的後面,因爲之前的任何一次往後滑動,都不可能匹配主串中{u}的情況。
當模式串中不存在等於{u}的子串時,直接將模式串滑動到主串{u}的後面。是否有點太過頭呢?
如果好後綴在模式串中不存在可匹配的子串,那在我們一步一步往後滑動模式串的過程中。 - 只要主串中的{u}與模式串有重合,那肯定就無法完全匹配。
- 但是當模式串滑動到前綴與主串中{u}的後綴有部分重合的時候,並且重合的部分相等的時候,就有可能會存在完全匹配的情況。
所以,在好後綴模式下,若模式串中找不到和好後綴完全匹配的子串,那麼: - 先看好後綴在模式串中,是否有另一個匹配的子串
- 還要考察好後綴的後綴子串,是否存在跟模式串的前綴子串匹配的。
BM算法會分別計算好後綴和壞字符往後滑動的位數,然後取兩者中的大者,作爲模式串往後滑動的位數。
BM算法實現
1.壞字符規則實現
“壞字符規則”本身不難理解。當遇到壞字符時,要計算往後移動的位數si-xi,其中xi的計算是重點,我們如何求得xi呢?
如果我們拿壞字符,在模式串中順序遍歷查找,這樣就會比較低效,勢必影響這個算法的性能。
- 利用哈希表,將模式串中的每個字符及其下標都存到哈希表中
假設字符集爲256,每個字符大小爲1個字節,可用大小爲256的數組,記錄每個字符在模式串中出現的位置,數組下標對應字符的ASCII碼值,數組值爲字符在模式串中的下標。
如上過程翻譯成代碼如下,其中變量b是模式串,m是模式串長度,bc是剛剛講的哈希表:
const SIZE int = 256
func generateBC(b []byte, m int, bc []int) {
for i := 0; i < SIZE; i++ {
bc[i] = -1//初始化bc
}
for i := 0; i < m; i++ {
ascii := int(b[i])//計算b[i]的asccii值
bc[ascii] = i
}
}
單有壞字符規則的BM算法,代碼如下:
func bm(mainStr []byte, modeStr []byte) int {
bc := make([]int, SIZE)
n := len(mainStr)
m := len(modeStr)
generateBC(modeStr, m, bc) //構建壞字符哈希
i := 0 //i表示主串與模式串對齊的第一個字符位置
for i <= n-m {
j := 0
for j = m - 1; j >= 0; j-- { //模式串從後往前匹配
if mainStr[i+j] != modeStr[j] {
break //壞字符對應模式串中的下標是j
}
}
if j < 0 {
return i //匹配成功,返回主串與模式串第一個匹配的字符的位置
}
moveNum := j - bc[int(mainStr[i+j])]
if moveNum <= 0 { //壞字符可能產生負數的移位
moveNum = 1
}
i = i + moveNum
}
return -1
}
好後綴規則實現
回顧一下,前面講過好後綴的處理規則中最核心的內容:
- 在模式串中,查找跟好後綴匹配的另一個子串;
- 在好後綴的後綴子串中,查找最長的、能跟模式串前綴子串匹配的後綴子串;
若這兩個操作直接使用“暴力”匹配,會使得BM算法效率不高。而好後綴也是模式串本身的後綴子串,所以在正式匹配之前,可以對模式串做預處理操作,預先計算好模式串中的每個後綴子串,對應的另一個可匹配子串的位置。
- 通過長度可以唯一確定一個後綴子串。
現在引入suffx數組,數組下標k表示後綴子串的長度,下標對應的數組值表示,在模式串中跟好後綴{u}相匹配的子串{u*}的起始下標位置。
- 若模式串中有多個(大於1個)子串跟後綴子串{u}匹配,那suffix數組中該存儲模式串中最靠後的那個子串的起始位置,也就是下標最大的那個子串的起始位置。
suffx數組可以解決好後綴在模式串中能找到另一個可匹配的情況,但是我們還要在好後綴的後綴子串中,查找最長的能跟模式串前綴子串匹配的後綴子串。
- 用bool類型的prefix數組,來記錄模式串的後綴子串是否能匹配模式串的前綴子串。
- 我們拿下標從0到i的子串(i可以是0到m-2)與整個模式串,求公共後綴子串。
- 如果公共後綴子串的長度是k,那我們就記錄suffix[k]=j(j表示公共後綴子串的起始下標)。
- 如果j等於0,也就是說,公共後綴子串也是模式串的前綴子串,我們就記錄prefix[k]=true。
把suffix數組和prefix數組的計算過程,用代碼實現出來,如下所示:
func generateGS(modeStr []byte, suffix []int, prefix []bool) {
m := len(modeStr)
for i := 0; i < m; i++ {
suffix[i] = -1 //默認是找不到和好後綴匹配的子串
prefix[i] = false //初始化
}
for i := 0; i < m-1; i++ { //modeStr[:i]
j := i
k := 0 //公共後綴子串長度
for j >= 0 && modeStr[j] == modeStr[m-k-1] { //與modeStr[:m-1]求公共後綴
j--
k++
suffix[k] = j + 1 //j+1表示公共後綴子串在modeStr[:i]中的起始下標
}
if j == -1 {
prefix[k] = true //表示有和後綴子串匹配的前綴子串
}
}
}
有了這兩個數組後,在模式串和主串匹配過程中,遇到不能匹配字符時,根據好後綴規則,移動過程如下:
- 假設好後綴的長度是k。我們先拿好後綴,在suffix數組中查找其匹配的子串。
- 如果suffix[k]不等於-1(-1表示不存在匹配的子串),那我們就將模式串往後移動j- suffix[k]+1位(j表示壞字符對應的模式串中的字符下標)。
- 如果suffix[k]等於-1,表示模式串中不存在另一個跟好後綴匹配的子串片段,可如下處理。
- 如果兩條規則都沒有找到可以匹配好後綴及其後綴子串的子串,我們就將整個模式串後移m位。
BM算法的完整版代碼如下:
func bm(mainStr []byte, modeStr []byte) int {
bc := make([]int, SIZE)
n := len(mainStr)
m := len(modeStr)
generateBC(modeStr, m, bc) //構建壞字符哈希
suffix := make([]int, m)
prefix := make([]bool, m)
generateGS(modeStr, suffix, prefix)
i := 0 //i表示主串與模式串對齊的第一個字符位置
for i <= n-m {
j := 0
for j = m - 1; j >= 0; j-- { //模式串從後往前匹配
if mainStr[i+j] != modeStr[j] {
break //壞字符對應模式串中的下標是j
}
}
if j < 0 {
return i //匹配成功,返回主串與模式串第一個匹配的字符的位置
}
x := j - bc[int(mainStr[i+j])]
y := 0
if j < m-1 { //如果有好後綴,j = m-1時,表示沒有好後綴
y = moveByGS(j, m, suffix, prefix) //返回好後綴規則下,模式串移動的位數
}
i = i + mathutil.Max(x, y) //壞字符規則和好後綴規則,取移動位數更多的
//這裏i一定大於0,因爲若無好後綴,那壞字符規則得到的移動位數一定大於0
}
return -1
}
//j表示壞字符對應的模式串中的字符下標,m表示模式串長度
func moveByGS(j, m int, suffix []int, prefix []bool) int {
k := m - 1 - j //好後綴長度
if suffix[k] != -1 { //模式串中存在和好後綴匹配的子串
return j - suffix[k] + 1
}
//這個for循環就是遍歷好後綴的後綴子串,看是否存在prefix[m-r]爲true的情況
for r := j + 2; r <= m-1; r++ { //有好後綴且模式串中沒有和好後綴匹配的子串
//r+2若爲m,此時表示好後綴長度爲1,此時移動m位即可
if prefix[m-r] == true { //m-r就是好後綴子串的長度
return r
}
}
//若好後綴的兩個規則都不命中,則移動m位
return m
}
BM算法總結
- BM算法核心思想是,利用模式串本身的特點,在模式串中某個字符與主串不能匹配的時候,將模式串往後多滑動幾位,以此來減少不必要的字符比較,提高匹配 的效率。
- BM算法構建的規則有兩類,壞字符規則和好後綴規則。好後綴規則可以獨立於壞字符規則使用。因爲壞字符規則的實現比較耗內存,爲了節省內存,我 們可以只用好後綴規則來實現BM算法。