前幾天打算一直想找一個時間把字符串匹配算認真弄一下,今天不想看其他的東西,那就想着把字符串匹配算法好好整理梳理一下。
字符串匹配算法有幾種相對比較出名的,分別是BF(暴力破解),RK()、BM()、KMP()。下文中 主串為被匹配的串, 模式串為匹配的串。 例如 s = “aabbcc”(主串),a = “aa" (模式串)
BF算法
首先我大概把BF說一下,就是BF(Brute Force)顧名思義就是暴力破解算法,意思就是我們在主串中,檢查的起始位置分別是 0、1、2…n-m 且長度為 m 的 n-m+1 個子串(n為主串的長度, m為模式串的長度),看有沒有跟模式串匹配的。這種算法的時間復雜度為O(n*m), 空間復雜度為O(1)。
雖然BF算法的時間復雜度高,但是如果在主串和模式串的長度不長時,這種算法的效率也不低,而且時間復雜度O(n*m)是最壞的時間復雜度,大部分情況下都不會出現這種情況。另外這種方法也是最簡單和易於理解的。
圖示步驟
RK算法
主串的長度為n, 模式串的長度為m,在BF算法中相當於我們將主串分成n-m+1個字串,然后使用模式串依次對這n-m-1個字串進行匹配。而在RK算法的思想就是我們根據模式串的長度求出主串中n-m+1個字串,然后分別對這n-m+1個字串進行hash求值,最后使用模式串的hash值進行比對,如果相等,為了防止hash沖突我們在對兩個hash值對應的串進行對比是否相等。從而得出結果。這種算法時間復雜度為O(n),空間復雜度為O(1)。
圖示
BM算法
前面兩種匹配算法中當遇到不匹配的字符時,BF 算法和 RK 算法的做法都是將模式串往后移動一位,然后從模式串的第一個字符開始重新匹配。一直到最后完成匹配為止。這兩種都是從頭部開始匹配,但是對於BM算法中他主要考慮和改進了前兩種算法的不足,他主要是從模式串的尾部到頭部依次對主串進行匹配的。而在匹配的過程中每一次向后移動幾位是由兩個規則決定出來的。即壞字符規則和好后綴規則。
壞字符規則
我們從模式串的末尾往前倒着匹配,當我們發現某個字符沒法匹配的時候。我們把這個沒有匹配的字符叫作壞字符。
這里的情況分成兩種:
(1)如果發現模式串中並不存在這個字符,我們可以將模式串直接往后滑動到壞字符的后面一位,再從模式串的末尾字符開始比較。
(2)如果發現模式串中存在這個字符,這里我們假定這個壞字符對應於模式串中的下標是x, 而壞字符在模式串出現離下標x前最近的位置是y(防止壞字符在模式串中多次出現,導致滑動過頭的情況),這時應該移動x-y位,然后重新開始從尾進行匹配。
好后綴規則
好后綴規則主要是為了防止x-y的值為負數,例如主串是' aaaaaaaaaaaaaaaa',模式串是' baaa'。這里不但不會向后滑動模式串,還有可能倒退。好后綴意思就是模式串后面已經匹配成功的字符串。
我們把已經匹配的z字符串叫作好后綴,記作{u}。我們拿它在模式串中查找,這里也存在兩種情況
(1)如果找到了另一個跟{u}相匹配的子串{u*},那我們就將模式串滑動到子串{u*}與主串中{u}對齊的位置。
(2)如果好后綴在模式串中不存在可匹配的子串{u*},我們就查找{u}中的子串是否存在跟模式串的前綴子串匹配的結果。
壞字符和好后綴進行比較來得出每次移動的位數
我們可以分別計算好后綴和壞字符往后滑動的位數,然后取兩個數中最大的,作為模式串往后滑動的位數。這種處理方法還可以避免我們前面提到的,根據壞字符規則,計算得到的往后滑動的位數,有可能是負數的情況。
代碼實現
因此壞字符和好后綴兩者是獨立的,我們可以先實現壞字符部分,當遇到壞字符時,我們需要在得到兩個值一個就是主串中壞字符對應模式串的下標x, 然后得到主串中對應的壞字符在模式串中最靠近x的位置下標(如果每次都使用遍歷進行查找 這樣效率低,我們可以采用空間換時間的辦法,設置一個數組來記錄出現的位置,如果全是英文字母的話,數組大小為256,小標為asc碼,值為模式串的下標)
例如
初始化壞字符的輔助數組的代碼
1 NUMS_SIZE = 256
2
3 def generateBC(str_b, bc):
6 for i in range(len(str_b)): # 將字符存儲到對應下標的位置
7 bc[ord(str_b[i])] = i
第二個就是后綴子串的問題, 這里我們設置兩個輔助數組,一個是suffix數組,下標表示后綴字符的長度,值表示在前綴中出現的位置。另一個是prefix數組,下標表示后綴字符的長度,值表示是否是前綴字串。
例如:
初始化好后綴的輔助數組代碼(建議自己畫圖理解一下,不然代碼不好理解)
1 def generateGS(str_b, suffix, prefix): # 針對模式串初始化相應的suffix數組和prefix數組。 2 str_b_length = len(str_b)
7 for i in range(str_b_length-1): 8 j, k = i, 0 9 while i >=0 and str_b[j] == str_b[str_b_length-1-k]: 10 j -= 1
11 k += 1
12 suffix[k] = j+1
13 if j < 0: 14 prefix[k] = True
在寫完上面上個規則之后,我們來進行匹配的主體函數
2 def moveByGS(j, m, suffix, prefix): # 找到好后綴對應的移動位數 3 k = m-1-j 4 if suffix[k] != -1: 5 return j - suffix[k]+1
6 for i in range(j+2, m): 7 if prefix[m-i]: 8 return i 9 return m 10
11
12 def BM(str_a, str_b): # BM算法 13 str_a_len, str_b_len = len(str_a), len(str_b) 14 bc = [-1] * NUMS_SIZE 15 generateBC(str_b, bc) 16 suffix, prefix = [-1] * str_b_len, [False] * str_b_len 17 generateGS(str_b, suffix, prefix) 18 i = 0 19 while i <= (str_a - str_b_len): # 從第一個位置開始查找 20 j = str_b_len-1
21 while j >=0: 22 if str_a[i+j] != str_b[j]: 23 break
24 j -= 1
25 if j < 0: 26 return i 27 x, y = j - bc[ord(str_a[i+j])], 0 # x表示壞字符移動的位數 28 if j < str_b-1: 29 y = moveByGS(j, str_b_len, suffix, prefix) # 得到好后綴的移動位數 30 i = i + max(x, y) # 取最大的進行移動 31
32 return -1
KMP算法