TreeMap分析(下)

来源:https://www.cnblogs.com/smartchen/archive/2018/06/02/9104106.html
-Advertisement-
Play Games

通過上篇文章,大家已經能夠清楚的瞭解到treeMap插入結點的過程,那麼本篇文章就來分析下TreeMap刪除一個結點時,內部數據結構發生了怎樣的變化。 TreeMap刪除某個結點的源碼分析 1 /** 2 * 刪除節點,並平衡紅黑樹的操作 3 * 4 * @Param Entry<K,V> p 要刪 ...


通過上篇文章,大家已經能夠清楚的瞭解到treeMap插入結點的過程,那麼本篇文章就來分析下TreeMap刪除一個結點時,內部數據結構發生了怎樣的變化。

 

TreeMap刪除某個結點的源碼分析

 

 1     /**
 2      * 刪除節點,並平衡紅黑樹的操作
 3      * 
 4      * @Param Entry<K,V> p  要刪除的節點Entry
 5      */
 6     private void deleteEntry(Entry<K,V> p) {
 7         modCount++;
 8         size--;  //節點總數-1
 9 
10         if (p.left != null && p.right != null) {  //當前要刪除的節點左右子節點都不為空
11             Entry<K,V> s = successor(p);  //找到一個待刪除節點的繼承者節點s
12             //將指向s節點,後續所有對p的節點判斷其實都是對s節點判斷
13             p.key = s.key;
14             p.value = s.value;
15             p = s;
16         }
17 
18         //替代節點選擇為當前被刪除節點的左子節點(優先)或右子節點
19         Entry<K,V> replacement = (p.left != null ? p.left : p.right);
20 
21         if (replacement != null) {  //替代節點(被刪除節點的子節點)不為空
22             
23             replacement.parent = p.parent;  //將替代節點的父節點指向被刪除節點的父節點
24             if (p.parent == null)  //如果被刪除節點的父節點為null (即被刪除的節點就是樹的根節點,且根節點下麵還有其他節點)
25                 root = replacement;  //將根節點設置為替換節點
26             else if (p == p.parent.left)  //如果原先被刪除節點是左子樹 插入
27                 p.parent.left  = replacement;  //則將替換節點也保持左子樹插入(將替換節點與被刪除節點的父節點左子節點建立引用)
28             else   //如果原先被刪除節點是右子樹 插入
29                 p.parent.right = replacement;  //則將替換節點也保持右子樹插入(將替換節點與被刪除節點的父節點右子節點建立引用)
30 
31             //將被刪除節點的左子節點、右子節點、父節點引用全部置為null
32             p.left = p.right = p.parent = null;
33 
34             //刪除後要執行後續的保證紅黑樹規則的操作
35             if (p.color == BLACK)  //如果被刪除節點是黑色的
36                 fixAfterDeletion(replacement);  //調用刪除後修正紅黑樹規則的方法
37         } else if (p.parent == null) { //被刪除節點就是根節點,且整個樹中就只有一個根節點的情況
38             root = null;  //將根節點置為null(此時整個樹中就沒有節點了)
39         } else {  //被刪除節點沒有子節點可替代的情況 (被刪除節點是葉子節點)
40             if (p.color == BLACK)   //如果被刪除節點是黑色的
41                 fixAfterDeletion(p);  //調用刪除後修正紅黑樹規則的方法
42 
43             if (p.parent != null) {  //如果被刪除節點的父節點不為null
44                 if (p == p.parent.left) //如果原先被刪除節點是左子樹 插入
45                     p.parent.left = null;  //刪除節點後將被刪除節點的父節點的左子節點置為null
46                 else if (p == p.parent.right)  //如果原先被刪除節點是右子樹 插入
47                     p.parent.right = null;  //刪除節點後將被刪除節點的父節點的右子節點置為null
48                 p.parent = null;  //將被刪除節點的父節點引用置為null(即將被刪除節點從樹中移除)
49             }
50         }
51     }
View Code

源碼邏輯很簡單,主要就是分為刪除結點有子結點和無子結點,而無子結點又分為刪除的是根結點或葉子結點這三種情況

然後如果被刪除結點是黑色的,那麼要註意下可能繼承者和被刪除結點的父結點都是紅色的情況,此時需要做平衡紅黑樹的操作。

這裡需要註意的方法有兩個: 第11行的 successor() 方法  以及  第36行的  fixAfterDeletion() 方法。

 

分別貼一下這兩個方法的源碼:

 1     /**
 2      * 返回被刪除節點的繼承者節點
 3      */
 4     static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
 5         if (t == null)  //如果被刪除節點為空,則直接返回null
 6             return null;
 7         else if (t.right != null) {  //如果被刪除節點的右子節點不為空
 8             Entry<K,V> p = t.right;  //將被刪除節點的右子節點記錄下來
 9             while (p.left != null)  //從該節點開始迴圈向下查找左子節點,直至找到葉子節點後返回該葉子節點
10                 p = p.left;
11             return p;
12         } else {  //如果被刪除節點的右子節點為空
13             Entry<K,V> p = t.parent;  //將被刪除節點的父節點用p變數記錄
14             Entry<K,V> ch = t;   //被刪除節點用ch變數記錄
15             while (p != null && ch == p.right) {//從被刪除節點開始迴圈向上查找父節點,直到父節點為空或者父節點沒有右子節點,返回該父節點
16                 ch = p;
17                 p = p.parent;
18             }
19             return p;
20         }
21     }

 

 1    /** 
 2      * 樹刪除一個節點後,將其根據紅黑樹的規則進行修正
 3      * @param x  當前刪除的節點
 4      */
 5     private void fixAfterDeletion(Entry<K,V> x) {
 6        //迴圈遍歷,x剛開始為被刪除的節點
 7         while (x != root && colorOf(x) == BLACK) {  //如果當前遍歷到的節點不是根節點且為黑色
 8             if (x == leftOf(parentOf(x))) {  //如果當前遍歷到的節點是其父節點的左子節點
 9                 Entry<K,V> sib = rightOf(parentOf(x));  //將當前遍歷到的節點的父節點的右子節點用sib變數保存(即和當前節點平級的另一個節點)
10 
11                 if (colorOf(sib) == RED) {  //如果sib引用的節點是紅色的
12                     setColor(sib, BLACK);  //將sib引用的節點設置為黑色
13                     setColor(parentOf(x), RED);  //將當前遍歷到節點的父節點設置為紅色
14                     rotateLeft(parentOf(x));  //對當前遍歷到節點的父節點進行一次左旋操作
15                     sib = rightOf(parentOf(x)); //sib引用的節點變更為旋轉後被刪除節點的父節點的右子節點
16                 }
17 
18                 if (colorOf(leftOf(sib))  == BLACK &&
19                     colorOf(rightOf(sib)) == BLACK) { //如果sib引用節點的左、右子節點都是黑色的
20                     setColor(sib, RED);  //將sib引用的節點設置為紅色
21                     x = parentOf(x);  //下一次遍歷的節點變更為當前遍歷到節點的父節點
22                 } else {  //如果sib引用節點的左、右子節點不全是黑色的
23                     if (colorOf(rightOf(sib)) == BLACK) {  //如果sib引用節點的右子節點是黑色的
24                         setColor(leftOf(sib), BLACK);  //將sib引用節點的左子節點設置為黑色
25                         setColor(sib, RED);   //sib引用節點設置為紅色
26                         rotateRight(sib);  //對sib節點進行一次右旋操作
27                         sib = rightOf(parentOf(x)); //sib引用的節點變更為當前遍歷到的節點的父節點的右子節點
28                     }
29                     setColor(sib, colorOf(parentOf(x)));  //將sib引用節點的顏色設置為 當前遍歷到節點的父節點 一樣的顏色
30                     setColor(parentOf(x), BLACK);  //將當前遍歷到節點的父節點設置為黑色
31                     setColor(rightOf(sib), BLACK);  //將sib引用節點的右子節點設置為黑色
32                     rotateLeft(parentOf(x));  //對當前遍歷到的節點的父節點進行一次左旋操作
33                     x = root;  //下一次遍歷的節點變更為根節點
34                 }
35             } else { // 當前遍歷到的節點是其父節點的右子節點,和上述情況相似,不再贅述
36                 Entry<K,V> sib = leftOf(parentOf(x));
37 
38                 if (colorOf(sib) == RED) {
39                     setColor(sib, BLACK);
40                     setColor(parentOf(x), RED);
41                     rotateRight(parentOf(x));
42                     sib = leftOf(parentOf(x));
43                 }
44 
45                 if (colorOf(rightOf(sib)) == BLACK &&
46                     colorOf(leftOf(sib)) == BLACK) {
47                     setColor(sib, RED);
48                     x = parentOf(x);
49                 } else {
50                     if (colorOf(leftOf(sib)) == BLACK) {
51                         setColor(rightOf(sib), BLACK);
52                         setColor(sib, RED);
53                         rotateLeft(sib);
54                         sib = leftOf(parentOf(x));
55                     }
56                     setColor(sib, colorOf(parentOf(x)));
57                     setColor(parentOf(x), BLACK);
58                     setColor(leftOf(sib), BLACK);
59                     rotateRight(parentOf(x));
60                     x = root;
61                 }
62             }
63         }
64 
65         setColor(x, BLACK);
66     }
View Code

可能光看這兩個方法的源碼不太好理解,下麵就以刪除幾個結點的實例,一步一步跟蹤下源碼看看數據結構到底發生了哪些變化。

 

 

先回顧下上一篇文章插入結點完成後的紅黑樹狀態圖:

 

好,那麼首先來刪除根結點30。因為30結點既有左子結點,又有右子結點,所以在deleteEntry()方法中需要調用successor() 方法尋找繼承者結點。

查看successor() 方法源碼,可以發現會走第7行的 else if 邏輯,然後從70結點開始迴圈查找左子結點,直到找到葉子結點50為止,所以最終的繼承者結點就是50,下麵的三行代碼會將根結點30的key-value變為繼承者的key-value,所以現在根結點為50,黑色。

然後下一行會取得replacement變數,因為現在p=s了,即這裡的p.left實際判斷的是葉子結點50是否有左子結點,很顯然葉子結點50既沒有左子結點也沒有右子結點,所以最後replacement = null。

而葉子結點50是有父結點的,所以不會走 else if (p.parent == null) 分支,而是走下麵的else{}分支。葉子結點50目前是紅色的,所以不需要做平衡紅黑樹的操作。

再往下,很顯然葉子結點50是有父結點的,所以會走 if (p.parent != null) 的分支,而這個分支裡面又是一個if-else分支,這裡判斷的是當前結點是左子樹插入還是右子樹插入,葉子結點50很明顯是左子樹插入,所以會執行  p.parent.left = null 這行代碼,即把葉子結點50給刪除掉了。

全部過程執行完後,紅黑樹當前的狀態圖如下所示:

 

經過一次刪除操作後,童鞋們是否對這兩個方法有點印象了呢?不過這次刪除30結點我們找到的繼承者結點50是紅色的,所以沒有走修正紅黑樹平衡的操作。那麼下麵就再刪除一個結點,讓它進行平衡紅黑樹的操作。

 

 

還是以最原始的紅黑樹狀態圖為基準,這次我們來刪除結點70。70也需要尋找繼承者結點,因為70有右子結點,所以會走  else if (t.right != null) 這個分支邏輯,然後指針移動到85結點,而又因為85沒有左子結點,所以不符合下麵的 while (p.left != null)迴圈條件,方法結束,最後選擇的繼承者結點就是85結點。

然後和上面一樣,會走三行代碼,將70結點變更為85結點,然後將指針指向85結點。很顯然85結點既沒有左子結點也沒有右子結點,所以replacement = null。然後85結點是有父結點的,所以和上面一樣走的是else{}分支。

註意,這裡和上面的不同之處在於,目前指針是指向85這個節點的,而這個節點是黑色的,所以在else{}分支里的  if (p.color == BLACK) 為true,則會調用 fixAfterDeletion(p) 這個方法進行紅黑樹的修正操作。

在調用修正操作之前,紅黑樹的狀態是這樣的:

好,接下來我們看一下具體的修正操作幹了哪些事情。

首先此時指針指向的黑色85結點是滿足  while (x != root && colorOf(x) == BLACK) 這個迴圈條件的,然後這個節點是右子樹插入的,所以會走else{}分支。

然後下一步的sib節點取得是父結點的左子結點,即60結點,而60結點是黑色,所以不會走  if (colorOf(sib) == RED) 這個分支邏輯。

然而60結點的左右子結點都不是黑色的,所以會走下麵的else{}分支,並且不會走 else分支中的  if (colorOf(leftOf(sib)) == BLACK) 這個判斷,只會從第 56 行的代碼開始走

setColor(sib, colorOf(parentOf(x)))  是將60結點的顏色設置為何黑色85結點的父結點(即紅色85結點)的顏色,所以此時60結點變為紅色。

setColor(parentOf(x), BLACK)  這一行又把紅色的85結點設置為了黑色

setColor(leftOf(sib), BLACK)  這一行又將50結點設置成了黑色,此時除了60結點為紅色,其餘的50、85、85結點都是黑色

rotateRight(parentOf(x))  這一行以之前為紅色的85結點為中心,做一次右旋操作。忘記旋轉操作的童鞋看看前面的帖子,就不難理解啦。

然後將指針移動回根結點,並設置根結點為黑色,此時整個修正紅黑樹操作就結束了。此時紅黑樹狀態圖如下所示:

 

註意:不要忘記再回到之前的deleteEntry方法中去,只是走完了修正紅黑樹的方法,但整個刪除操作還沒全部結束呢!

回到原方法的  if (p.parent != null)分支,此時85結點是有父結點的,所以會走這個分支邏輯,然後85又是右子樹插入的,所以會走 else if (p == p.parent.right) 這個分支邏輯

然後這行  p.parent.right = null  會將葉子結點85刪除,此時才是真正走完了整個刪除節點的操作。此時童鞋們小本本上的紅黑樹應該是這樣的喲:

 

好了,大家是不是覺得其實紅黑樹的操作並不是很困難呢?只要肯踏踏實實、一步一步的去仔細分析每一步紅黑樹是如何變化的就能夠輕鬆得到結果。

 

最後還有一個刪除葉子結點的沒有講,比較簡單就讓童鞋們自己去研究實踐吧!

在這裡放上兩張狀態圖,以便童鞋們進行驗證(圖中的狀態是以刪除葉子結點85為例所得到的結果):

1.進行紅黑樹修正操作之後的狀態圖:

 

2.刪除葉子結點85操作全部結束後的狀態圖:

 

註:以上圖片出處均為 博客園——五月的倉頡,非本人原創!

 

到這裡,關於紅黑樹的所有知識全部都講解完畢,並且集合的基礎知識也暫告一段落了。

有的童鞋會問,怎麼只講了List、Map介面下的一些常用工具類,Set介面的怎麼不講了呢?

大家可以去看下HashSet、TreeSet的源碼相關實現,其實就是HashSet、TreeSet去掉了Value而已,絕大多數實現都是一樣的,所以我這裡就不再去細說了

註意我在這個集合分類中所講的集合類都是非線程安全的,像CopyOnWriteArrayList、ConcurrentHashMap、BlockingQueue等線程安全的集合工具類我會放在多線程的分類主題中去講解。

 

OK,接下來我會更新一些多線程相關的知識,下期見!

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 假設現在已經打包了一個文件(1233444333),要將這個文件傳輸給另一方: 其中的上傳數據模塊和下載模塊可以單獨進行分裝後使用。 結果: ...
  • 時間序列深度學習:狀態 LSTM 模型預測太陽黑子 本文翻譯自《Time Series Deep Learning: Forecasting Sunspots With Keras Stateful Lstm In R》 "原文鏈接" 由於數據科學機器學習和深度學習的發展,時間序列預測在預測準確性方 ...
  • 前言 本人在通過《C語言程式設計:現代方法(第2版)》自學C語言時,發現國內並沒有該書完整的課後習題答案,所以就想把自己在學習過程中所做出的答案分享出來,以供大家參考。這些答案是本人自己解答,並參考GitHub上相關的分享和Chegg.com相關資料。因為並沒有權威的答案來源,所以可能會存在錯誤的地 ...
  • Java的數組以及操作數組一:什麼是數組;二:如何使用 Java 中的數組;三:使用迴圈操作 Java 中的數組;四:使用 Arrays 類操作 Java 中的數組;五:使用 foreach 操作數組;六:Java 中的二維數組; ...
  • 在Spring生態中,JavaConfig 如何優雅的替換 XML ...
  • RabbitMQ基礎教程之使用進階篇 相關博文,推薦查看: I. 背景 前一篇基本使用篇的博文中,介紹了rabbitmq的三種使用姿勢,可以知道如何向RabbitMQ發送消息以及如何消費,但遺留下幾個疑問,本篇則主要希望弄清楚這幾點 Exchange聲明的問題(是否必須聲明,如果不聲明會怎樣) Ex ...
  • 推薦寫法 具體解釋可以往後看。 原理 1. 每一個 執行文件,都自動創建一個 對象,同時, 對象會創建一個叫 的屬性,初始化的值是 。即: 2. 是引用 的值 3. 模塊導出的時候,真正導出的執行是 ,而不是 1與2的demo 3的demo 為了驗證真正導出的是 而不是 ,我們對 修改如下: 的輸出 ...
  • 心血來潮,手機上導出的圖片全部按日期放在不同文件夾,很是麻煩,想放在一起方便瀏覽,手動操作費時費力,想到bat命令,不是很熟,看到python欣喜不已,很是方便 遞歸遍歷文件,剪切出來,刪除空文件夾 不足:未考慮各種異常 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...