之前有過一篇介紹java中hashmap使用的,深入理解hashmap,比較側重於 程式碼分析,沒有從理論上分析hashmap,今天把hashmap的理論部分補充一下(之後應該還有兩篇補充 一篇講紅黑樹一篇講多執行緒)。
雜湊(雜湊)函式到底是幹嘛的?和雜湊表是啥關係?其主要作用和應用場景到底在哪裡?
簡單來說 雜湊函式主要就是:將一個二進位制串 通過一定的演算法計算以後 得到一個新的二進位制串。這個計算的方法就是雜湊函式。 也叫雜湊函式,得到的值就是雜湊值
那麼要設計一個雜湊函式還需要幾個特性: 1.通過雜湊值不能得到原始的值。 這個很多人都清楚,比方說我們的密碼都是md5以後存在伺服器的,否則資料庫被盜, 大家的密碼就都完蛋了,這個md5 其實就是一種雜湊演算法。
2.對於原始值來說,因為計算機中的任何物件,都是一串二進位制值,所以要求 哪怕是有一個bit的不同,得出來的雜湊值也 應該不同。
3.滿足上面2個條件以後,最好雜湊衝突的概率要小,並且這個演算法的速度要快。
那麼雜湊表和雜湊函式的關係就顯而易見:利用陣列這種結構隨機訪問資料的時間複雜度為o(1)的優點,我們將資料 經過雜湊演算法計算以後得到一個key值,這個key值就對應的陣列的位置。 這樣以後我們查詢資料 只要把資料計算出來 key值就可以得到想要陣列的位置,自然查詢的效率就是o(1)了。
所以雜湊表其主要目的其實就是為了解決快速查詢的問題。其應用場景也主要圍繞這個功能展開。這裡簡單舉個例子:
1.負載均衡
最簡單的負載均衡我們可以想到,無非就是建立一張表,表裡面 對應著 客戶ip地址 和伺服器ip地址。那這樣每次有客戶端請求 進來,我們都去這個表裡面查到對應應該分配的伺服器ip,然後再把客戶請求發到這個伺服器ip上。那麼很明顯這樣做 非常不好,第一這個表會無限大,消耗儲存空間,第二 表大的時候查詢效率也會變低,第三 伺服器擴容以後處理起來很麻煩。
那這裡如果用雜湊函式來做就簡單多了,我們只要把客戶ip地址 經過雜湊演算法以後 得出一個值,然後對伺服器的個數取模 就可以很快的建立這個 key-value關係。
更多的例子比如網路協議裡面的crc校驗,p2p的下載演算法,甚至git中的commit id都是利用雜湊函式來做。
雜湊函式的碰撞衝突是怎麼回事,一定發生嗎?
簡單來說,雜湊函式不管設計的有多優秀,雜湊衝突都一定無法避免。因為我們容量是有限的。大家可以百度下抽屜原理, 舉個例子,我們有5個橘子,你只有4個抽屜,那你必定會有一個抽屜裡面有2個橘子。
對於雜湊演算法也是一樣,因為我們雜湊演算法的出來的值是固定長度,所以肯定數量是有限的,比如說md5出來的值 就是128個bit。固定長度。如果你有超過這個長度的資料要經過md5演算法計算雜湊值,那麼肯定至少會有重複的!
雜湊函式的碰撞衝突如何解決?
主要有兩種方法,一種是開放定址法(java中的ThreadLocalMap),一種是連結串列法(hashmap)。其中前者現在用的不多,有興趣的同學可以學學看。 我們重點講連結串列法。所謂連結串列法其實就是 在發生雜湊衝突的時候,把相同雜湊值的資料存放在連結串列中。
連結串列大家都知道的,查詢複雜度就是o(n)了,所以可想而知,如果你臉不好雜湊衝突的次數過多,那我們o(1)的 雜湊表的查詢效率就會下降到o(n),jdk新版本優化的hashmap就是優化了這個問題,當這個解決衝突的連結串列長度 大於8的時候,就會自動轉成紅黑樹(二叉搜尋樹的一種),紅黑樹的查詢效率是o(logn),大家之前看二分查詢的 時候應該知道這個效率是很高的。查詢大概42億的資料也不過就32次左右。(紅黑樹後面我們再單獨講)
裝載因子和動態擴容的關係是什麼?如何理解
一般而言,裝載因子這個值越大,那麼就意味著 對於一個雜湊表來說,如果元素過多的情況下,裝載因子大的雜湊表 空閒位置就越少,那麼雜湊衝突的概率就越大。對於大部分採用連結串列法來解決雜湊衝突的 雜湊表來說,雜湊衝突概率大 那麼 就會導致 連結串列過長,這樣查詢的效率就會無限變低。
所以當我們發現裝載因子已經過大的時候,我們就可以擴容這個雜湊表,比如java裡的hashmap擴容就是擴容一倍的大小, 比方說陣列長度一開始16,擴容以後就變成32.
對於陣列擴容來說,其實沒啥好說的,大家都會,但是雜湊表的擴容還涉及到重新計算雜湊值,這樣資料在擴容 以後的雜湊表裡的位置 和之前的位置 就有可能不同。這個步驟叫做重新計算雜湊值。
所以動態擴容是一個比較耗時的操作:重新申請新的陣列空間,重新申請計算雜湊值(也就是得出在陣列中的位置),最後 把老陣列的資料拷貝到新陣列(解決雜湊衝突的連結串列裡的值也可能要搬遷到新陣列裡面)
java中的LinkedHashMap是用連結串列實現的雜湊表嗎,不然為啥帶個Linked的關鍵字?
廢話,當然不是。雜湊表的基礎儲存一定是用陣列,否則無法實現o(1)的查詢效率。但是LinkedHashMap和普通hashmap最大的區別就是LinkedHashMap除了維護了一個陣列以外,還維護了一個額外的雙向連結串列。熟悉android的人都知道,很多開源的圖片快取框架裡面的LRU演算法都是用的LinkedHashMap來做資料結構,比方說對於一個圖片快取框架來說,當快取到達MAX的時候,就需要把 最近最少使用的圖片移出快取。然後把新來的放進快取中,這個過程就是一個簡單的LRU演算法,而用LinkedHashMap則可以輕鬆的 完成這個需求(LinkedHashMap具體怎麼呼叫就不說了,這裡只說實現的原理以及和hashmap有什麼不同)
簡單來說,HashMap的 結構如下:
基礎儲存用陣列,如果有同樣的雜湊值的資料那麼就用單連結串列串起來。所以hashmap的儲存基本結構就是四個欄位
hash值---------->key------>value------->next
其中next指標就是用來 如果出現重複hash值雜湊衝突的情況,用於構造單連結串列的。
而LinkedHashMap,為了實現LRU,還額外實現了一套雙連結串列來保證。也就是說:
LinkedHashMap的基礎儲存也是用陣列,只不過,除了用陣列,他還單獨維護了一個雙向連結串列,這個雙向連結串列就把 整個 (陣列+單連結串列是java中雜湊表的基礎實現)給串起來,而實現LRU的資料結構就是 雙向連結串列。
所以大家可以猜到LinkedHashMap的儲存基本結構是
雙連結串列中的before指標-->hash值---------->key------>value------->next---->雙連結串列中的after指標。
雜湊表還有什麼妙用?
額,生產環境上其實有很多地方都在用hashmap,大家可以自行搜尋一下,這裡僅奉送一個簡單的leetcode演算法題。
兩數求和問題:
給定一個整數陣列和一個目標值,找出陣列中和為目標值的兩個數。
你可以假設每個輸入只對應一種答案,且同樣的元素不能被重複利用。
示例:
給定 nums = [2, 7, 11, 15], target = 9
因為 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]
正常情況我們想的是 雙迴圈暴力遍歷來解決,複雜度很容易就O(n2),其實用hashmap 可以很方便的解決 兩數,三數,甚至是四數求和問題。 對於兩數求和問題來說,用map的複雜度就僅僅只有o(n)了。
既然想速度快一點只遍歷一次,那麼其實 既然已經確定了target的值,那麼遍歷一次,我們只要找一下 是否有target-陣列[i]的值即可。
/**
* 演算法核心思想:new一個map,map的key是陣列元素的值,value是陣列元素的位置也就是俗稱的index
* 然後我們遍歷陣列的時候 用target的值 --當前陣列的值(wanted的值) 就是我們想要的值。如果在map裡面找到了,
* 那麼就直接返回當前陣列的index和 map裡面這個wanted的值的value(value就是陣列的index)即可。
*
* 如果map裡找不到這個wanted的值,那麼就把當前這個陣列的元素放到map裡面即可
* @return
*/
public int[] twoSum(int[] nums, int target) {
Map map = new HashMap<Integer, Integer>();
for (int i = 0; i < nums.length; i++) {
int wanted = target - nums[i];
if (map.containsKey(wanted)) {
return new int[]{i, (int) map.get(wanted)};
}
map.put(nums[i], i);
}
return null;
}
複製程式碼