Rabin-Karp算法在go的實現

原文連接 githubhtml

簡介

Rabin-Karp字符串快速查找算法和FNV hash算法是golang中strings包中字符串查所用到的具體算法,算法的核心就在於循環hash,而 FNV hash則是散列方法的具體算法實現。git

算法思想

Rabin-Karp算法思想:github

  • 假設待匹配字符串長度M,目標字符串長度N(N>M)
  • 首先計算待匹配字符串hash,計算目標字符串前M個字符hash
  • 比較前兩個hash值,比較次數N-M+1
    • 若hash不相等,繼續計算目標字符串下一個長度爲M的hash並繼續循環比較
    • 若hash相等則再次判斷字符串是否相等已確保正確性

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就能夠實現所謂的字符串快速查找算法了。

結合golang源碼

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)。

原文連接 github

相關文章
相關標籤/搜索