面經手冊 · 第2篇《數據結構,HashCode爲何使用31做爲乘數?》


做者:小傅哥
博客:https://bugstack.cnhtml

沉澱、分享、成長,讓本身和他人都能有所收穫!😄

1、前言

在面經手冊的前兩篇介紹了[《面試官都問我啥》]()和[《認知本身的技術棧盲區》](),這兩篇內容主要爲了說明面試過程的考查範圍,包括我的的自我介紹、技術棧積累、項目經驗等,以及在技術棧盲區篇章中介紹了一個整套技術棧在系統架構用的應用,以此全方面的掃描本身有哪些盲區還須要補充。而接下來的章節會以各個系列的技術棧中遇到的面試題做爲切入點,講解技術要點,瞭解技術原理,包括;數據結構、數據算法、技術棧、框架等進行逐步展開學習。java

在進入數據結構章節講解以前能夠先了解下,數據結構都有哪些,基本能夠包括;數組(Array)棧(Stack)隊列(Queue)鏈表(LinkList)樹(Tree)散列表(Hash)堆(Heap)圖(Graph)git

而本文主要講解的就是與散列表相關的HashCode,原本想先講HashMap,但隨着整理資料發現與HashMap的實現中,HashCode的散列佔了很重要的一設計思路,因此最好把這部分知識補全,再往下講解。程序員

2、面試題

說到HashCode的面試題,可能這是一個很是核心的了。其餘考點;怎麼實現散列、計算邏輯等,均可以經過這道題的學習瞭解相關知識。github

Why does Java's hashCode() in String use 31 as a multiplier?面試

這個問題其實☞指的就是,hashCode的計算邏輯中,爲何是31做爲乘數。算法

3、資源下載

本文講解的過程當中涉及部分源碼等資源,能夠經過關注公衆號:bugstack蟲洞棧,回覆下載進行獲取{回覆下載後打開得到的連接,找到編號ID:19},包括;spring

  1. HashCode 源碼測試驗證工程,interview-03
  2. 103976個英語單詞庫.txt,驗證HashCode值
  3. HashCode散列分佈.xlsx,散列和碰撞圖表

4、源碼講解

1. 固定乘積31在這用到了

// 獲取hashCode "abc".hashCode();
public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

在獲取hashCode的源碼中能夠看到,有一個固定值31,在for循環每次執行時進行乘積計算,循環後的公式以下;
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]編程

那麼這裏爲何選擇31做爲乘積值呢?數組

2. 來自stackoverflow的回答

stackoverflow關於爲何選擇31做爲固定乘積值,有一篇討論文章,Why does Java's hashCode() in String use 31 as a multiplier? 這是一個時間比較久的問題了,摘取兩個回答點贊最多的;

413個贊👍的回答

最多的這個回答是來自《Effective Java》的內容;

The value 31 was chosen because it is an odd prime. If it were even and the multiplication overflowed, information would be lost, as multiplication by 2 is equivalent to shifting. The advantage of using a prime is less clear, but it is traditional. A nice property of 31 is that the multiplication can be replaced by a shift and a subtraction for better performance: 31 * i == (i << 5) - i. Modern VMs do this sort of optimization automatically.

這段內容主要闡述的觀點包括;

  1. 31 是一個奇質數,若是選擇偶數會致使乘積運算時數據溢出。
  2. 另外在二進制中,2個5次方是32,那麼也就是 31 * i == (i << 5) - i。這主要是說乘積運算可使用位移提高性能,同時目前的JVM虛擬機也會自動支持此類的優化。

80個贊👍的回答

As Goodrich and Tamassia point out, If you take over 50,000 English words (formed as the union of the word lists provided in two variants of Unix), using the constants 31, 33, 37, 39, and 41 will produce less than 7 collisions in each case. Knowing this, it should come as no surprise that many Java implementations choose one of these constants.
  • 這個回答就頗有實戰意義了,告訴你用超過5千個單詞計算hashCode,這個hashCode的運算使用3一、3三、3七、39和41做爲乘積,獲得的碰撞結果,31被使用就很正常了。
  • 他這句話就就能夠做爲咱們實踐的指向了。

3. Hash值碰撞機率統計

接下來要作的事情並不難,只是根據stackoverflow的回答,統計出不一樣的乘積數對10萬個單詞的hash計算結果。10個單詞表已提供,能夠經過關注公衆號:bugstack蟲洞棧進行下載

3.1 讀取單詞字典表

