Hash 衝突的通常解決方案與字符串查找中 hash 的使用

使用什麼數據結構存儲HASH

將每一項存在數組中,經過下標來索引。這種實現的方式問題在於:算法

  1. 要存儲的key不是int,不能做爲下標;

解決方案:將key從string映射成int數組

  1. 須要的key很是多,儲存key所須要的空間可能很是大

解決方案:將全部可能的key映射到一個大小爲m的table中,理想狀況 m=n,n表示table中key的個數。問題:有可能形成衝突,即兩個不一樣的key計算hash以後,卻獲得了同一個keybash

如何將key映射到table的索引的方案

使用hash函數。數據結構

除法

h(k)=k mod mapp

這種方式選擇的m一般是與2的冪次方不太接近的質數ide

乘法

h(k) = [(a · k) mod 2^w] >> (w − r)

其中a是個隨機數,k包含w個bit,m通常選擇 m=2^r函數

取值規則以下: ui

全局hash

h(k)=[(ak+b)mod p]mod m
其中a,b是{0,..,p-1}中的隨機值,P是一個大的質數spa

使用鏈表解決hash衝突

若是key是同樣的,就在table的當前索引值以後加一個鏈表,指向新的加入的值,此時,最壞的狀況就是,全部的key都hash衝突,致使最壞的查找時間爲O(n).net

簡單一致hash

假設每一個key被映射到table中的任意一個索引的機率是同樣的,與其它的key經過hashing計算出來的位置無關。
在這種假設下 ,假設一共有n個key,表的大小爲m,那麼每一個鏈條的長度

α =n/m

那麼通常狀況下,運行時間爲 O(1+α),於是能夠看到在假設的前提之下,使用鏈表解決hash衝突是個不錯的選擇

使用open address來解決hash衝突

具體策略爲,hash函數包括要計算hash的key和嘗試的次數來獲得具體的下標

假設通過3次插入數據,h(586,1)=1,h(133,1)=2,h(112,1)=4

再次插入一個數據h(226,1)=4,此時產生了衝突,增長重試的次數,獲得h(226,2)=3此時尚未存儲值,能夠插入

  • 插入:經過給定的hash函數計算下標位置,若是計算出來的下標沒有值,或者數據已經刪除了,就插入,不然增長嘗試的次數,再次計算
  • 搜索:經過hash函數計算獲得下標,若是獲得的key和要搜索的key不一致,就增長嘗試的次數,直到找到或者是計算獲得的下標所在處沒有值,就中止
  • 刪除:首先找到對應的值,此時,僅標記爲這個數據已經刪除了,可是不把存儲的地方置爲空

    標記的方式用於解決,示例中的,加入刪除了112,在查找226的過程當中,計算h(226,1)==4,而以前的位置被112佔據,若是刪除112的時候置爲空,那麼此時會標記爲找不到,很明顯不正確,若是僅標記爲已經刪除則能夠解決這個問題,對於帶有刪除標記的位置,一樣能夠插入,這樣就解決了問題

