【算法】HashMap相關要點記錄

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

 

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

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 }

 

2、構造函數的選擇
      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的容量,最大能分配的容量爲;

1 static final int MAXIMUM_CAPACITY = 1 << 30;

       第三個可以用於複製指定的HashMap。由於擴容需要執行不少操作,所以肯定是會佔用一些資源的,如果平時開發比較明確需要使用多少容量,最好使用第二個,可以避免頻繁擴容影響性能。

3、元素的插入

      插入元素的方法是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)


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


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

 

6、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 }

 

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章