以前有過一篇介紹java中hashmap使用的,深刻理解hashmap,比較側重於 代碼分析,沒有從理論上分析hashmap,今天把hashmap的理論部分補充一下(以後應該還有兩篇補充 一篇講紅黑樹一篇講多線程)。java
簡單來講 散列函數主要就是:將一個二進制串 經過必定的算法計算之後 獲得一個新的二進制串。這個計算的方法就是散列函數。 也叫哈希函數,獲得的值就是哈希值android
那麼要設計一個散列函數還須要幾個特性: 1.經過哈希值不能獲得原始的值。 這個不少人都清楚,比方說咱們的密碼都是md5之後存在服務器的,不然數據庫被盜, 你們的密碼就都完蛋了,這個md5 其實就是一種哈希算法。git
2.對於原始值來講,由於計算機中的任何對象,都是一串二進制值,因此要求 哪怕是有一個bit的不一樣,得出來的哈希值也 應該不一樣。算法
3.知足上面2個條件之後,最好散列衝突的機率要小,而且這個算法的速度要快。數據庫
那麼哈希表和哈希函數的關係就顯而易見:利用數組這種結構隨機訪問數據的時間複雜度爲o(1)的優勢,咱們將數據 通過哈希算法計算之後獲得一個key值,這個key值就對應的數組的位置。 這樣之後咱們查找數據 只要把數據計算出來 key值就能夠獲得想要數組的位置,天然查找的效率就是o(1)了。數組
因此哈希表其主要目的其實就是爲了解決快速查找的問題。其應用場景也主要圍繞這個功能展開。這裏簡單舉個例子:緩存
1.負載均衡bash
最簡單的負載均衡咱們能夠想到,無非就是創建一張表,表裏面 對應着 客戶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.
對於數組擴容來講,其實沒啥好說的,你們都會,可是哈希表的擴容還涉及到從新計算哈希值,這樣數據在擴容 之後的哈希表裏的位置 和以前的位置 就有可能不一樣。這個步驟叫作從新計算哈希值。
因此動態擴容是一個比較耗時的操做:從新申請新的數組空間,從新申請計算哈希值(也就是得出在數組中的位置),最後 把老數組的數據拷貝到新數組(解決哈希衝突的鏈表裏的值也可能要搬遷到新數組裏面)
廢話,固然不是。哈希表的基礎存儲必定是用數組,不然沒法實現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;
}
複製代碼