1    a    "n.(A)As 或 A's  安(ampere(a) art.一;n.字母A /[軍] Analog.Digital,模擬/數字 /(=account of) 賬上"
2    aaal    American Academy of Arts and Letters 美國藝術和文學學會
3    aachen     亞琛[德意志聯邦共和國西部城市]
4    aacs    Airways and Air Communications Service (美國)航路與航空通信聯絡處
5    aah    " [軍]Armored Artillery Howitzer,裝甲榴彈炮;[軍]Advanced Attack Helicopter,先進攻擊直升機"
6    aal    "ATM Adaptation Layer,ATM適應層"
7    aapamoor    "n.[生]丘澤,高低位鑲嵌沼澤"
  • 單詞表的文件格式如上,能夠自行解析
  • 讀取文件的代碼比較簡單,這裏不展現了,能夠經過資源下載進行獲取

3.2 Hash計算函數

public static Integer hashCode(String str, Integer multiplier) {
    int hash = 0;
    for (int i = 0; i < str.length(); i++) {
        hash = multiplier * hash + str.charAt(i);
    }
    return hash;
}
  • 這個過程比較簡單,與原hash函數對比只是替換了可變參數,用於咱們統計不一樣乘積數的計算結果。

3.3 Hash碰撞機率計算

想計算碰撞很簡單,也就是計算那些出現相同哈希值的數量,計算出碰撞總量便可。這裏的實現方式有不少,可使用setmap也可使用java8stream流統計distinct

private static RateInfo hashCollisionRate(Integer multiplier, List<Integer> hashCodeList) {
    int maxHash = hashCodeList.stream().max(Integer::compareTo).get();
    int minHash = hashCodeList.stream().min(Integer::compareTo).get();
    int collisionCount = (int) (hashCodeList.size() - hashCodeList.stream().distinct().count());
    double collisionRate = (collisionCount * 1.0) / hashCodeList.size();
    return new RateInfo(maxHash, minHash, multiplier, collisionCount, collisionRate);
}
  • 這裏記錄了最大hash和最小hash值,以及最終返回碰撞數量的統計結果。

3.4 單元測試

@Before
public void before() {
    "abc".hashCode();
    // 讀取文件,103976個英語單詞庫.txt
    words = FileUtil.readWordList("E:/itstack/git/github.com/interview/interview-01/103976個英語單詞庫.txt");
}

@Test
public void test_collisionRate() {
    List<RateInfo> rateInfoList = HashCode.collisionRateList(words, 2, 3, 5, 7, 17, 31, 32, 33, 39, 41, 199);
    for (RateInfo rate : rateInfoList) {
        System.out.println(String.format("乘數 = %4d, 最小Hash = %11d, 最大Hash = %10d, 碰撞數量 =%6d, 碰撞機率 = %.4f%%", rate.getMultiplier(), rate.getMinHash(), rate.getMaxHash(), rate.getCollisionCount(), rate.getCollisionRate() * 100));
    }
}
  • 以上先設定讀取英文單詞表中的10個單詞,以後作hash計算。
  • 在hash計算中把單詞表傳遞進去,同時還有乘積數;2, 3, 5, 7, 17, 31, 32, 33, 39, 41, 199,最終返回一個list結果並輸出。
  • 這裏主要驗證同一批單詞,對於不一樣乘積數會有怎麼樣的hash碰撞結果。

測試結果

單詞數量:103976
乘數 =    2, 最小Hash =          97, 最大Hash = 1842581979, 碰撞數量 = 60382, 碰撞機率 = 58.0730%
乘數 =    3, 最小Hash = -2147308825, 最大Hash = 2146995420, 碰撞數量 = 24300, 碰撞機率 = 23.3708%
乘數 =    5, 最小Hash = -2147091606, 最大Hash = 2147227581, 碰撞數量 =  7994, 碰撞機率 = 7.6883%
乘數 =    7, 最小Hash = -2147431389, 最大Hash = 2147226363, 碰撞數量 =  3826, 碰撞機率 = 3.6797%
乘數 =   17, 最小Hash = -2147238638, 最大Hash = 2147101452, 碰撞數量 =   576, 碰撞機率 = 0.5540%
乘數 =   31, 最小Hash = -2147461248, 最大Hash = 2147444544, 碰撞數量 =     2, 碰撞機率 = 0.0019%
乘數 =   32, 最小Hash = -2007883634, 最大Hash = 2074238226, 碰撞數量 = 34947, 碰撞機率 = 33.6106%
乘數 =   33, 最小Hash = -2147469046, 最大Hash = 2147378587, 碰撞數量 =     1, 碰撞機率 = 0.0010%
乘數 =   39, 最小Hash = -2147463635, 最大Hash = 2147443239, 碰撞數量 =     0, 碰撞機率 = 0.0000%
乘數 =   41, 最小Hash = -2147423916, 最大Hash = 2147441721, 碰撞數量 =     1, 碰撞機率 = 0.0010%
乘數 =  199, 最小Hash = -2147459902, 最大Hash = 2147480320, 碰撞數量 =     0, 碰撞機率 = 0.0000%

