Hash中的一些概率計算


  Hash是把鋒利的刀子,處理海量數據時經常用到,大家可能經常用hash,但hash的有些特點你是否想過、理解過。我們可以利用我們掌握的概率和期望的知識,來分析Hash中一些有趣的問題,比如:

  • 平均每個桶上的項的個數
  • 平均查找次數
  • 平均沖突次數
  • 平均空桶個數
  • 使每個桶都至少有一個項的項個數的期望

  本文hash的采用鏈地址法發處理沖突,即對hash值相同的不同對象添加到hash桶的鏈表上。

每個桶上的項的期望個數

  將n個不同的項hash到大小為k的hash表中,平均每個桶會有多少個項?首先,對於任意一個項items(i)被hash到第1個桶的概率為1/k,那么將n個項都hash完后,第1個桶上的項的個數的期望為C(項的個數)=n/k,這里我們選取了第一個桶方便敘述,事實上對於任一個特定的桶,這個期望值都是適用的。這就是每個桶平均項的個數。

  用程序模擬的過程如下:

 1     /***  2  * 對N個字符串hash到大小為K的哈希表中,每個桶上的項的期望個數  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         // 計算每個桶的平均次數
17         long sum = 0; 18         for (int itemNum : bucket) 19             sum += itemNum; 20         return 1.0 * sum / K; 21  } 22 
23     /*** 24  * 多次測試計算每個桶上的項的期望個數, 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("程序模擬的期望個數:" + fact); 39         double expected = N * 1.0 / K; 40         System.out.println("估計的期望個數 n/k:" + expected); 41     }

   輸出的結果如下,可以看到我們用公式計算的期望與實際是很接近的,這也說明我們的期望公式計算正確了,畢竟實踐是檢驗真理的唯一標准。

K=1000 N=618
程序模擬的期望個數:0.6180000000000007
估計的期望個數 n/k:0.618

空桶的期望個數

  將n個不同的項hash到大小為k的hash表中,平均會有多少個空桶?我們還是以第1個桶為例,任意一個項item(i)沒有hash到第一個桶的概率為(1-1/k),hash完n個項后,所有的項都沒有hash到第一個桶的概率為(1-1/k)^n,這也是每個桶為空的概率。桶的個數為k,因此期望的空桶個數就是C(空桶的個數)=k(1-1/k)^n,這個公式不好計算,用程序跑還可能被歸零了,轉化一下就容易計算了:\begin{equation} C(空桶的個數)=k(1-\frac{1}{k})^n=k(1-\frac{1}{k})^{-k(-\frac{n}{k})}=ke^{(-\frac{n}{k})}\end{equation}  同樣我們模擬測試一下:

 1     /***
 2      * 計算期望的空桶個數
 3      * 
 4      * @return
 5      */
 6     private int expectedEmputyBuckts() {
 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         // 記錄空桶的個數
17         int count = 0;
18         for (int itemNum : bucket)
19             if (itemNum == 0)
20                 count++;
21         return count;
22     }
23 
24     /***
25      * 多次測試求空桶的期望個數
26      */
27     private static void expectedEmputyBucktsTest() {
28         MyHash myHash = new MyHash();
29         // 測試100次
30         int tryNum = 100;
31         long sum = 0;
32         for (int i = 0; i < tryNum; i++) {
33             int count = myHash.expectedEmputyBuckts();
34             sum += count;
35         }
36         // 取100次測試的平均值
37         double fact = sum / tryNum;
38         System.out.println("K=" + K + " N=" + N);
39         System.out.println("程序模擬的期望空桶個數:" + fact);
40         double expected = K * Math.exp(-1.0 * N / K);
41         System.out.println("估計的期望空桶個數ke^(-n/k):" + expected);
42     }
View Code

  輸出結果:

