基于哈希函數的鍵值映射集合;
底層資料結構是數組+連結清單;
數組展現在内部的Entry類型的數組table;
連結清單展現在内部類Entry中的Entry類型屬性next,用于存放下一個節點的引用;
向HashMap中存放元素:hashMap.put(“name”,“paul”);
既然底層是數組,按我們一般數組存放元素的方法:
int[] nums = new int[10];
nums[0] = 0;
是需要指定數組下标的,那put時究竟元素放在數組的什麼位置了呢?這就引出了哈希函數;
哈希函數簡單了解為給定一個值,該函數會傳回另一個值,然後根據計算出的值再想辦法找到一個數組下标,僞代碼為:
int hash = hash(key);//根據key計算出哈希值
int index = hash % arr.length;//根據哈希值與數組長度取餘,得出數組下标(實際HashMap使用了更高效的按位與運算)
下标找到了,那就把元素放在該數組該下标位置了,但是如果多個元素key根據哈希函數和取餘算出了相同的數組下标時怎麼辦呢?
其實這就是哈希沖突,解決辦法有開放位址法、再哈希法、拉鍊法等,HashMap使用拉鍊法解決該問題。當有多個元素都計算出了
相同的下标時,就把新元素放在連結清單的頭,next指針指向原來連結清單的頭。
随着放入的元素越來越多,哈希沖突越來越嚴重,連結清單越來越長,哈希表的時間複雜度從O(1)逐漸向O(n)發展,性能越來越差,怎麼辦呢?
擴容吧。什麼時候擴容?擴容多少?如何擴容?
當容量超過門檻值時擴容,門檻值=容量負載因子,預設160.75=12;
擴容擴一倍,即擴容後容量是原來的2倍;
申請記憶體,建立一個容量是原來2倍的Entry數組,然後把原數組中元素轉移到新數組中,元素存放位置為:原下标或
(原下标+擴容大小);
最佳實踐:
擴容需要新申請記憶體,轉移元素,回收舊數組記憶體,耗時多吧,怎麼辦呢?如果能預估哈希表中的元素個數,那就在初始化哈希表時指定一個初始化容量,減少頻繁擴容申請記憶體回收舊記憶體的性能消耗。(阿裡代碼規約推薦:集合初始化時,指定集合初始值大小)
如果對讀性能要求高,那就調低負載因子,理想情況下遇到哈希沖突就擴容,使得每個數組下标隻存放一個元素,那就能一直保持O(1)的時間複雜度;
如果記憶體不太夠,那就調高負載因子,盡量不讓數組擴容;