軟件開發中,一個hash表相當于把n個key隨機放入到b個bucket中,以實現n個數據在b個單位空間的存儲。
我們發現hash表中存在一些有趣現象:
當hash表中key和bucket數量一樣時(n/b=1):
下圖直觀的展示了當n=b=20時,hash表里每個bucket中key的數量(按照key的數量對bucket做排序):
往往我們對hash表的感覺是:如果將key隨機放入所有的bucket,bucket中key的數量較為均勻,每個bucket里key數量的期望是1。
而實際上,bucket里key的分布在n較小時非常不均勻;當n增大時,才會逐漸趨于平均。
key的數量對3類bucket數量的影響
下表表示當b不變,n增大時,n/b的值如何影響3類bucket的數量占比(沖突率即含有多于1個key的bucket占比):
更直觀一點,我們用下圖來展示空bucket率和沖突率隨n/b值的變化趨勢:
key數量對bucket均勻程度的影響
上面幾組數字是當n/b較小時有意義的參考值,但隨n/b逐漸增大,空bucket與1個key的bucket數量幾乎為0,絕大多數bucket含有多個key。
當n/b超過1(1個bucket允許存儲多個key), 我們主要觀察的對象就轉變成bucket里key數量的分布規律。
下表表示當n/b較大,每個bucket里key的數量趨于均勻時,不均勻的程度是多少。
為了描述這種不均勻的程度,我們使用bucket中key數量的大值和小值之間的比例((most-fewest)/most)來表示。
下表列出了b=100時,隨n增大,key的分布情況。
可以看出,隨著bucket里key平均數量的增加,其分布的不均勻程度也逐漸降低。
和空bucket或1個key的bucket的占比不同n/b,均勻程度不僅取決于n/b的值,也受b值的影響,后面會提到。
未使用統計中常用的均方差法去描述key分布的不均勻程度,是因為軟件開發過程中,更多時候要考慮壞情況下所需準備的內存等資源。
hash表中常用一個概念 load factor α=n/b,來描述hash表的特征。
通常,基于內存存儲的hash表,它的 n/b ≤0.75。這樣設定,既可節省空間,也可以保持key的沖突率相對較低,低沖突率意味著低頻率的hash重定位,hash表的插入會更快。
線性探測是一個經常被使用的解決插入時hash沖突的算法,它在1個bucket出現沖突時,按照逐步增加的步長順序向后查看這個bucket后面的bucket,直到找到1個空bucket。因此它對hash的沖突非常敏感。
在n/b=0.75 這個場景中,如果不使用線性探測(譬如使用bucket內的鏈表來保存多個key),大約有47% 的bucket是空的;如果使用線性探測,這47%的bucket中,大約一半的bucket會被線性探測填充。
在很多內存hash表的實現中,選擇n/b=<=0.75作為hash表的容量上限,不僅是考慮到沖突率隨n/b增大而增大,更重要的是線性探測的效率會隨著n/b的增大而迅速降低。
hash表特性小貼士:
另外一種hash表的實現,專門用來存儲比較多的key,當 n/b>1n/b1.0時,線性探測失效(沒有足夠的bucket存儲每個key)。這時1個bucket里不僅存儲1個key,一般在一個bucket內用chaining,將所有落在這個bucket的key用鏈表連接起來,來解決沖突時多個key的存儲。
鏈表只在n/b不是很大時適用。因為鏈表的查找需要O(n)的時間開銷,對于非常大的n/b,有時會用tree替代鏈表來管理bucket內的key。
n/b值較大的使用場景之一是:將一個網站的用戶隨機分配到多個不同的web-server上,這時每個web-server可以服務多個用戶。多數情況下,我們都希望這種分配能盡可能均勻,從而有效利用每個web-server資源。
這就要求我們關注hash的均勻程度。因此,接下來要討論的是,假定hash函數完全隨機的,均勻程度根據n和b如何變化。
n/b 越大,key的分布越均勻
當 n/b 足夠大時,空bucket率趨近于0,且每個bucket中key的數量趨于平均。每個bucket中key數量的期望是:
avg=n/b
定義一個bucket平均key的數量是100%:bucket中key的數量剛好是n/b,下圖分別模擬了 b=20,n/b分別為 10、100、1000時,bucket中key的數量分布。
可以看出,當 n/b 增大時,bucket中key數量的大值與小值差距在逐漸縮小。下表列出了隨b和n/b增大,key分布的均勻程度的變化:
結論:
上述大部分結果來自于程序模擬,現在我們來解決從數學上如何計算這些數值。
每類bucket的數量
空bucket數量
對于1個key, 它不在某個特定的bucket的概率是 (b?1)/b
所有key都不在某個特定的bucket的概率是( (b?1)/b)n
已知:
空bucket率是:
空bucket數量為:
有1個key的bucket數量
n個key中,每個key有1/b的概率落到某個特定的bucket里,其他key以1-(1/b)的概率不落在這個bucket里,因此,對某個特定的bucket,剛好有1個key的概率是:
剛好有1個key的bucket數量為:
多個key的bucket
剩下即為含多個key的bucket數量:
類似的,1個bucket中剛好有i個key的概率是:n個key中任選i個,并都以1/b的概率落在這個bucket里,其他n-i個key都以1-1/b的概率不落在這個bucket里,即:
這就是的二項式分布。
我們可通過二項式分布估計bucket中key數量的大值與小值。
通過正態分布來近似
當 n, b 都很大時,二項式分布可以用正態分布來近似估計key分布的均勻性:
p=1/b,1個bucket中剛好有i個key的概率為:
1個bucket中key數量不多于x的概率是:
所以,所有不多于x個key的bucket數量是:
bucket中key數量的小值,可以這樣估算: 如果不多于x個key的bucket數量是1,那么這1個bucket就是少key的bucket。我們只要找到1個小的x,讓包含不多于x個key的bucket總數為1, 這個x就是bucket中key數量的小值。
一個bucket里包含不多于x個key的概率是:
Φ(x) 是正態分布的累計分布函數,當x-μ趨近于0時,可以使用以下方式來近似:
這個函數的計算較難,但只是要找到x,我們可以在[0~μ]的范圍內逆向遍歷x,以找到一個x 使得包含不多于x個key的bucket期望數量是1。
x可以認為這個x就是bucket里key數量的小值,而這個hash表中,不均勻的程度可以用key數量大值與小值的差異來描述: 因為正態分布是對稱的,所以key數量的大值可以用 μ + (μ-x) 來表示。終,bucket中key數量大值與小值的比例就是:
(μ是均值n/b)
以下python腳本模擬了key在bucket中分布的情況,同時可以作為對比,驗證上述計算結果。
import sys import math import time import hashlib def normal_pdf(x, mu, sigma): x = float(x)
mu = float(mu)
m = 1.0 / math.sqrt( 2 * math.pi ) / sigma
n = math.exp(-(x-mu)**2 / (2*sigma*sigma)) return m * n def normal_cdf(x, mu, sigma): # integral(-oo,x) x = float(x)
mu = float(mu)
sigma = float(sigma) # to standard form x = (x - mu) / sigma
s = x
v = x for i in range(1, 100):
v = v * x * x / (2*i+1)
s += v return 0.5 + s/(2*math.pi)**0.5 * math.e ** (-x*x/2) def difference(nbucket, nkey): nbucket, nkey= int(nbucket), int(nkey) # binomial distribution approximation by normal distribution # find the bucket with minimal keys. # # the probability that a bucket has exactly i keys is: # # probability density function # normal_pdf(i, mu, sigma) # # the probability that a bucket has 0 ~ i keys is: # # cumulative distribution function # normal_cdf(i, mu, sigma) # # if the probability that a bucket has 0 ~ i keys is greater than 1/nbucket, we # say there will be a bucket in hash table has: # (i_0*p_0 + i_1*p_1 + ...)/(p_0 + p_1 + ..) keys. p = 1.0 / nbucket
mu = nkey * p
sigma = math.sqrt(nkey * p * (1-p))
target = 1.0 / nbucket
minimal = mu while True:
xx = normal_cdf(minimal, mu, sigma) if abs(xx-target) < target/10: break minimal -= 1 return minimal, (mu-minimal) * 2 / (mu + (mu - minimal)) def difference_simulation(nbucket, nkey): t = str(time.time())
nbucket, nkey= int(nbucket), int(nkey)
buckets = [0] * nbucket for i in range(nkey):
hsh = hashlib.sha1(t + str(i)).digest()
buckets[hash(hsh) % nbucket] += 1 buckets.sort()
nmin, mmax = buckets[0], buckets[-1] return nmin, float(mmax - nmin) / mmax if __name__ == "__main__":
nbucket, nkey= sys.argv[1:]
minimal, rate = difference(nbucket, nkey) print 'by normal distribution:' print ' min_bucket:', minimal print ' difference:', rate
minimal, rate = difference_simulation(nbucket, nkey) print 'by simulation:' print ' min_bucket:', minimal print ' difference:', rate
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。