Process finished with exit code 0

公衆號:bugstack蟲洞棧,hash碰撞圖表

以上就是不一樣的乘數下的hash碰撞結果圖標展現,從這裏能夠看出以下信息;

  1. 乘數是2時,hash的取值範圍比較小,基本是堆積到一個範圍內了,後面內容會看到這塊的展現。
  2. 乘數是三、五、七、17等,都有較大的碰撞機率
  3. 乘數是31的時候,碰撞的機率已經很小了,基本穩定。
  4. 順着往下看,你會發現199的碰撞機率更小,這就至關於一排奇數的茅坑量多,天然會減小碰撞。但這個範圍值已經遠超過int的取值範圍了,若是用此數做爲乘數,又返回int值,就會丟失數據信息

4. Hash值散列分佈

除了以上看到哈希值在不一樣乘數的一個碰撞機率後,關於散列表也就是hash,還有一個很是重要的點,那就是要儘量的讓數據散列分佈。只有這樣才能減小hash碰撞次數,也就是後面章節要講到的hashMap源碼。

那麼怎麼看散列分佈呢?若是咱們能把10萬個hash值鋪到圖表上,造成的一張圖,就能夠看出整個散列分佈。可是這樣的圖會比較大,當咱們縮小看後,就成一個了大黑點。因此這裏咱們採起分段統計,把2 ^ 32方分64個格子進行存放,每一個格子都會有對應的數量的hash值,最終把這些數據展現在圖表上。

4.1 哈希值分段存放

public static Map<Integer, Integer> hashArea(List<Integer> hashCodeList) {
    Map<Integer, Integer> statistics = new LinkedHashMap<>();
    int start = 0;
    for (long i = 0x80000000; i <= 0x7fffffff; i += 67108864) {
        long min = i;
        long max = min + 67108864;
        // 篩選出每一個格子裏的哈希值數量,java8流統計;https://bugstack.cn/itstack-demo-any/2019/12/10/%E6%9C%89%E7%82%B9%E5%B9%B2%E8%B4%A7-Jdk1.8%E6%96%B0%E7%89%B9%E6%80%A7%E5%AE%9E%E6%88%98%E7%AF%87(41%E4%B8%AA%E6%A1%88%E4%BE%8B).html
        int num = (int) hashCodeList.parallelStream().filter(x -> x >= min && x < max).count();
        statistics.put(start++, num);
    }
    return statistics;
  • 這個過程主要統計int取值範圍內,每一個哈希值存放到不一樣格子裏的數量。
  • 這裏也是使用了java8的新特性語法,統計起來仍是比較方便的。

4.2 單元測試

@Test
public void test_hashArea() {
    System.out.println(HashCode.hashArea(words, 2).values());
    System.out.println(HashCode.hashArea(words, 7).values());
    System.out.println(HashCode.hashArea(words, 31).values());
    System.out.println(HashCode.hashArea(words, 32).values());
    System.out.println(HashCode.hashArea(words, 199).values());
}
  • 這裏列出咱們要統計的乘數值,每個乘數下都會有對應的哈希值數量彙總,也就是64個格子裏的數量。
  • 最終把這些統計值放入到excel中進行圖表化展現。

統計圖表

公衆號:bugstack蟲洞棧,hash散列表

  • 以上是一個堆積百分比統計圖,能夠看到下方是不一樣乘數下的,每一個格子裏的數據統計。
  • 除了199不能用之外,31的散列結果相對來講比較均勻。
4.2.1 乘數2散列

  • 乘數是2的時候,散列的結果基本都堆積在中間,沒有很好的散列。
4.2.2 乘數31散列

  • 乘數是31的時候,散列的效果就很是明顯了,基本在每一個範圍都有數據存放。
4.2.3 乘數199散列

  • 乘數是199是不能用的散列結果,可是它的數據是更加分散的,從圖上能看到有兩個小山包。但由於數據區間問題會有數據丟失問題,因此不能選擇。

文中引用

5、總結

  • 以上主要介紹了hashCode選擇31做爲乘數的主要緣由和實驗數據驗證,算是一個散列的數據結構的案例講解,在後續的相似技術中,就能夠解釋其餘的源碼設計思路了。
  • 看過本文至少應該讓你能夠從根本上解釋了hashCode的設計,關於他的全部問題也就不須要死記硬背了,學習編程內容除了最開始的模仿到深刻之後就須要不斷的研究數學邏輯和數據結構。
  • 文中參考了優秀的hashCode資料和stackoverflow,並親自作實驗驗證結果,你們也能夠下載本文中資源內容;英文字典、源碼、excel圖表等內容。

6、推薦閱讀

相關文章
相關標籤/搜索