1. 程式人生 > >Java 從 Map 到 HashMap 的一步步實現

Java 從 Map 到 HashMap 的一步步實現

# Java 從 Map 到 HashMap 的一步步實現 ## 一、 Map ### 1.1 Map 介面 在 Java 中, Map 提供了鍵——值的對映關係。對映不能包含重複的鍵,並且每個鍵只能對映到一個值。 以 Map 鍵——值對映為基礎,java.util 提供了 HashMap(最常用)、 TreeMap、Hashtble、LinkedHashMap 等資料結構。 衍生的幾種 Map 的主要特點: + HashMap:最常用的資料結構。鍵和值之間通過 Hash函式 來實現對映關係。當進行遍歷的 key 是無序的 + TreeMap:使用紅黑樹構建的資料結構,因為紅黑樹的原理,可以很自然的對 key 進行排序,所以 TreeMap 的 key 遍歷時是預設按照自然順序(升序)排列的。 + LinkedHashMap: 儲存了插入的順序。遍歷得到的記錄是按照插入順序的。 ### 1.2 Hash 雜湊函式 Hash (雜湊函式)是把任意長度的輸入通過雜湊演算法變換成固定長度的輸出。Hash 函式的返回值也稱為 雜湊值 雜湊碼 摘要或雜湊。Hash作用如下圖所示: ![引用自維基百科](https://img2020.cnblogs.com/blog/1600185/202012/1600185-20201223082556974-1719941306.png) Hash 函式可以通過選取適當的函式,可以在時間和空間上取得較好平衡。 解決 Hash 的兩種方式:拉鍊法和線性探測法 ### 1.3 鍵值關係的實現 ``` interface Entry ``` 在 HashMap 中基於連結串列的實現 ```java static class Node implements Map.Entry { final int hash; final K key; V value; Node next; Node(int hash, K key, V value, Node next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } ``` 用樹的方式實現: ```java static final class TreeNode extends LinkedHashMap.Entry { TreeNode parent; // red-black tree links TreeNode left; TreeNode right; TreeNode prev; // needed to unlink next upon deletion boolean red; TreeNode(int hash, K key, V val, Node next) { super(hash, key, val, next); } ``` ### 1.4 Map 約定的 API #### 1.4.1 Map 中約定的基礎 API 基礎的增刪改查: ```java int size(); // 返回大小 boolean isEmpty(); // 是否為空 boolean containsKey(Object key); // 是否包含某個鍵 boolean containsValue(Object value); // 是否包含某個值 V get(Object key); // 獲取某個鍵對應的值 V put(K key, V value); // 存入的資料 V remove(Object key); // 移除某個鍵 void putAll(Map m); //將將另一個集插入該集合中 void clear(); // 清除 Set keySet(); //獲取 Map的所有的鍵返回為 Set集合 Collection values(); //將所有的值返回為 Collection 集合 Set> entrySet(); // 將鍵值對對映為 Map.Entry,內部類 Entry 實現了對映關係的實現。並且返回所有鍵值對映為 Set 集合。 boolean equals(Object o); int hashCode(); // 返回 Hash 值 default boolean replace(K key, V oldValue, V newValue); // 替代操作 default V replace(K key, V value); ``` #### 1.4.2 Map 約定的較為高階的 API ```java default V getOrDefault(Object key, V defaultValue); //當獲取失敗時,用 defaultValue 替代。 default void forEach(BiConsumer action) // 可用 lambda 表示式進行更快捷的遍歷 default void replaceAll(BiFunction function); default V putIfAbsent(K key, V value); default V computeIfAbsent(K key, Function mappingFunction); default V computeIfPresent(K key, BiFunction remappingFunction); default V compute(K key, BiFunction remappingFunction) default V merge(K key, V value, BiFunction remappingFunction) ``` #### 1.4.3 Map 高階 API 的使用 + getOrDefault() 當這個通過 key獲取值,對應的 key 或者值不存在時返回預設值,避免在使用過程中 null 出現,避免程式異常。 + ForEach() 傳入 BiConsumer 函式式介面,表達的含義其實和 Consumer 一樣,都 accept 擁有方法,只是 BiConsumer 多了一個 andThen() 方法,接收一個BiConsumer介面,先執行本介面的,再執行傳入的引數的 accept 方法。 ```java Map map = new HashMap<>(); map.put("a", "1"); map.put("b", "2"); map.put("c", "3"); map.put("d", "4"); map.forEach((k, v) -> { System.out.println(k+"-"+v); }); } ``` 更多的函式用法: [菜鳥教程-Java HashMap](runoob.com/java/java-hashmap.html) ### 1.5 從 Map 走向 HashMap HashMap 是 Map的一個實現類,也是 Map 最常用的實現類。 #### 1.5.1 HashMap 的繼承關係 ```Java public class HashMap extends AbstractMap implements Map, Cloneable, Serializable ``` 在 HashMap 的實現過程中,解決 Hash衝突的方法是拉鍊法。因此從原理來說 HashMap 的實現就是 陣列 + 連結串列(陣列儲存連結串列的入口)。 當連結串列過長,為了優化查詢速率,HashMap 將連結串列轉化為紅黑樹(陣列儲存樹的根節點),使得查詢速率為 log(n),而不是連結串列的 O(n)。 ## 二、HashMap ```java /* * @author Doug Lea * @author Josh Bloch * @author Arthur van Hoff * @author Neal Gafter * @see Object#hashCode() * @see Collection * @see Map * @see TreeMap * @see Hashtable * @since 1.2 */ ``` 首先 HashMap 由 Doug Lea 和 Josh Bloch 兩位大師的參與。同時 Java 的 Collections 集合體系,併發框架 Doug Lea 也做出了不少貢獻。 ### 2.1 基本原理 對於一個插入操作,首先將鍵通過 Hash 函式轉化為陣列的下標。 若該陣列為空,直接建立節點放入陣列中。若該陣列下標存在節點,即 Hash 衝突,使用拉鍊法,生成一個連結串列插入。 ![](https://img-blog.csdnimg.cn/20181102221702492.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dvc2hpbWF4aWFvMQ==,size_16,color_FFFFFF,t_70) > 引用圖片來自 [Java集合之一—HashMap](https://blog.csdn.net/woshimaxiao1/article/details/83661464) 如果存在 Hash 衝突,使用拉鍊法插入,我們可以在這個連結串列的頭部插入,也可以在連結串列的尾部插入,所以在 JDK 1.7 中使用了頭部插入的方法,JDK 1.8 後續的版本中使用尾插法。JDK 1.7 使用頭部插入的可能依據是最近插入的資料是最常用的,但是頭插法帶來的問題之一,在多執行緒會連結串列的複製會出現死迴圈。所以 JDK 1.8 之後採用的尾部插入的方法。 在 HashMap 中,前面說到的 陣列+連結串列 的陣列的定義 ```java transient Node[] table; ``` 連結串列的定義: ```java static class Node implements Map.Entry ``` table 來表示上述的資料。 #### 2.1.2 提供的建構函式 ```java public HashMap() { // 空參 this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } public HashMap(int initialCapacity) { //帶有初始大小的,一般情況下,我們需要規劃好 HashMap 使用的大小,因為對於一次擴容操作,代價是非常的大的 this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap(int initialCapacity, float loadFactor); // 可以自定義負載因子 ``` 三個建構函式,都沒有完全的初始化 HashMap,當我們第一次插入資料時,才進行堆記憶體的分配,這樣提高了程式碼的響應速度。 ### 2.2 HashMap 中的 Hash函式定義 ```java static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // 將 h 高 16 位和低 16 位 進行異或操作。 } // 採用 異或的原因:兩個進行位運算,在與或異或中只有異或到的 0 和 1 的概率是相同的,而&和|都會使得結果偏向0或者1。 ``` 這裡可以看到,Map 的鍵可以為 null,且 hash 是一個特定的值 0。 Hash 的目的是獲取陣列 table 的下標。Hash 函式的目標就是將資料均勻的分佈在 table 中。 讓我們先看看如何通過 hash 值得到對應的陣列下標。第一種方法:hash%table.length()。但是除法操作在 CPU 中執行比加法、減法、乘法慢的多,效率低下。第二種方法 table[(table.length - 1) & hash] 一個與操作一個減法,仍然比除法快。 這裡的約束條件為 table.length = 2N
。 ``` java table.length =16 table.length -1 = 15 1111 1111 任何一個數與之與操作,獲取到這個數的低 8 位,其他位為 0 ``` 上面的例子可以讓我們獲取到對應的下標,而 `(h = key.hashCode()) ^ (h >>> 16)` 讓高 16 也參與運算,讓資料充分利用,一般情況下 table 的索引不會超過 216,所以高位的資訊我們就直接拋棄了,`^ (h >>> 16)` 讓我們在資料量較少的情況下,也可以使用高位的資訊。如果 table 的索引超過 216, hashCode() 的高 16 為 和 16 個 0 做異或得到的 Hash 也是公平的。 ### 2.3 HashMap 的插入操作 上面我們已經知道如果通過 Hash 獲取到 對應的 table 下標,因此我們將對應的節點加入到連結串列就完成了一個 Map 的對映,的確 JDK1.7 中的 HashMap 實現就是這樣。讓我們看一看 JDK 為實現現實的 put 操作。 定位到 put() 操作。 ```java public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } ``` 可以看到 put 操作交給了 putVal 來進行通用的實現。 ```java final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict); //onlyIfAbsent 如果當前位置已存在一個值,是否替換,false是替換,true是不替換 evict // 鉤子函式的引數,LinkedHashMap 中使用到,HashMap 中無意義。 ``` #### 2.3.1 putVal 的流程分析 其實 putVal() 流程的函式非常的明瞭。這裡挑了幾個關鍵步驟來引導。 是否第一次插入,true 呼叫 resizer() 進行調整,其實此時 resizer() 是進行完整的初始化,之後直接賦值給對應索引的位置。 ```java if ((tab = table) == null || (n = tab.length) == 0) // 第一次 put 操作, tab 沒有分配記憶體,通過 redize() 方法分配記憶體,開始工作。 n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); ``` 如果連結串列已經轉化為樹,則使用樹的插入。 ```java else if (p instanceof TreeNode) e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); ``` 用遍歷的方式遍歷每個 Node,如果遇到鍵相同,或者到達尾節點的next 指標將資料插入,記錄節點位置退出迴圈。若插入後連結串列長度為 8 則呼叫 treeifyBin() 是否進行樹的轉化 。 ```java for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } ``` 對鍵重複的操作:更新後返回舊值,同時還取決於onlyIfAbsent,普通操作中一般為 true,可以忽略。 ```java if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); //鉤子函式,進行後續其他操作,HashMap中為空,無任何操作。 return oldValue; } ``` ```java ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; ``` 後續的資料維護。 #### 2.3.2 modCount 的含義 fail-fast 機制是java集合(Collection)中的一種錯誤機制。當多個執行緒對同一個集合的內容進行操作時,就可能會產生fail-fast事件。例如:當某一個執行緒A通過iterator去遍歷某集合的過程中,若該集合的內容被其他執行緒所改變了;那麼執行緒A訪問集合時,就會丟擲ConcurrentModificationException異常,產生fail-fast事件。一種多執行緒錯誤檢查的方式,減少異常的發生。 一般情況下,多執行緒環境 我們使用 `ConcurrentHashMap` 來代替 HashMap。 ### 2.4 resize() 函式 HashMap 擴容的特點:預設的table 表的大小事 16,threshold 為 12。負載因子 loadFactor .75,這些都是可以構造是更改。以後擴容都是 2 倍的方式增加。 至於為何是0.75 程式碼的註釋中也寫了原因,對 Hash函式構建了泊松分佈模型,進行了分析。 #### 2.4.1 HashMap 預定義的一些引數 ```java static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 HashMap 的預設大小。 為什麼使用 1 <<4 static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 載入因子,擴容使用 static final int UNTREEIFY_THRESHOLD = 6;// 樹結構轉化為連結串列的閾值 static final int TREEIFY_THRESHOLD = 8; // 連結串列轉化為樹結構的閾值 static final int MIN_TREEIFY_CAPACITY = 64; // 連結串列轉變成樹之前,還會有一次判斷,只有陣列長度大於 64 才會發生轉換。這是為了避免在雜湊表建立初期,多個鍵值對恰好被放入了同一個連結串列中而導致不必要的轉化。 // 定義的有關變數 int threshold; // threshold表示當HashMap的size大於threshold時會執行resize操作 ``` 這些變數都是和 HashMap 的擴容機制有關,將會在下文中用到。 #### 2.4.2 resize() 方法解析 ```java Node[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; // 定義了 舊錶長度、舊錶閾值、新表長度、新表閾值 ``` ```java if (oldCap > 0) { // 插入過資料,引數不是初始化的 if (oldCap >= MAXIMUM_CAPACITY) { // 如果舊的表長度大於 1 << 30; threshold = Integer.MAX_VALUE; // threshold 設定 Integer 的最大值。也就是說我們可以插入 Integer.MAX_VALUE 個數據 return oldTab; // 直接返回舊錶的長度,因為表的下標索引無法擴大了。 } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && // oldCap >
= DEFAULT_INITIAL_CAPACITY) //新表的長度為舊錶的長度的 2 倍。 newThr = oldThr << 1; // double threshold 新表的閾值為同時為舊錶的兩倍 } else if (oldThr > 0) // public HashMap(int initialCapacity, float loadFactor) 中的 this.threshold = tableSizeFor(initialCapacity); 給正確的位置 newCap = oldThr; else { // zero initial threshold signifies using defaults ,如果呼叫了其他兩個建構函式,則下面程式碼初始化。因為他們都沒有對其 threshold 設定,預設為 0, newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { // 修正 threshold,例如上面的 else if (oldThr >
0) 部分就沒有設定。 float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) ``` 當一些引數設定正確後便開始擴容。 ```java Node[] newTab = (Node[])new Node[newCap]; ``` 當擴容完畢之後,自然就是將原表中的資料搬到新的表中。下面程式碼完成了該任務。 ```java if (oldTab != null) { // for (int j = 0; j < oldCap; ++j) { .... } } ``` 如何正確的,快速的擴容調整每個鍵值節點對應的下標?第一種方法:遍歷節點再使用 put() 加入一遍,這種方法實現,但是效率低下。第二種,我們手動組裝好連結串列,加入到相應的位置。顯然第二種比第一種高效,因為第一種 put() 還存在其他不屬於這種情況的判斷,例如重複鍵的判斷等。所以 JDK 1.8 也使用了第二種方法。我們可以繼續使用`e.hash & (newCap - 1)`找到對應的下標位置,對於舊的連結串列,執行`e.hash & (newCap - 1)` 操作,只能產生兩個不同的索引。一個保持原來的索引不變,另一個變為 原來索引 + oldCap(因為 newCap 的加入產生導致索引的位數多了 1 位,即就是最左邊的一個,且該位此時結果為 1,所以相當於 原來索引 + oldCap)。所以可以使用 `if ((e.hash & oldCap) == 0) ` 來確定出索引是否來變化。因此這樣我們就可以將原來的連結串列拆分為兩個新的連結串列,然後加入到對應的位置。為了高效,我們手動的組裝好連結串列再儲存到相應的下標位置上。 ``` oldCap = 16 newCap = 32 hash : 0001 1011 oldCap-1 : 0000 1111 結果為 : 0000 1011 對應的索引的 11 ------------------------- e.hash & oldCap 則定於 1,則需要進行調整索引 oldCap = 16 hash : 0001 1011 newCap-1 : 0001 1111 結果為 : 0001 1011 相當於 1011 + 1 0000 原來索引 + newCap ``` ```java for (int j = 0; j < oldCap; ++j) // 處理每個連結串列 ``` 特殊條件處理 ```java Node e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) // 該 連結串列只有一個節點,那麼直接複製到對應的位置,下標由 e.hash & (newCap - 1) 確定 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) // 若是 樹,該給樹的處理程式 ((TreeNode)e).split(this, newTab, j, oldCap); ``` 普通情況處理: ```java else { // preserve order Node loHead = null, loTail = null; // 構建原來索引位置 的連結串列,需要的指標 Node hiHead = null, hiTail = null; // 構建 原來索引 + oldCap 位置 的連結串列需要的指標 Node next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 將原來的連結串列劃分兩個連結串列 if (loTail != null) { // 將連結串列寫入到相應的位置 loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } ``` 到此 resize() 方法的邏輯完成了。總的來說 resizer() 完成了 HashMap 完整的初始化,分配記憶體和後續的擴容維護工作。 ### 2.5 remove 解析 ```java public V remove(Object key) { Node e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } ``` 將 remove 刪除工作交給內部函式 removeNode() 來實現。 ```java final Node removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node[] tab; Node p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { // 獲取索引, Node node = null, e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 判斷索引處的值是不是想要的結果 node = p; else if ((e = p.next) != null) { // 交給樹的查詢演算法 if (p instanceof TreeNode) node = ((TreeNode)p).getTreeNode(hash, key); else { do { // 遍歷查詢 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) //樹的刪除 ((TreeNode)node).removeTreeNode(this, tab, movable); else if (node == p) // 修復連結串列,連結串列的刪除操作 tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; } ``` ## 三、 HashMap 從連結串列到紅黑樹的轉變 如果連結串列的長度(衝突的節點數)已經達到8個,此時會呼叫 treeifyBin() ,treeifyBin() 首先判斷當前hashMap 的 table的長度,如果不足64,只進行resize,擴容table,如果達到64,那麼將衝突的儲存結構為紅黑樹。 在原始碼還有這樣的一個欄位。 ```java static final int UNTREEIFY_THRESHOLD = 6; // 這樣表明了從紅黑樹轉化為連結串列的閾值為 6,為何同樣不是 8 那? 如果插入和刪除都在 8 附近,將多二者相互轉化將浪費大量的時間,對其效能影響。 如果是的二者轉化的操作不平衡,偏向一方,則可以避免此類影響。 ``` ### 3.1 紅黑樹的資料結構 ```java static final class TreeNode extends LinkedHashMap.Entry { TreeNode parent; // red-black tree links TreeNode left; TreeNode right; TreeNode prev; // 刪除後需要取消連結,指向前一個節點(原連結串列中的前一個節點) boolean red; } ``` 因為 繼承了 LinkedHashMap.Entry ,所以儲存的資料最總最 Entry 中: ```java static class Node implements Map.Entry { final int hash; final K key; V value; Node next; Node(int hash, K key, V value, Node next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } } ``` ### 3.2 承上啟下的 treeifyBin() treeifyBin() 決定了一個連結串列何時轉化為一個紅黑樹。 treeifyBin() 有兩種格式: ```java final void treeifyBin(Node[] tab, int hash); final void treeify(Node[] tab); ``` ```java final void treeifyBin(Node[] tab, int hash) { // 簡單的 Node 修改為 TreeNode,同時維護了 prev 屬性。 int n, index; Node e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode hd = null, tl = null; do { TreeNode p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); // 真正生成紅黑樹的 } } ``` ```java TreeNode replacementTreeNode(Node p, Node next) { return new TreeNode<>(p.hash, p.key, p.value, next); } // 實現 Node 連結串列節點到 TreeNode 節點的轉化。 ``` 下面函式真正實現了連結串列的紅黑樹的轉變。首先構建一個標準查詢二叉樹,然後在標準查詢二叉樹然後調整為一個紅黑樹。而 balanceInsertion() 實現了調整。 ```java /** * Forms tree of the nodes linked from this node. */ final void treeify(Node[] tab) { TreeNode root = null; for (TreeNode x = this, next; x != null; x = next) { next = (TreeNode)x.next; x.left = x.right = null; if (root == null) { // 第一次轉化過程,將連結串列的頭節點作為根節點。 x.parent = null; x.red = false; // 紅黑樹的定義 根節點必須為黑色 root = x; } else { K k = x.key; int h = x.hash; Class kc = null; for (TreeNode p = root;;) { int dir, ph; K pk = p.key; if ((ph = p.hash) > h) //// 通過 Hash 的大小來確定插入順序 dir = -1; // dir 大小順序的標識 else if (ph < h) dir = 1; else if ((kc == null && //當 兩個 Hash 的值相同,進行特殊的方法,確定大小。 (kc = comparableClassFor(k)) == null) || // Returns x's Class if it is of the form "class C implements Comparable ", else null. 如果 key類的 原始碼書寫格式為 C implement Comparable 那麼返回該類型別 C, 如果間接實現也不行。 如果是 String 型別,直接返回 String.class (dir = compareComparables(kc, k, pk)) == 0) // ((Comparable)k).compareTo(pk)); 強制轉換後進行對比,若 dir == 0,則 tieBreakOrder(),繼續仲裁 dir = tieBreakOrder(k, pk); // 首先通過二者的類型別進行比較,如果相等的話,使用 (System.identityHashCode(a) <= System.identityHashCode(b) 使用原始的 hashcode,不是重寫的在對比。 TreeNode xp = p; // 遍歷的,上一個節點 if ((p = (dir <= 0) ? p.left : p.right) == null) { //通過 dir,將 p 向下查詢,直到 p 為 null,找到一個插入時機 x.parent = xp; if (dir <= 0) xp.left = x; else xp.right = x; root = balanceInsertion(root, x); //進行二叉樹的調整 break; } } } } moveRootToFront(tab, root); } ``` ### 3.3 將一個二叉樹轉化為紅黑樹的操作-balanceInsertion() 當紅黑樹中新增節點的時候需要呼叫balanceInsertion方法來保證紅黑樹的特性。 如果想要了解紅黑樹的插入過程那麼必須對紅黑樹的性質有一個較為清晰的瞭解。 紅黑樹的性質: 1. 每個結點或是紅色的,或是黑色的 2. 根節點是黑色的 3. 每個葉結點(NIL)是黑色的 4. 如果一個節點是紅色的,則它的兩個兒子都是黑色的。 5. 對於每個結點,從該結點到其葉子結點構成的所有路徑上的黑結點個數相同。 ```java static TreeNode balanceInsertion(TreeNode root, TreeNode x) { x.red = true; // 插入的子節點必須為 red for (TreeNode xp, xpp, xppl, xppr;;) { //// x 當前處理節點 xp父節點 xpp祖父節點 xppl祖父左節點 xppr 祖父右節點 if ((xp = x.parent) == null) { // 如果 當前處理節點為根節點,滿足紅黑樹的性質,結束迴圈 x.red = false; return x; } else if (!xp.red || (xpp = xp.parent) == null) return root; if (xp == (xppl = xpp.left)) { if ((xppr = xpp.right) != null && xppr.red) { xppr.red = false; xp.red = false; xpp.red = true; x = xpp; } else { if (x == xp.right) { root = rotateLeft(root, x = xp); xpp = (xp = x.parent) == null ? null : xp.parent; } if (xp != null) { xp.red = false; if (xpp != null) { xpp.red = true; root = rotateRight(root, xpp); } } } } else { if (xppl != null && xppl.red) { xppl.red = false; xp.red = false; xpp.red = true; x = xpp; } else { if (x == xp.left) { root = rotateRight(root, x = xp); xpp = (xp = x.parent) == null ? null : xp.parent; } if (xp != null) { xp.red = false; if (xpp != null) { xpp.red = true; root = rotateLeft(root, xpp); } } } } } } ``` #### TreeNode 紅黑樹總結 TreeNode 完整的實現了一套紅黑樹的增刪改查的規則。實現參考了《演算法導論》 ```java /* ------------------------------------------------------------ */ // Red-black tree methods, all adapted from CLR ``` 這裡推薦一個紅黑樹動畫演示網站 https://rbtree.phpisfuture.com/ 紅黑樹是一個不嚴格的平衡二叉查詢樹,高度近似 log(N)。 ## 四、HashMap 的擴充套件 Map中 key 有一個性質,就是 key 不能重複,而 Java Set 的含義:集合中不能有重複的元素。HashMap 的實現已經足夠的優秀。那麼我們是否可以用 key 的性質來實現 Set ? 的確 JDK 中的 HashSet 就是這樣做的。 ```java public class HashSet extends AbstractSet implements Set, Cloneable, java.io.Serializable { private transient HashMap map; // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); } ``` `PRESENT` 就是存進 Map 中的 value,而 key 正是 Set 語義的實現。而且可以判斷出 HashSet 中是允許存入 Null 值的。