Hash衝突的一般解決方案與字串查詢中hash的使用
將每一項存在陣列中,通過下標來索引。這種實現的方式問題在於:
- 要儲存的key不是int,不能作為下標;
解決方案:將key從string對映成int
- 需要的key非常多,儲存key所需要的空間可能非常大
解決方案:將所有可能的key對映到一個大小為m的table中,理想情況 m=n,n表示table中key的個數。問題:有可能造成衝突,即兩個不同的key計算hash之後,卻得到了同一個key
如何將key對映到table的索引的方案
使用hash函式。
除法
h(k)=k mod m
這種方式選擇的m通常是與2的冪次方不太接近的質數
乘法
其中a是個隨機數,k包含w個bit,m一般選擇
取值規則如下:

全域性hash
h(k)=[(ak+b)mod p]mod m
其中a,b是{0,..,p-1}中的隨機值,P是一個大的質數
使用連結串列解決hash衝突
如果key是一樣的,就在table的當前索引值之後加一個連結串列,指向新的加入的值,此時,最壞的情況就是,所有的key都hash衝突,導致最壞的查詢時間為O(n)

簡單一致hash
假設每個key被對映到table中的任意一個索引的概率是一樣的,與其它的key通過hashing計算出來的位置無關。
在這種假設下 ,假設一共有n個key,表的大小為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的時候置為空,那麼此時會標記為找不到,很明顯不正確,如果僅標記為已經刪除則可以解決這個問題,對於帶有刪除標記的位置,同樣可以插入,這樣就解決了問題
嘗試的策略選擇
- 線性增長。選取h(k,i)=(h'(k)+i)mod m,其中h'(k)為一個可行的hash函式,這種場景下它是能夠去遍歷所有的儲存陣列的位置,但是這種方式存在一個問題,隨著已經儲存的資料越多,需要嘗試的次數也就越多,最終插入和搜尋將不會是常數時間
- double hash。選取 ,當 和m互為素數的時候,就可以遍歷所有的儲存陣列的位置
這種情況下,需要嘗試的次數為
hash儲存的表大小(m)應該是多少?
期望查詢的時間是常量,那麼希望 ,考慮到m太小,查詢慢;m太大,浪費
- 需要增長m。可以首先使得m是一個較小的值,然後再使m增大為m'。考慮兩種增長策略:
- m'=m+1,此時的時間花銷為
- m'=2m,此時的時間花銷為
- 需要縮小m。如果刪除了表中的很多資料,原來的佔據空間過大,存在浪費,最好減少空間的浪費
- m'=n/2,將m變為原來的一半。假設僅有8個元素,如果再插入一個元素,需要一次增長達到16,此時再刪除一個元素,又要縮減到8,每次都需要移動原來的 元素
- 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,需要經過如下兩步操作:
- r.skip(oldChar)
- r.append(newChar)
- 計算新的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來直接拼接,真整個計算過程匹配如下:
- 第一個匹配的字串為 "abc",對應的hash值為 656667
- 沒有找到,首先移除第一個字元,按照100進位制來計算,有 656667-65*100^2
- 在後面新增一個字元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= [h1 base -old magic +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 複製程式碼