如何藉助哈希算法實現高效字符串匹配?——RK算法

前言

字符串匹配算法簡單的有BF算法,RK算法,今天我們只談論RK算法。
BF 算法中的 BF 是 Brute Force 的縮寫,中文叫作暴力匹配算法,也叫樸素匹配算法。從名字可以看出,這種算法的字符串匹配方式很“暴力”,當然也就會比較簡單、好懂,但相應的性能也不高。
在討論RK算法前,定義兩個概念,方便我後面講解。它們分別是主串和模式串。
作爲最簡單、最暴力的字符串匹配算法,BF 算法的思想可以用一句話來概括,那就是,我們在主串中,檢查起始位置分別是 0、1、2…n-m 且長度爲 m 的 n-m+1 個子串,看有沒有跟模式串匹配的。
在這裏插入圖片描述
從上面的算法思想和例子,我們可以看出,在極端情況下,比如主串是“aaaaa…aaaaaa”(省略號表示有很多重複的字符 a),模式串是“aaaaab”。我們每次都比對 m 個字符,要比對 n-m+1 次,所以,這種算法的最壞情況時間複雜度是 O(nm)。
儘管理論上,BF 算法的時間複雜度很高,是 O(n
m),但在實際的開發中,它卻是一個比較常用的字符串匹配算法。

RK 算法

RK 算法的全稱叫 Rabin-Karp 算法,是由它的兩位發明者 Rabin 和 Karp 的名字來命名的。這個算法理解起來也不是很難。

BF 算法的時候講過,如果模式串長度爲 m,主串長度爲 n,那在主串中,就會有 n-m+1 個長度爲 m 的子串,我們只需要暴力地對比這 n-m+1 個子串與模式串,就可以找出主串與模式串匹配的子串。

但是,每次檢查主串與子串是否匹配,需要依次比對每個字符,所以 BF 算法的時間複雜度就比較高,是 O(n*m)。我們對樸素的字符串匹配算法稍加改造,引入哈希算法,時間複雜度立刻就會降低。

RK 算法的思路是這樣的:我們通過哈希算法對主串中的 n-m+1 個子串分別求哈希值,然後逐個與模式串的哈希值比較大小。如果某個子串的哈希值與模式串相等,那就說明對應的子串和模式串匹配了(這裏先不考慮哈希衝突的問題,後面我們會講到)。因爲哈希值是一個數字,數字之間比較是否相等是非常快速的,所以模式串和子串比較的效率就提高了。
在這裏插入圖片描述
不過,通過哈希算法計算子串的哈希值的時候,我們需要遍歷子串中的每個字符。儘管模式串與子串比較的效率提高了,但是,算法整體的效率並沒有提高。有沒有方法可以提高哈希算法計算子串哈希值的效率呢?

這就需要哈希算法設計的非常有技巧了。我們假設要匹配的字符串的字符集中只包含 K 個字符,我們可以用一個 K 進制數來表示一個子串,這個 K 進制數轉化成十進制數,作爲子串的哈希值。表述起來有點抽象,我舉了一個例子,看完應該就能懂了。

比如要處理的字符串只包含 a~z 這 26 個小寫字母,那我們就用二十六進制來表示一個字符串。我們把 a~z 這 26 個字符映射到 0~25 這 26 個數字,a 就表示 0,b 就表示 1,以此類推,z 表示 25。

在十進制的表示法中,一個數字的值是通過下面的方式計算出來的。對應到二十六進制,一個包含 a 到 z 這 26 個字符的字符串,計算哈希的時候,我們只需要把進位從 10 改成 26 就可以。

在這裏插入圖片描述
這個哈希算法你應該看懂了吧?現在,爲了方便解釋,在下面的講解中,我假設字符串中只包含 a~z 這 26 個小寫字符,我們用二十六進制來表示一個字符串,對應的哈希值就是二十六進制數轉化成十進制的結果。

