【算法】HashMap相關要點記錄

        在刷leetcode的算法題時,HashMap須要大量使用,並且也是面試的高頻問題。這裏記錄了HashMap一些增、刪、改、查的實現細節和時間複雜度,羅列了一些比較有用的方法,以及其它的一些細節。html

 

一、底層數據結構
       HashMap在jdk1.7及以前的版本中,由數組+鏈表的結構實現,從jdk1.8開始,由數組+鏈表+紅黑樹的結構實現,這裏在jdk1.8的基礎上探討HashMap。
源碼中維護了一個數組:java

1 transient Node<K,V>[] table;
2 static class Node<K,V> implements Map.Entry<K,V> {
3     final int hash;
4     final K key;
5     V value;
6     Node<K,V> next;
7 }

      這個數組存儲的Node,就包含了咱們put時的K與V,K的hash值,以及指向下一個節點的指針next。數組中查詢節點的時間複雜度是O(1),可是插入、刪除的時間複雜度是O(n),因此執行插入和刪除操做比較耗時。HashMap中加入鏈表結構來解決這個問題。咱們知道,解決hash衝突的通常方法有:開發地址法、二次hash法、拉鍊法等,這裏採用的就是拉鍊法,也就是這裏的數組+鏈表結構了。查找元素時,最好的狀況是就在數組中,時間複雜度爲O(1),最壞的狀況是在鏈表的末尾,時間複雜度是O(n)(固然,因爲HashMap的擴容機制和良好的hash算法,hash衝突發生得比較少);插入和刪除的時間複雜度就變成了O(1)了。面試

        jdk1.8加入了紅黑樹,當鏈表的長度達到8的時候就會由鏈表升維爲紅黑樹,當紅黑樹減小到6時又由紅黑樹降到鏈表。這裏須要補充一點的是,紅黑樹的節點佔用的空間比鏈表要大,維護紅黑樹的空間成本比較大,但操做方便;而鏈表正好相反,因此這裏的8和6是一個平衡的值。在鏈表轉爲紅黑樹時,還會判斷當前的Entry的數量是否小於64,小於64時會擴容,減小hash衝突,生成紅黑樹的可能性就小了不少。可見,只有當數量比較多時,維護紅黑樹的效率才比較明顯。算法

       紅黑樹的節點以下,實際上也Node的子類:數組

1 static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<K,V> {
2      TreeNode<K,V> parent; // red-black tree links
3      TreeNode<K,V> left;
4      TreeNode<K,V> right;
5      TreeNode<K,V> prev; // needed to unlink next upon deletion
6      boolean red;
7 }

 

二、構造函數的選擇
      HashMap提供了4個構造函數,實際工做中可能會用到下面3個:數據結構

 1 public HashMap() {
 2      this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
 3 }
 4 public HashMap(int initialCapacity) {
 5      this(initialCapacity, DEFAULT_LOAD_FACTOR);
 6 }
 7 public HashMap(Map<? extends K, ? extends V> m) {
 8      this.loadFactor = DEFAULT_LOAD_FACTOR;
 9      putMapEntries(m, false);
10 }

這三個構造函數都使用了默認的擴容因子,函數

static final float DEFAULT_LOAD_FACTOR = 0.75f;

其值爲0.75,當HashMap當前使用率達到整個容量(capacity)的75%時就會擴容。第一個構造函數使用得最頻繁,會分配默認大小的容量:性能

1 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

       第二個構造函數會指定初始容量,指定容量後經過計算,會分配比該初始值大的最近的2的n次方大小的容量,好比傳入的initialCapacity爲12,實際上會分配16的容量,最大能分配的容量爲;this

1 static final int MAXIMUM_CAPACITY = 1 << 30;

       第三個能夠用於複製指定的HashMap。因爲擴容須要執行很多操做,因此確定是會佔用一些資源的,若是平時開發比較明確須要使用多少容量,最好使用第二個,能夠避免頻繁擴容影響性能。spa

三、元素的插入

      插入元素的方法是put(K,V),其基本步驟是:

  (1)根據Key算出hash值,(n-1)&hash來肯定其在數組中的index(這裏的n表示數組的長度)

  (2)若是數組的這個index位置爲空,則直接插入,時間複雜度是O(1),若是達到擴容條件還會擴容。

  (3)若是數組的這個index已經有值了,那就依次遍歷,比價Key來判斷是否已經存在,存在就修改該節點的Value,不存在就新建節點並插在鏈尾。
若是鏈表長度達到了8,此時會升維造成紅黑樹。若是還在鏈表階段,時間複雜度是O(1)+O(k),這裏O(1)是插入,O(k)是遍歷,因爲不會超過8,因此也能夠認爲是O(1)。在造成紅黑樹時,還會判斷容量是否小於64,若是是,會擴容。

  (4)在第3步中,可能插入前已是紅黑樹了,那就在紅黑樹中先查找是否存在,存在則修改,不存在則新建並插入。這樣,時間複雜度是O(l)+O(logK)。因此綜合來看,能夠理解爲插入一個元素時時間複雜度最好是O(1),最壞是O(logn)


四、獲取元素
     獲取元素的方法是get(K),基本步驟是:
  (1)根據Key的hash值肯定其在數組中的index。
  (2)先判斷數組的這個地方是否有節點,沒有則返回null。
  (3)若是有,則根據hash和Key判斷第一個節點是否爲目標節點,是則返回其Value。不然繼續判斷,根據第一個節點是TreeNode實例來判斷當前是鏈表仍是紅黑樹。 一樣根據hash值和Key來肯定是否存在,存在則返回Value,不然返回null。因此時間複雜度也和插入時相似,最好時是O(1),最壞時是O(logn)。


五、刪除元素
       刪除元素的方法是remove(K),先和獲取元素同樣查找該節點,刪除,而後調整結構。

 

六、Key爲null時的處理
      HashMap的K和V都可覺得null,當Key爲null時有,其hash值定爲0;

1 public V put(K key, V value) {
2      return putVal(hash(key), key, value, false, true);
3 }
4 static final int hash(Object key) {
5      int h;
6      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
7 }

 

七、作算法題時經常使用的方法

 1 Map<Object, Object> map = new HashMap<>();
 2 map.put(K,V); //存取KV對
 3 map.get(K); //若是不存在,則返回null
 4 map.getOrDefault(K,defaultValue); //相比get方法,會獲得設定的默認值defaultValue。該方法頗有用
 5 map.entrySet(); //獲取全部KV對的實體Set,其元素類型爲Map.Entry<K, V>。HashMap中的Node,TreeNode都是其子類。
 6 map.keySet(); //獲取Key的集合Set
 7 map.values(); //獲取value的集合Collection,區別於Set
 8 map.containsKey(K); //判斷是否包含指定Key的Entry
 9 map.containsValue(V); //判斷是否包含指定Value的Entry
10 map.remove(K); //刪除指定Key的Entry
11 map.putAll(otherMap); //複製給定的map
12 map.size(); //Entry的數量
13 map.clear(); //清除全部Entry
14 map.isEmpty(); //判斷是否爲空

相關閱讀

https://tech.meituan.com/2016/06/24/java-hashmap.html

相關文章
相關標籤/搜索