Rabin-Karp
字符串快速查找算法和FNV hash
算法是golang中strings包中字符串查所用到的具體算法,算法的核心就在於循環hash,而 FNV hash
則是散列方法的具體算法實現。git
Rabin-Karp算法思想:github
FNV hash:golang
將字符串看做是字符串長度的整數,這個數的進制是一個質數。計算出來結果以後,按照哈希的範圍求餘數獲得結果。算法
其中不一樣機制對應質數分別是:bash
32 bit FNV_prime = 224 + 28 + 0x93 = 16777619
64 bit FNV_prime = 240 + 28 + 0xb3 = 1099511628211
128 bit FNV_prime = 288 + 28 + 0x3b = 309485009821345068724781371
256 bit FNV_prime = 2168 + 28 + 0x63 = 374144419156711147060143317175368453031918731002211
512 bit FNV_prime = 2344 + 28 + 0x57 =
35835915874844867368919076489095108449946327955754392558399825615420669938882575
126094039892345713852759
1024 bit FNV_prime = 2680 + 28 + 0x8d =
50164565101131186554345988110352789550307653454047907443030175238311120551081474
51509157692220295382716162651878526895249385292291816524375083746691371804094271
873160484737966720260389217684476157468082573
複製代碼
以上這幾個數都是質數(哈希的理論基石,質數分辨定理),簡單地說就是:n個不一樣的質數能夠「分辨」的連續整數的個數和他們的乘積相等。「分辨」就是指這些連續的整數不可能有徹底相同的餘數序列。證實詳見ui
若是想要獲得不是上面進制的hash:spa
好比想獲得24位的哈希值,方法:取上面比24大的最小的位數,固然是32了,先算對應32位哈希值,再轉換成24位的。 轉換方法:32 - 24 = 8, 好了把獲得的32砍成兩段,高8位最和低24位。第8位與低24位中的低8位作抑或,獲得的24位值是最終結果。 (hash»24) ^ (hash & 0xFFFFFF);3d
若是想獲得範圍在0~9999的哈希值,方法:取上面比9999大的最小的位數,固然是32,先算對應32位哈希值,再mod(9999 +1)。code
如上所述,結合Rabin-Karp
的思想加上FNV hash
就能夠實現所謂的字符串快速查找算法了。
src/strings/strings.go
// Rabin-Karp 中須要使用的32位FNV hash算法中的基礎質數(至關於進制)
const primeRK = 16777619
// hash散列方法, 返回字符串hash以及 primeRK的k-1(len(sep)-1)次方
func hashStr(sep string) (uint32, uint32) {
hash := uint32(0)
for i := 0; i < len(sep); i++ {
hash = hash*primeRK + uint32(sep[i]) // 循環獲得字符串hash
}
// 位運算巧妙的獲取 primeRK 的 len(sqp)-1 次方
var pow, sq uint32 = 1, primeRK
for i := len(sep); i > 0; i >>= 1 {
if i&1 != 0 {
pow *= sq
}
sq *= sq
}
return hash, pow
}
func indexRabinKarp(s, substr string) int {
// Rabin-Karp search
hashss, pow := hashStr(substr)
n := len(substr)
var h uint32
// 計算目標字符串前n位hash並與待匹配字符串hash進行對比
for i := 0; i < n; i++ {
h = h*primeRK + uint32(s[i])
}
// hash相同而且字符串相等則返回當前位置下標
if h == hashss && s[:n] == substr {
return 0
}
// Rabin-Karp 算法的精華所在,相面詳細介紹
for i := n; i < len(s); {
h *= primeRK
h += uint32(s[i])
h -= pow * uint32(s[i-n])
i++
if h == hashss && s[i-n:i] == substr {
return i - n
}
}
return -1
}
複製代碼
結合源碼能夠知道:若是如今咱們要求第i位日後k個長度字符串的hash能夠列個公式
其中:s[i] 表示第i位字節對應32位整數也就是上面uint32(s[i])
(這裏強轉一下也就是對2^32次方取餘了),R 就是 對應進制的FNV_prime
。
由上述類推H(i+1)的hash公式就是:
由此能夠看出來:每次咱們其實不用從新計算整個字符串的hash而是直接原hash值乘以R加上s[k-1]而且減去s[i]R^(k-1),這裏也就是 FNV_prime的k-1次方,對應上面代碼:
var pow, sq uint32 = 1, primeRK
for i := len(sep); i > 0; i >>= 1 {
if i&1 != 0 {
pow *= sq
}
sq *= sq
}
複製代碼
相對於暴力匹配O(mn)是時間複雜度, Rabin-Karp 的時間複雜度在O(m+n), 最壞的狀況每次hash相同字符串不相同時間複雜度會變成O(mn)可是這種狀況比較罕見。
Rabin-Karp 還有個優勢在於他能夠進行多模式匹配,好比論文重複性檢測,只要預熱計算出全部帶匹配字符串的hash,目標字符串的遍歷比較時只是多一步比較全部待匹配字符串hash。若是待匹配字符串個數是k,那麼 Rabin-Karp 的時間複雜度是O(nk)。