數據結構與算法之美-字符串匹配(上)
BF (Brute Force) 暴力/樸素匹配算法
主串和模式串
我們在字符串 A 中查找字符串 B,那字符串 A 就是主串,字符串 B 就是模式串。
我們把主串的長度記作 n,模式串的長度記作 m。因為我們是在主串中查找模式串,所以 n>m。
BF算法思想
在主串中,檢查起始位置分別是 0、1、2…n-m 且長度為 m 的 n-m+1 個子串,看有沒有跟模式串匹配的。
BF算法的缺點
在極端情況下,如主串是“aaaaa…aaaaaa”,模式串是“aaaaab”。我們每次都比對 m 個字符,需要比對 n-m+1 次。這種算法的最壞情況時間復雜度是 O(n*m)。
但在實際開發在BF是一種常用的字符串匹配算法。因為實際的軟件開發中,大部分情況下,模式串和主串的長度都不會太長,算法執行效率要比 O(n*m)高很多。而且樸素字符串匹配算法思想簡單,代碼實現也非常簡單。
RK (Rabin-Karp) 算法
它其實就是 BF 算法的升級版。對樸素的字符串匹配算法稍加改造,引入哈希算法,時間復雜度立刻就會降低。
RK算法思想
通過哈希算法對主串中的 n-m+1 個子串分別求哈希值,然後逐個與模式串的哈希值比較大小。
如果某個子串的哈希值與模式串相等,那就說明對應的子串和模式串匹配了,這裏先不考慮哈希沖突的問題。
哈希值是一個數字,數字之間比較是否相等是非常快速的,所以模式串和子串比較的效率就提高了。
巧妙的哈希算法
通過哈希算法計算子串的哈希值的時候,需要遍歷子串中的每個字符。盡管模式串與子串比較的效率提高了,但是算法整體的效率並沒有提高。這就需要設計一個非常巧妙的哈希算法了。
假設要匹配的字符串的字符集中只包含 K 個字符,我們可以用一個 K 進制數來表示一個子串,這個 K 進制數轉化成十進制數,作為子串的哈希值。
假設要處理的字符串只包含 a~z 這 26 個小寫字母,那就用二十六進制來表示一個字符串。計算哈希值的時候,我們只需要把進位從 10 改成 26 就可以。
這種哈希算法有一個特點,在主串中,相鄰兩個子串的哈希值的計算公式有一定關系。即可以使用 s[i-1] 的哈希值很快的計算出 s[i] 的哈希值。公式如下所示:
//其中, h[i]、h[i-1] 分別對應 s[i] 和 s[i-1] 兩個子串的哈希值 h[i] = 26*(h[i-1]-26^(m-1)*(s[i-1]-‘a‘)) + (s[i+m-1]-‘a‘);
其中 26^(m-1) 這部分的計算,可以通過查表的方法來提高效率。事先計算好 26^0、26^1等等,並且存儲在一個長度為 m 的數組中。
公式中的“次方”就對應數組的下標。需要計算 26 的 x 次方的時候,就可以從數組的下標為 x 的位置取值,省去了計算的時間。
綜上所述,可得RK 算法整體的時間復雜度是 O(n)。
哈希算法的缺點
模式串很長,相應的主串中的子串也會很長,通過上面的哈希算法計算得到的哈希值就可能很大,可能會超過了計算機中整型數據可以表示的範圍。
前面設計的哈希算法是沒有散列沖突的。因此,為了能將哈希值落在整型數據範圍內,是可以犧牲一下,允許哈希沖突的。比如將26進制轉為10進制的算法改為數字相加。
當存在哈希沖突的時候,有可能子串和模式串的哈希值雖然是相同的,但是兩者本身並不匹配。我們只需要再對比一下子串和模式串本身就好了。
如果存在大量沖突,就會導致 RK 算法的時間復雜度退化,效率下降。極端情況下,如果存在大量的沖突,每次都要再對比子串和模式串本身,那時間復雜度就會退化成 O(n*m)。
數據結構與算法之美-字符串匹配(上)