07算法導論-散列表(hash table)

前言

很多的應用都需要一些動態集合結構,這些動態集合結構都支持INSERT, SEARCH, DELETE的字典操作。對於普通的數組進行尋址我們需要Θ(1)\Theta(1)。但是我們實際存儲的關鍵字數比全部的關鍵字要小很多,我們使用hash替代普通的數組,在合理假設的情況下,我們做上述操作的時候,也只需要Θ(1)\Theta(1)。即使hash函數在最糟糕的情況下,也就是hash都碰撞到同一個槽,這樣所需要Θ(n)\Theta(n)
本文首先介紹了hash table是個什麼東西,以及內部實現的原理。然後講解了hash函數,和開放尋址法來處理hash碰撞的問題。最後講解了完全hash,一種在最壞的情況下,也能在Θ(1)\Theta(1)的時間完成search操作。文中有些證明過程就省略了。

hash是什麼

hash table原理說明
我們先考慮一下map獲取其中元素的方法,就是通過key值來獲取的。這裏的hash表也是一樣的道理。如圖所示,我們對輸入的key值,使用設計的hash函數來進行計算,得到相應的尋址地址,然後就可以獲取到key對應的value。
在討論hash函數之前,先介紹一下hash碰撞。
假設有k1,k2,......,knk_1,k_2,......,k_n其中h(ki)h(k_i)=h(kj)h(k_j),則就會在hash table裏面尋址到同一個位置,這樣就會產生hash碰撞。解決hash碰撞的方法就是將slot擴展爲鏈表的結構,slot裏面儲存這指向鏈表的頭部。這也解釋了爲什麼,在最壞的情況下hash的時間複雜度爲n
我們讓n個key映射到table上,table有m個slot,在每一個key映射到每一個slot上的概率都是獨立和相等的情況下,hash表的承載因子爲α=n/m\alpha=n/m
因此搜索一個存在的記錄,其給定的時間爲Θ(1+α)\Theta(1+\alpha)

hash函數

hash函數的好壞與否,主要是看能否把keys均勻的分佈到table的solt裏面。由於我們不知道keys的分佈,或者keys可能在一些局部分佈會比較密集。因此很難找到一種統一的hash函數的方法。

division method

h(k)=k mod mh(k)=k\ mod\ m
m不應爲2k2^k,一個不太接近2的整數冪的素數是一個很好的選擇。

Multiplication method

h(k)=(Ak mod 2w)rsh(wr)h(k)=(Ak\ mod\ 2^w)rsh(w-r)
在這裏插入圖片描述

全域hash

首先需要明確的一點是對於單一的hash函數,我們總能設計相應的key集合,讓每一個key都映射到一個solt裏面,因此就有了隨機選擇hash函數的方法也就是全域hash。
我們先定義一個有限的hash函數集合HH,hash table的槽位0,1,..........m1{0,1,..........m-1},對於k,lUk,l\in U。在獨立隨機分佈的情況下,使得h(k)=h(l)h(k)=h(l)的hash函數有H/m|H|/m個。這是一個概率問題,對於k,l在隨機選擇一個hash函數的情況下,他們發生碰撞的概率爲1/m1/m,因此想要使得k,l發生碰撞的函數個數按概率來說需要H/m|H|/m
定理:Let h be a hash function chosen (uniformly) at random from a universal set H of hash function. Suppose h is used to hash n arbitrary keys into the m slots of a table T. Then, for a given key x, we have E[collision with x]=n/mE[collision\ with\ x] = n/m
上面的定律話句話說就是,從H中隨機選取的一個h,使得x發生碰撞的概率不會大於nm{\frac nm}(證明就省略了,也是indicate function)

構造一個全域函數集

構造一個全域hash函數集有很多種方法,現在介紹一種使用點積的構造方法(dot product)。
Let m be prime, Decompose key k into r+1 digits, each with value in the set {0, 1, …, m-1}. That is , let k = {k0.....kr}\{k_0.....k_r\}, where 0<=ki<=m0<=k_i<=m,
randomized strategy:
Pick a = {a0...ara_0...a_{r}}, where each ai is chosen randomly from {0, 1, … , m-1}
we define ha(k)=ir(kiai)mod xh_a(k)=\sum_i^r{(k_i*a_i)}mod\ x
所以H=mr+1|H|=m^{r+1}
要證明全域hash,即使要證明造成hash碰撞的可能性非常低,低到可以忽略不記。k分解爲{k0...kr}\{k_0...k_r\}恰恰方便證明全域hash函數,
證明過程就不給出了,結論是這樣的a0=((i=1rai(xiyi))(x0y0)1)mod ma_0=((-\sum_{i=1}^r{a_i(x_i-y_i)})*(x_0-y_0)^{-1})mod\ m,
要想造成一次hash碰撞需要試mrm^r次也就是上述說的Hm{\frac {|H|}m}

開放尋址(open address)

開放尋址和鏈接法的差別是,它的slot裏面沒有存放指針,如果hash到一個slot之後,這個槽以及被佔有,就使用探查probe方法(線性探查,二次探查,雙重探查)進行下一個slot的檢索。
至於使用怎樣的一個序列來探查slot,我們將這個探查序列表示爲<h(k,0),.....k(k,m1)><h(k,0),.....k(k,m-1)><0,1,...,m1><0,1,...,m-1>的一個序列。
我們來看看insert

hashInsert(T, k)
	i=0
	repeat
		j = h(k,i)
		if T[j]==nil
			T[i}=k
			return i
		else i++
	until i==m
	error "hash table overflow"

0到m-1有m!種組合,因此探查序列應該是從這m!個序列中選擇一個。
線性探查
h(k,i)=(h(k)+i)mod m, i=0,....,m1h(k,i)=(h^`(k)+i)mod\ m,\ i=0,....,m-1 爲什麼mod m這樣可以實現循環的探查,但是這個循環只實現一次。
二次探查
h(k,i)=(h(k)+c1i+c2i2)mod mh(k,i)=(h^`(k)+c_1 i+c_2 i^2)mod\ m不做評論,和線性探查差不多,都有m種可能,由初始位置決定探查序列
雙重探查
h(k,i)=(h1(k)+ih2(k))mod mh(k,i)=(h_1(k)+i*h_2(k))mod\ m
這兩個函數的設計有很多講究的,會產生m2m^2種序列,是這三種方法中最好的

完全hash(prefect hash)

首先要明確一點就是完全hash函數是針對靜態的key集合,我們可以通過嘗試多組hash函數來確定最終的函數完成key到slot的映射。最後實現在Θ(1)\Theta(1)的時間和O(n)\Omicron(n)的空間的hash函數。
我們有定理,隨機從全域hash函數中選擇一個h,對於m=n2m=n^2個槽下映射,發生碰撞的概率不大於1\2。我們可以試幾次,就可以得到一個不發生碰撞的函數函數。 但是對於n的平方的空間消耗還是太大了,因此就引入了二級hash的方式,在兩級hash裏面都使用全域hash函數。第一級的slot的總數爲m=n,第二級就有點講究了爲mj=nj2m_j=n_j^2。我們可以證明其期望的消耗的空間是小於2n的。所以多試幾次就好了,穩住

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