Hash概率问题
Hash是把鋒利的刀子,處理海量數(shù)據(jù)時(shí)經(jīng)常用到,大家可能經(jīng)常用hash,但hash的有些特點(diǎn)你是否想過、理解過。我們可以利用我們掌握的概率和期望的知識,來分析Hash中一些有趣的問題,比如:
- 平均每個(gè)桶上的項(xiàng)的個(gè)數(shù)
- 平均查找次數(shù)
- 平均沖突次數(shù)
- 平均空桶個(gè)數(shù)
- 使每個(gè)桶都至少有一個(gè)項(xiàng)的項(xiàng)個(gè)數(shù)的期望
本文hash的采用鏈地址法發(fā)處理沖突,即對hash值相同的不同對象添加到hash桶的鏈表上。
每個(gè)桶上的項(xiàng)的期望個(gè)數(shù)
將n個(gè)不同的項(xiàng)hash到大小為k的hash表中,平均每個(gè)桶會有多少個(gè)項(xiàng)?首先,對于任意一個(gè)項(xiàng)items(i)被hash到第1個(gè)桶的概率為1/k,那么將n個(gè)項(xiàng)都hash完后,第1個(gè)桶上的項(xiàng)的個(gè)數(shù)的期望為C(項(xiàng)的個(gè)數(shù))=n/k,這里我們選取了第一個(gè)桶方便敘述,事實(shí)上對于任一個(gè)特定的桶,這個(gè)期望值都是適用的。這就是每個(gè)桶平均項(xiàng)的個(gè)數(shù)。
用程序模擬的過程如下:
1 /*** 2 * 對N個(gè)字符串hash到大小為K的哈希表中,每個(gè)桶上的項(xiàng)的期望個(gè)數(shù) 3 * 4 * @return 5 */ 6 private double expectedItemNum() { 7 // 桶大小為K 8 int[] bucket = new int[K]; 9 // 生成測試字符串 10 List<String> strings = getStrings(N); 11 // hash映射 12 for (int i = 0; i < strings.size(); i++) { 13 int h = hash(strings.get(i), 37); 14 bucket[h]++; 15 } 16 // 計(jì)算每個(gè)桶的平均次數(shù) 17 long sum = 0; 18 for (int itemNum : bucket) 19 sum += itemNum; 20 return 1.0 * sum / K; 21 } 22 23 /*** 24 * 多次測試計(jì)算每個(gè)桶上的項(xiàng)的期望個(gè)數(shù), 25 */ 26 private static void expectedItemNumTest() { 27 MyHash myHash = new MyHash(); 28 // 測試100次 29 int tryNum = 100; 30 double sum = 0; 31 for (int i = 0; i < tryNum; i++) { 32 double count = myHash.expectedItemNum(); 33 sum += count; 34 } 35 // 取100次測試的平均值 36 double fact = sum / tryNum; 37 System.out.println("K=" + K + " N=" + N); 38 System.out.println("程序模擬的期望個(gè)數(shù):" + fact); 39 double expected = N * 1.0 / K; 40 System.out.println("估計(jì)的期望個(gè)數(shù) n/k:" + expected); 41 }
輸出的結(jié)果如下,可以看到我們用公式計(jì)算的期望與實(shí)際是很接近的,這也說明我們的期望公式計(jì)算正確了,畢竟實(shí)踐是檢驗(yàn)真理的唯一標(biāo)準(zhǔn)。
K=1000 N=618 程序模擬的期望個(gè)數(shù):0.6180000000000007 估計(jì)的期望個(gè)數(shù) n/k:0.618空桶的期望個(gè)數(shù)
將n個(gè)不同的項(xiàng)hash到大小為k的hash表中,平均會有多少個(gè)空桶?我們還是以第1個(gè)桶為例,任意一個(gè)項(xiàng)item(i)沒有hash到第一個(gè)桶的概率為(1-1/k),hash完n個(gè)項(xiàng)后,所有的項(xiàng)都沒有hash到第一個(gè)桶的概率為(1-1/k)^n,這也是每個(gè)桶為空的概率。桶的個(gè)數(shù)為k,因此期望的空桶個(gè)數(shù)就是C(空桶的個(gè)數(shù))=k(1-1/k)^n,這個(gè)公式不好計(jì)算,用程序跑還可能被歸零了,轉(zhuǎn)化一下就容易計(jì)算了:
C(空桶的個(gè)數(shù))=k(1?1k)n=k(1?1k)?k(?nk)=ke(?nk)(1)
同樣我們模擬測試一下:
View Code
輸出結(jié)果:
K=1000 N=618 程序模擬的期望空桶個(gè)數(shù):539.0 估計(jì)的期望空桶個(gè)數(shù)ke^(-n/k):539.021403076357沖突次數(shù)期望
我們這里的n個(gè)項(xiàng)是各不相同的,只要某個(gè)項(xiàng)hash到的桶已經(jīng)被其他項(xiàng)hash過,那就認(rèn)為是一次沖突,直接計(jì)算沖突次數(shù)不好計(jì)算,但我們知道C(沖突次數(shù))=n-C(被占用的桶的個(gè)數(shù)),而被占用的桶的個(gè)數(shù)C(被占用的桶的個(gè)數(shù))=k-C(空桶的個(gè)數(shù)),因此我們的得到:
C(沖突次數(shù))=n?(k?ke?n/k)(2)
程序模擬如下:
View Code
輸出結(jié)果:
K=1000 N=618 程序模擬的沖突數(shù):157.89 估計(jì)的期望沖突次數(shù)n-(k-ke^(-n/k)):157.02140307635705不發(fā)生沖突的概率
將n個(gè)項(xiàng)hash完后,一次沖突也沒有發(fā)生的概率,首先對第一個(gè)被hash的項(xiàng)item(1),item(1)可以hash到任意桶中,但一旦item(1)固定后,第二個(gè)項(xiàng)item(2)就只能hash到除item(1)所在位置的其他k-1個(gè)位置上了,依次類推,可以知道
P(不發(fā)生沖突的概率)=kk×k?1k×k?1k×k?2k×???×k?(n?1)k
這個(gè)概率也是不好計(jì)算,但當(dāng)k比較大、n比較小時(shí),有
P(不發(fā)生沖突的概率)=e?n(n?1)2k
模擬過程:
View Code
輸出結(jié)果如下,這個(gè)逼近公式只有在k比較大n比較小時(shí)誤差較小。
K=1000 N=50 程序模擬的不沖突概率:0.29 估計(jì)的期望不沖突概率e^(-n(n-1)/(2k)):0.29375770032353277 程序模擬的沖突概率:0.71 估計(jì)的期望沖突沖突概率1-e^(-n(n-1)/(2k)):0.7062422996764672使每個(gè)桶都至少有一個(gè)項(xiàng)的項(xiàng)個(gè)數(shù)的期望
實(shí)際使用Hash時(shí),我們一開始并不知道要hash多少個(gè)項(xiàng),如果把桶設(shè)置過大,會浪費(fèi)空間,一般都是設(shè)置一個(gè)初始大小,當(dāng)hash的項(xiàng)超過一定數(shù)量時(shí),將桶的大小擴(kuò)大一倍,并將桶內(nèi)的元素重新hash一遍。查看Java的HashMap源碼可以看到,每次調(diào)用put添加數(shù)據(jù)都會檢查大小,當(dāng)n>k*裝置因子時(shí),對hashMap進(jìn)行重建。
1 public V put(K key, V value) { 2 if(...) 3 return ...; 4 ... 5 modCount++; 6 addEntry(hash, key, value, i); 7 return null; 8 } 9 /** 10 * Adds a new entry with the specified key, value and hash code to 11 * the specified bucket. It is the responsibility of this 12 * method to resize the table if appropriate. 13 * 14 * Subclass overrides this to alter the behavior of put method. 15 */ 16 void addEntry(int hash, K key, V value, int bucketIndex) { 17 if ((size >= threshold) && (null != table[bucketIndex])) { 18 resize(2 * table.length); 19 hash = (null != key) ? hash(key) : 0; 20 bucketIndex = indexFor(hash, table.length); 21 } 22 23 createEntry(hash, key, value, bucketIndex); 24 }
現(xiàn)在我們不是直接當(dāng)n大于某一個(gè)數(shù)時(shí)對Hash表進(jìn)行重建,而是預(yù)計(jì)Hash表的每一個(gè)桶都至少有了一個(gè)項(xiàng)時(shí),才對hash表進(jìn)行重建,現(xiàn)在問當(dāng)n為多少時(shí),每個(gè)桶至少有了一個(gè)項(xiàng)。要計(jì)算這個(gè)n的期望,我們先設(shè)Xi表示從第一次占用i?1個(gè)桶到第一次占用i個(gè)桶所插入的項(xiàng)的個(gè)數(shù)。首先,很容易理解X1=1,對于X2表示從插入第一個(gè)元素后,占用兩個(gè)桶所需要的插入次數(shù),理論上它可以是任意大于1的值,我們一次接一次的插入項(xiàng),每次插入有兩種獨(dú)立的結(jié)果,一個(gè)結(jié)果是映射到的桶是第一次映射的桶;另一個(gè)是映射到的桶是新的桶,當(dāng)占用了新桶時(shí)插入了項(xiàng)的個(gè)數(shù)即為X2,又因?yàn)榇藭r(shí)映射到新桶的概率p=k?1k,因此X2的期望E(X2)=1/p=kk?1;同樣的道理,占用兩個(gè)桶后,對任意一次hash映射到新桶的概率為k?2k,因此E(X2)=kk?2。
現(xiàn)在定義隨機(jī)變量X=X1+X2+???+Xk,我們可以看出X實(shí)際上就是每個(gè)桶都填上項(xiàng)所需要插入的項(xiàng)的個(gè)數(shù)。
E(X)=∑j=1kE(Xj)
=∑j=1kkk?j+1
=k∑j=1k1k?j+1
=令i=k?j+1k∑i=1k1i
上面這個(gè)數(shù)是一個(gè)有趣的數(shù),叫做調(diào)和數(shù)(Harmonic_number),這個(gè)數(shù)(常記做Hk)沒有極限,但已經(jīng)有數(shù)學(xué)家給我們確定了它關(guān)于n的一個(gè)等價(jià)近似值:
14+lnk≤Hk≤1+lnk
因此E(X)=O(klnk),當(dāng)項(xiàng)的個(gè)數(shù)為klnk時(shí),平均每個(gè)桶至少有一個(gè)項(xiàng)。
結(jié)論總結(jié)
- 每個(gè)桶上的項(xiàng)的期望個(gè)數(shù):將n個(gè)項(xiàng)hash到大小為k的hash表中,平均每個(gè)桶的項(xiàng)的個(gè)數(shù)為nk
- 空桶的期望個(gè)數(shù):將n個(gè)項(xiàng)hash到大小為k的hash表中,平均空桶個(gè)數(shù)為ke(?nk)
- 沖突次數(shù)期望:當(dāng)我們hash某個(gè)項(xiàng)到一個(gè)桶上,而這個(gè)桶已經(jīng)有項(xiàng)了,就認(rèn)為是發(fā)生了沖突
- 不發(fā)生沖突的概率:將n個(gè)項(xiàng)hash到大小為k的hash表中,平均沖突次數(shù)為n?(k?ke?n/k)
- 調(diào)和數(shù):Hk=∑ki=11i稱為調(diào)和數(shù),∑ki=11i=Θlogk
本文主要參考自參考文獻(xiàn)[1],寫這邊博客復(fù)習(xí)了一下組合數(shù)學(xué)和概率論的知識,對hash理解得更深入了一點(diǎn),自己設(shè)計(jì)hash結(jié)構(gòu)時(shí)能對性能有所把握。另外還學(xué)會了在博客園插入公式,之前都是在MathType敲好再截圖。
總結(jié)
- 上一篇: VB6工程在Win10系统打开提示MSC
- 下一篇: FPGA实现千兆/百兆自适应以太网UDP