嘗試的策略選擇

  1. 線性增加。選取h(k,i)=(h'(k)+i)mod m,其中h'(k)爲一個可行的hash函數,這種場景下它是可以去遍歷全部的存儲數組的位置,可是這種方式存在一個問題,隨着已經存儲的數據越多,須要嘗試的次數也就越多,最終插入和搜索將不會是常數時間
  2. double hash。選取h(k,i)=(h_1(k)+ih_2(k))mod\space m,當h_2和m互爲素數的時候,就能夠遍歷全部的存儲數組的位置

    這種狀況下,須要嘗試的次數爲1/(1-\alpha),\alpha=n/m

hash存儲的表大小(m)應該是多少?

指望查找的時間是常量,那麼但願m=\theta(n),考慮到m過小,查詢慢;m太大,浪費

  1. 須要增加m。能夠首先使得m是一個較小的值,而後再使m增大爲m'。考慮兩種增加策略:
    • m'=m+1,此時的時間花銷爲\theta(1+2+3+..+n)=\theta(n^2)
    • m'=2m,此時的時間花銷爲\theta(1+2+4+8+...+n)=\theta(n)
  2. 須要縮小m。若是刪除了表中的不少數據,原來的佔據空間過大,存在浪費,最好減小空間的浪費
    • m'=n/2,將m變爲原來的一半。假設僅有8個元素,若是再插入一個元素,須要一次增加達到16,此時再刪除一個元素,又要縮減到8,每次都須要移動原來的\theta(n)元素
    • m'=n/4,使得m變爲原來的一半。這個時候並不會出現上面的問題

hash的運用

給定兩個字符串s和t,須要判斷s是否在t中出現。
最簡單的方法是兩次遍歷:

for i in range(len(t)-len(s)):
    for j in range(len(s)):
        依次對比是否可以成功匹配
複製代碼

它的執行規則爲遍歷整個的字符串,而後依次去匹配短字符串s是否存在原來的數組中,沒有找到,依次後移

可看到總的時間爲O(|s|.|t|)

Karp-Rabin算法

使用Karp-Rabin算法提升速度,對於要匹配的字符串s,能夠直接算出它的hash值,對於字符串t,須要首選獲取一個長度爲|s|的字符串,一樣能夠計算它的hash值

若是不知足,在下一次的移動過程當中,實際上就是要剪掉原有獲取的第一個字符串的hash值,並增長一個新的字符串的hash值,如圖,黃色塊表示要去掉的,綠色塊表示新增的,按照這種方式一直進行下去

分析過程當中能夠看到從t中獲取的字符串s,須要通過以下兩步操做:

  1. r.skip(oldChar)
  2. r.append(newChar)
  3. 計算新的hash值

若是在上面的計算過程都可以在常量時間內完成,那麼總共的花銷爲O(|t|)。具體實施以下:

def rhCombinationMatch(self):
		winLength = len(self.findStr)
		//構建要查找的字符串RollingHash對象
		winRh = RollingHashCombination(self.findStr)
		lineLen = len(self.lines)
		//構建要屢次計算的字符串的RollingHash對象
		matchRh = RollingHashCombination(self.lines[0:winLength])
		for i in range(0,lineLen-winLength+1):
		    //判斷兩個的hash值是否一致
			if matchRh.hash() == winRh.hash():
				sequence=self.lines[i:i+winLength]
				# 若是一致,排除hash衝突的影響,看下字符串是否相等
				if sequence == self.findStr:
					self.count+=1
			if i+winLength<lineLen:
			    //沒有匹配到,變換新的字符串,去掉第一個,加上下一個
				matchRh.slide(self.lines[i],self.lines[i+winLength])
複製代碼

構建的RollingHash對象以下,它主要負責去將每個步驟組裝起來

class RollingHashCombination(object):
	""" 將rolling hash的每一步組合起來 """
	def __init__(self,s):
		base = 7
		p=499999
		self.rhStepByStep = RollingHashStepByStep(base,p)
		for c in s:
			self.rhStepByStep.append(c)
		self.chash = self.rhStepByStep.hash()
	
	def hash(self):
		return self.chash
    //依次刪掉以前的值 , 添加新的值
	def slide(self,preChar,nextChar):
		""" 刪掉以前的值 , 添加新的值 """
		self.rhStepByStep.skip(preChar)
		self.rhStepByStep.append(nextChar)
		self.chash = self.rhStepByStep.hash()
複製代碼

舉例假設有5個字符串爲"ABCDEF",要找的字符串長度爲3,而hash值僅根據ASCII來直接拼接,真整個計算過程匹配以下:

  1. 第一個匹配的字符串爲 "abc",對應的hash值爲 656667
  2. 沒有找到,首先移除第一個字符,按照100進制來計算,有 656667-65*100^2
  3. 在後面添加一個字符D,計算結果爲 6667*100+68

於是原始的字符從656667演變成了666768。假設

  • n(0) 舊數字
  • n(1) 新數字
  • old 要刪除的元素
  • new 要增長的元素
  • base表示進制
  • k表示要比較的字串的長度

那麼n(1) = (n(0)-old*base^(K-1))*base+new,假設舊數字的hash值是 h1,新數字的hash是

h2=[(n(0)-old*base^(K-1))*base+new] mod p
      =[(n(0) mode p)*base -old *(base^(k) mod p) +new ] mod p  //對同行一個數求兩次餘數不會改變結果
複製代碼

使magic = base(k) mod p 而 h1 = n(0) mod p,h2= [h1base -oldmagic +new ]mod p

代碼實現以下,負責每一個步驟hash值的計算

class RollingHashStepByStep(object):
	""" 對RollingHash進行一步一步的拆分,能夠分紅兩個步驟,每一個步驟都會生成對應的hash值 """
	def __init__(self, base,p):
		""" 獲得一個rollinghash初始值 """
		super(RollingHashStepByStep, self).__init__()
		self.base = base
		# 質數
		self.p = p
		# 剛開始沒有元素
		self.chash= 0 
		# 剛開始沒有元素 magic = magic ** k %p k=0
		self.magic= 1
		self.ibase = base ** (-1) 
	# 保證數據小
	def append(self,newChar):
		""" 在原有的hash基礎上增長一個字符,計算其hash值 """
		# old 返回一個字串的 ASCII值
		new10=ord(newChar)
		self.chash = (self.chash * self.base + new10 ) % self.p
		#滑動窗口中增長一個元素,根據magic的定義 magic是base的長度的次方
		self.magic = (self.magic * self.base) 

	def skip(self,oldChar):
		""" 在原有的hash基礎上去掉一個字符,計算其hash值 """
		# hash-old*magic 多是負值 old < base magic <p
		self.magic =int(self.magic * self.ibase) 
		# todo 進制計算,爲何傳進來的數字不須要轉換成對應的進制 在不用base的地方進行解答
		old10 =ord(oldChar); 
		self.chash = (self.chash-old10*self.magic + self.p * self.base )  % self.p
	
	def hash(self):
		return self.chash
複製代碼
相關文章
相關標籤/搜索