K=1000 N=618
程序模擬的期望空桶個數:539.0
估計的期望空桶個數ke^(-n/k):539.021403076357

 沖突次數期望

  我們這里的n個項是各不相同的,只要某個項hash到的桶已經被其他項hash過,那就認為是一次沖突,直接計算沖突次數不好計算,但我們知道C(沖突次數)=n-C(被占用的桶的個數),而被占用的桶的個數C(被占用的桶的個數)=k-C(空桶的個數),因此我們的得到:\begin{equation} C(沖突次數)=n-(k-ke^{-n/k}) \end{equation}  程序模擬如下:

 1     /***
 2      * 期望沖突次數
 3      * 
 4      * @return
 5      */
 6     private int expextedCollisions() {
 7         // 桶大小為K
 8         int[] bucket = new int[K];
 9         int count = 0;
10         // 生成測試字符串
11         List<String> strings = getStrings(N);
12         for (int i = 0; i < strings.size(); i++) {
13             // hash映射
14             int h = hash(strings.get(i), 37);
15             // 桶h沒有被占用
16             if (bucket[h] == 0)
17                 bucket[h] = 1;
18             // 桶h已經被占用,發生了沖突
19             else
20                 count++;
21         }
22         return count;
23     }
24 
25     private static void expextedCollisionsTest() {
26         MyHash myHash = new MyHash();
27         // 測試100次
28         int tryNum = 100;
29         long sum = 0;
30         for (int i = 0; i < tryNum; i++) {
31             int count = myHash.expextedCollisions();
32             sum += count;
33         }
34         // 取100次測試的平均值
35         double fact = sum / tryNum;
36         System.out.println("K=" + K + " N=" + N);
37         System.out.println("程序模擬的沖突數:" + fact);
38         double expected = N - (K - K * Math.exp(-1.0 * N / K));
39         System.out.println("估計的期望沖突次數n-(k-ke^(-n/k)):" + expected);
40 
41     }
View Code

  輸出結果:

K=1000 N=618
程序模擬的沖突數:157.89
估計的期望沖突次數n-(k-ke^(-n/k)):157.02140307635705

 不發生沖突的概率 

  將n個項hash完后,一次沖突也沒有發生的概率,首先對第一個被hash的項item(1),item(1)可以hash到任意桶中,但一旦item(1)固定后,第二個項item(2)就只能hash到除item(1)所在位置的其他k-1個位置上了,依次類推,可以知道$$P(不發生沖突的概率)=\frac{k}{k}\times\frac{k-1}{k}\times\frac{k-1}{k}\times\frac{k-2}{k}\times\cdot\cdot\cdot\times\frac{k-(n-1)}{k}$$ 這個概率也是不好計算,但當k比較大、n比較小時,有$$P(不發生沖突的概率)=e^{\frac{-n(n-1)}{2k}}$$  模擬過程:

 1     /***
 2      * hash N個字符串的過程是否產生沖突
 3      * 
 4      * @return
 5      */
 6     private boolean containsCollison() {
 7         // 桶大小為K
 8         int[] bucket = new int[K];
 9         // 生成測試字符串
10         List<String> strings = getStrings(N);
11         for (int i = 0; i < strings.size(); i++) {
12             // hash映射
13             int h = hash(strings.get(i), 37);
14             // 桶h沒有被占用
15             if (bucket[h] == 0)
16                 bucket[h] = 1;
17             // 桶h已經被占用,發生了沖突,直接返回
18             else
19                 return true;
20         }
21         return false;
22     }
23 
24     /***
25      * 重復調用多次containsCollison,計算不發生沖突的概率
26      */
27     private static void probCollisionTest() {
28         MyHash myHash = new MyHash();
29         // 測試100次
30         int tryNum = 100;
31         // 不沖突的次數
32         int count = 0;
33         for (int i = 0; i < tryNum; i++) {
34             if (!myHash.containsCollison())
35                 count++;
36         }
37         // 取100次測試的平均值
38         double fact = 1.0 * count / tryNum;
39         System.out.println("K=" + K + " N=" + N);
40         System.out.println("程序模擬的不沖突概率:" + fact);
41         double expected = Math.exp(-1.0 * N * (N - 1) / (2 * K));
42         System.out.println("估計的期望不沖突概率e^(-n(n-1)/(2k)):" + expected);
43         System.out.println("程序模擬的沖突概率:" + (1 - fact));
44         System.out.println("估計的期望沖突沖突概率1-e^(-n(n-1)/(2k)):" + (1 - expected));
45 
46     }
View Code

  輸出結果如下,這個逼近公式只有在k比較大n比較小時誤差較小。