這種哈希算法有一個特點,在主串中,相鄰兩個子串的哈希值的計算公式有一定關係。我這有個個例子,你先找一下規律,再來看我後面的講解。
在這裏插入圖片描述
從這裏例子中,我們很容易就能得出這樣的規律:相鄰兩個子串 s[i-1]和 s[i](i 表示子串在主串中的起始位置,子串的長度都爲 m),對應的哈希值計算公式有交集,也就是說,我們可以使用 s[i-1]的哈希值很快的計算出 s[i]的哈希值。如果用公式表示的話,就是下面這個樣子:

在這裏插入圖片描述
不過,這裏有一個小細節需要注意,那就是 26^(m-1) 這部分的計算,我們可以通過查表的方法來提高效率。我們事先計算好 260、261、262……26(m-1),並且存儲在一個長度爲 m 的數組中,公式中的“次方”就對應數組的下標。當我們需要計算 26 的 x 次方的時候,就可以從數組的下標爲 x 的位置取值,直接使用,省去了計算的時間。
在這裏插入圖片描述
我們開頭的時候提過,RK 算法的效率要比 BF 算法高,現在,我們就來分析一下,RK 算法的時間複雜度到底是多少呢?

整個 RK 算法包含兩部分,計算子串哈希值和模式串哈希值與子串哈希值之間的比較。第一部分,我們前面也分析了,可以通過設計特殊的哈希算法,只需要掃描一遍主串就能計算出所有子串的哈希值了,所以這部分的時間複雜度是 O(n)。

模式串哈希值與每個子串哈希值之間的比較的時間複雜度是 O(1),總共需要比較 n-m+1 個子串的哈希值,所以,這部分的時間複雜度也是 O(n)。所以,RK 算法整體的時間複雜度就是 O(n)。

這裏還有一個問題就是,模式串很長,相應的主串中的子串也會很長,通過上面的哈希算法計算得到的哈希值就可能很大,如果超過了計算機中整型數據可以表示的範圍,那該如何解決呢?

剛剛我們設計的哈希算法是沒有散列衝突的,也就是說,一個字符串與一個二十六進制數一一對應,不同的字符串的哈希值肯定不一樣。因爲我們是基於進制來表示一個字符串的,你可以類比成十進制、十六進制來思考一下。實際上,我們爲了能將哈希值落在整型數據範圍內,可以犧牲一下,允許哈希衝突。這個時候哈希算法該如何設計呢?

哈希算法的設計方法有很多,我舉一個例子說明一下。假設字符串中只包含 a~z 這 26 個英文字母,那我們每個字母對應一個數字,比如 a 對應 1,b 對應 2,以此類推,z 對應 26。我們可以把字符串中每個字母對應的數字相加,最後得到的和作爲哈希值。這種哈希算法產生的哈希值的數據範圍就相對要小很多了。

不過,你也應該發現,這種哈希算法的哈希衝突概率也是挺高的。當然,我只是舉了一個最簡單的設計方法,還有很多更加優化的方法,比如將每一個字母從小到大對應一個素數,而不是 1,2,3……這樣的自然數,這樣衝突的概率就會降低一些。

那現在新的問題來了。之前我們只需要比較一下模式串和子串的哈希值,如果兩個值相等,那這個子串就一定可以匹配模式串。但是,當存在哈希衝突的時候,有可能存在這樣的情況,子串和模式串的哈希值雖然是相同的,但是兩者本身並不匹配。

實際上,解決方法很簡單。當我們發現一個子串的哈希值跟模式串的哈希值相等的時候,我們只需要再對比一下子串和模式串本身就好了。當然,如果子串的哈希值與模式串的哈希值不相等,那對應的子串和模式串肯定也是不匹配的,就不需要比對子串和模式串本身了。

所以,哈希算法的衝突概率要相對控制得低一些,如果存在大量衝突,就會導致 RK 算法的時間複雜度退化,效率下降。極端情況下,如果存在大量的衝突,每次都要再對比子串和模式串本身,那時間複雜度就會退化成 O(n*m)。但也不要太悲觀,一般情況下,衝突不會很多,RK 算法的效率還是比 BF 算法高的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章