做者:小傅哥
博客:https://bugstack.cnhtml
沉澱、分享、成長,讓本身和他人都能有所收穫!😄
在面經手冊的前兩篇介紹了[《面試官都問我啥》]()和[《認知本身的技術棧盲區》](),這兩篇內容主要爲了說明面試過程的考查範圍,包括我的的自我介紹、技術棧積累、項目經驗等,以及在技術棧盲區篇章中介紹了一個整套技術棧在系統架構用的應用,以此全方面的掃描本身有哪些盲區還須要補充。而接下來的章節會以各個系列的技術棧中遇到的面試題做爲切入點,講解技術要點,瞭解技術原理,包括;數據結構、數據算法、技術棧、框架等進行逐步展開學習。java
在進入數據結構章節講解以前能夠先了解下,數據結構都有哪些,基本能夠包括;數組(Array)
、棧(Stack)
、隊列(Queue)
、鏈表(LinkList)
、樹(Tree)
、散列表(Hash)
、堆(Heap)
、圖(Graph)
。git
而本文主要講解的就是與散列表相關的HashCode
,原本想先講HashMap
,但隨着整理資料發現與HashMap
的實現中,HashCode
的散列佔了很重要的一設計思路,因此最好把這部分知識補全,再往下講解。程序員
說到HashCode的面試題,可能這是一個很是核心的了。其餘考點;怎麼實現散列、計算邏輯等,均可以經過這道題的學習瞭解相關知識。github
Why does Java's hashCode() in String use 31 as a multiplier?面試
這個問題其實☞指的就是,hashCode的計算邏輯中,爲何是31做爲乘數。算法
本文講解的過程當中涉及部分源碼等資源,能夠經過關注公衆號:bugstack蟲洞棧
,回覆下載進行獲取{回覆下載後打開得到的連接,找到編號ID:19},包括;spring
interview-03
// 獲取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做爲乘積值呢?數組
在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.
這段內容主要闡述的觀點包括;
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.
接下來要作的事情並不難,只是根據stackoverflow
的回答,統計出不一樣的乘積數對10萬個單詞的hash計算結果。10個單詞表已提供,能夠經過關注公衆號:bugstack蟲洞棧進行下載
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.[生]丘澤,高低位鑲嵌沼澤"
資源下載
進行獲取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; }
想計算碰撞很簡單,也就是計算那些出現相同哈希值的數量,計算出碰撞總量便可。這裏的實現方式有不少,可使用set
、map
也可使用java8
的stream
流統計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); }
@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)); } }
2, 3, 5, 7, 17, 31, 32, 33, 39, 41, 199
,最終返回一個list結果並輸出。測試結果
單詞數量: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
以上就是不一樣的乘數下的hash碰撞結果圖標展現,從這裏能夠看出以下信息;
除了以上看到哈希值在不一樣乘數的一個碰撞機率後,關於散列表也就是hash,還有一個很是重要的點,那就是要儘量的讓數據散列分佈。只有這樣才能減小hash碰撞次數,也就是後面章節要講到的hashMap源碼。
那麼怎麼看散列分佈呢?若是咱們能把10萬個hash值鋪到圖表上,造成的一張圖,就能夠看出整個散列分佈。可是這樣的圖會比較大,當咱們縮小看後,就成一個了大黑點。因此這裏咱們採起分段統計,把2 ^ 32方分64個格子進行存放,每一個格子都會有對應的數量的hash值,最終把這些數據展現在圖表上。
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
取值範圍內,每一個哈希值存放到不一樣格子裏的數量。@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()); }
統計圖表
文中引用