K=1000 N=50
程序模擬的不沖突概率:0.29
估計的期望不沖突概率e^(-n(n-1)/(2k)):0.29375770032353277
程序模擬的沖突概率:0.71
估計的期望沖突沖突概率1-e^(-n(n-1)/(2k)):0.7062422996764672

 使每個桶都至少有一個項的項個數的期望

  實際使用Hash時,我們一開始並不知道要hash多少個項,如果把桶設置過大,會浪費空間,一般都是設置一個初始大小,當hash的項超過一定數量時,將桶的大小擴大一倍,並將桶內的元素重新hash一遍。查看Java的HashMap源碼可以看到,每次調用put添加數據都會檢查大小,當n>k*裝置因子時,對hashMap進行重建。

 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     }    

  現在我們不是直接當n大於某一個數時對Hash表進行重建,而是預計Hash表的每一個桶都至少有了一個項時,才對hash表進行重建,現在問當n為多少時,每個桶至少有了一個項。要計算這個n的期望,我們先設$X_i$表示從第一次占用$i-1$個桶到第一次占用$i$個桶所插入的項的個數。首先,很容易理解$X_1=1$,對於$X_2$表示從插入第一個元素后,占用兩個桶所需要的插入次數,理論上它可以是任意大於1的值,我們一次接一次的插入項,每次插入有兩種獨立的結果,一個結果是映射到的桶是第一次映射的桶;另一個是映射到的桶是新的桶,當占用了新桶時插入了項的個數即為$X_2$,又因為此時映射到新桶的概率$p=\frac{k-1}{k}$,因此$X_2$的期望$E(X_2)=\frac{1}/{p}=\frac{k}{k-1}$;同樣的道理,占用兩個桶后,對任意一次hash映射到新桶的概率為$\frac{k-2}{k}$,因此$E(X_2)=\frac{k}{k-2}$。

  現在定義隨機變量$X=X_1+X_2+\cdot\cdot\cdot+X_k$,我們可以看出$X$實際上就是每個桶都填上項所需要插入的項的個數。$$E(X)=\sum_{j=1}^k E(X_j)$$$$=\sum_{j=1}^k \frac{k}{k-j+1}$$$$=k\sum_{j=1}^k \frac{1}{k-j+1}$$$$\overset{\underset{\mathrm{令i=k-j+1}}{}}{=}k\sum_{i=1}^k \frac{1}{i}$$  上面這個數是一個有趣的數,叫做調和數(Harmonic_number),這個數(常記做$H_k$)沒有極限,但已經有數學家給我們確定了它關於n的一個等價近似值:$$\frac{1}{4}+\ln k\le H_k \le 1+\ln k$$  因此$E(X)=O(k\ln k)$,當項的個數為$k\ln k$時,平均每個桶至少有一個項。 

結論總結

  • 每個桶上的項的期望個數:將n個項hash到大小為k的hash表中,平均每個桶的項的個數為${\frac{n}{k}}$
  • 空桶的期望個數:將n個項hash到大小為k的hash表中,平均空桶個數為$ke^{(-\frac{n}{k})}$
  • 沖突次數期望:當我們hash某個項到一個桶上,而這個桶已經有項了,就認為是發生了沖突,將n個項hash到大小為k的hash表中,平均沖突次數為$n-(k-ke^{-n/k})$
  • 不發生沖突的概率:$$P(不發生沖突的概率)=e^{\frac{-n(n-1)}{2k}}$$
  • 調和數:$H_k=\sum_{i=1}^k \frac{1}{i}$稱為調和數,$\sum_{i=1}^k \frac{1}{i}=\Theta{logk}$

   本文主要參考自參考文獻[1],寫這邊博客復習了一下組合數學和概率論的知識,對hash理解得更深入了一點,自己設計hash結構時能對性能有所把握。另外還學會了在博客園插入公式,之前都是在MathType敲好再截圖。

  希望本文對您有所幫助,歡迎評論交流!

轉載請注明出處:http://www.cnblogs.com/fengfenggirl

參考文獻:

[1].http://www.math.dartmouth.edu/archive/m19w03/public_html/Section6-5.pdf

[2].http://preshing.com/20110504/hash-collision-probabilities/

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM