死磕 java集合之TreeMap源碼分析(三)- 內含紅黑樹分析全過程

来源:https://www.cnblogs.com/tong-yuan/archive/2019/04/04/10657620.html
-Advertisement-
Play Games

死磕 java集合之TreeMap源碼分析(三) 紅黑樹刪除元素的時間複雜度如何? 為什麼刪除元素之後要做平衡? 以什麼樣的形式平衡最省時間? ...


歡迎關註我的公眾號“彤哥讀源碼”,查看更多源碼系列文章, 與彤哥一起暢游源碼的海洋。


刪除元素

刪除元素本身比較簡單,就是採用二叉樹的刪除規則。

(1)如果刪除的位置有兩個葉子節點,則從其右子樹中取最小的元素放到刪除的位置,然後把刪除位置移到替代元素的位置,進入下一步。

(2)如果刪除的位置只有一個葉子節點(有可能是經過第一步轉換後的刪除位置),則把那個葉子節點作為替代元素,放到刪除的位置,然後把這個葉子節點刪除。

(3)如果刪除的位置沒有葉子節點,則直接把這個刪除位置的元素刪除即可。

(4)針對紅黑樹,如果刪除位置是黑色節點,還需要做再平衡。

(5)如果有替代元素,則以替代元素作為當前節點進入再平衡。

(6)如果沒有替代元素,則以刪除的位置的元素作為當前節點進入再平衡,平衡之後再刪除這個節點。

public V remove(Object key) {
    // 獲取節點
    Entry<K,V> p = getEntry(key);
    if (p == null)
        return null;

    V oldValue = p.value;
    // 刪除節點
    deleteEntry(p);
    // 返回刪除的value
    return oldValue;
}

private void deleteEntry(Entry<K,V> p) {
    // 修改次數加1
    modCount++;
    // 元素個數減1
    size--;

    if (p.left != null && p.right != null) {
        // 如果當前節點既有左子節點,又有右子節點
        // 取其右子樹中最小的節點
        Entry<K,V> s = successor(p);
        // 用右子樹中最小節點的值替換當前節點的值
        p.key = s.key;
        p.value = s.value;
        // 把右子樹中最小節點設為當前節點
        p = s;
        // 這種情況實際上並沒有刪除p節點,而是把p節點的值改了,實際刪除的是p的後繼節點
    }

    // 如果原來的當前節點(p)有2個子節點,則當前節點已經變成原來p的右子樹中的最小節點了,也就是說其沒有左子節點了
    // 到這一步,p肯定只有一個子節點了
    // 如果當前節點有子節點,則用子節點替換當前節點
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);

    if (replacement != null) {
        // 把替換節點直接放到當前節點的位置上(相當於刪除了p,並把替換節點移動過來了)
        replacement.parent = p.parent;
        if (p.parent == null)
            root = replacement;
        else if (p == p.parent.left)
            p.parent.left  = replacement;
        else
            p.parent.right = replacement;

        // 將p的各項屬性都設為空
        p.left = p.right = p.parent = null;

        // 如果p是黑節點,則需要再平衡
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    } else if (p.parent == null) {
        // 如果當前節點就是根節點,則直接將根節點設為空即可
        root = null;
    } else {
        // 如果當前節點沒有子節點且其為黑節點,則把自己當作虛擬的替換節點進行再平衡
        if (p.color == BLACK)
            fixAfterDeletion(p);

        // 平衡完成後刪除當前節點(與父節點斷絕關係)
        if (p.parent != null) {
            if (p == p.parent.left)
                p.parent.left = null;
            else if (p == p.parent.right)
                p.parent.right = null;
            p.parent = null;
        }
    }
}

刪除再平衡

經過上面的處理,真正刪除的肯定是黑色節點才會進入到再平衡階段。

因為刪除的是黑色節點,導致整顆樹不平衡了,所以這裡我們假設把刪除的黑色賦予當前節點,這樣當前節點除了它自已的顏色還多了一個黑色,那麼:

(1)如果當前節點是根節點,則直接塗黑即可,不需要再平衡;

(2)如果當前節點是紅+黑節點,則直接塗黑即可,不需要平衡;

(3)如果當前節點是黑+黑節點,則我們只要通過旋轉把這個多出來的黑色不斷的向上傳遞到一個紅色節點即可,這又可能會出現以下四種情況:

(假設當前節點為父節點的左子節點)

情況 策略
1)x是黑+黑節點,x的兄弟是紅節點 (1)將兄弟節點設為黑色;
(2)將父節點設為紅色;
(3)以父節點為支點進行左旋;
(4)重新設置x的兄弟節點,進入下一步;
2)x是黑+黑節點,x的兄弟是黑節點,且兄弟節點的兩個子節點都是黑色 (1)將兄弟節點設置為紅色;
(2)將x的父節點作為新的當前節點,進入下一次迴圈;
3)x是黑+黑節點,x的兄弟是黑節點,且兄弟節點的右子節點為黑色,左子節點為紅色 (1)將兄弟節點的左子節點設為黑色;
(2)將兄弟節點設為紅色;
(3)以兄弟節點為支點進行右旋;
(4)重新設置x的兄弟節點,進入下一步;
3)x是黑+黑節點,x的兄弟是黑節點,且兄弟節點的右子節點為紅色,左子節點任意顏色 (1)將兄弟節點的顏色設為父節點的顏色;
(2)將父節點設為黑色;
(3)將兄弟節點的右子節點設為黑色;
(4)以父節點為支點進行左旋;
(5)將root作為新的當前節點(退出迴圈);

(假設當前節點為父節點的右子節點,正好反過來)

情況 策略
1)x是黑+黑節點,x的兄弟是紅節點 (1)將兄弟節點設為黑色;
(2)將父節點設為紅色;
(3)以父節點為支點進行右旋;
(4)重新設置x的兄弟節點,進入下一步;
2)x是黑+黑節點,x的兄弟是黑節點,且兄弟節點的兩個子節點都是黑色 (1)將兄弟節點設置為紅色;
(2)將x的父節點作為新的當前節點,進入下一次迴圈;
3)x是黑+黑節點,x的兄弟是黑節點,且兄弟節點的左子節點為黑色,右子節點為紅色 (1)將兄弟節點的右子節點設為黑色;
(2)將兄弟節點設為紅色;
(3)以兄弟節點為支點進行左旋;
(4)重新設置x的兄弟節點,進入下一步;
3)x是黑+黑節點,x的兄弟是黑節點,且兄弟節點的左子節點為紅色,右子節點任意顏色 (1)將兄弟節點的顏色設為父節點的顏色;
(2)將父節點設為黑色;
(3)將兄弟節點的左子節點設為黑色;
(4)以父節點為支點進行右旋;
(5)將root作為新的當前節點(退出迴圈);

讓我們來看看TreeMap中的實現:

/**
 * 刪除再平衡
 *(1)每個節點或者是黑色,或者是紅色。
 *(2)根節點是黑色。
 *(3)每個葉子節點(NIL)是黑色。(註意:這裡葉子節點,是指為空(NIL或NULL)的葉子節點!)
 *(4)如果一個節點是紅色的,則它的子節點必須是黑色的。
 *(5)從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。
 */
private void fixAfterDeletion(Entry<K,V> x) {
    // 只有當前節點不是根節點且當前節點是黑色時才進入迴圈
    while (x != root && colorOf(x) == BLACK) {
        if (x == leftOf(parentOf(x))) {
            // 如果當前節點是其父節點的左子節點
            // sib是當前節點的兄弟節點
            Entry<K,V> sib = rightOf(parentOf(x));

            // 情況1)如果兄弟節點是紅色
            if (colorOf(sib) == RED) {
                // (1)將兄弟節點設為黑色
                setColor(sib, BLACK);
                // (2)將父節點設為紅色
                setColor(parentOf(x), RED);
                // (3)以父節點為支點進行左旋
                rotateLeft(parentOf(x));
                // (4)重新設置x的兄弟節點,進入下一步
                sib = rightOf(parentOf(x));
            }

            if (colorOf(leftOf(sib))  == BLACK &&
                    colorOf(rightOf(sib)) == BLACK) {
                // 情況2)如果兄弟節點的兩個子節點都是黑色
                // (1)將兄弟節點設置為紅色
                setColor(sib, RED);
                // (2)將x的父節點作為新的當前節點,進入下一次迴圈
                x = parentOf(x);
            } else {
                if (colorOf(rightOf(sib)) == BLACK) {
                    // 情況3)如果兄弟節點的右子節點為黑色
                    // (1)將兄弟節點的左子節點設為黑色
                    setColor(leftOf(sib), BLACK);
                    // (2)將兄弟節點設為紅色
                    setColor(sib, RED);
                    // (3)以兄弟節點為支點進行右旋
                    rotateRight(sib);
                    // (4)重新設置x的兄弟節點
                    sib = rightOf(parentOf(x));
                }
                // 情況4)
                // (1)將兄弟節點的顏色設為父節點的顏色
                setColor(sib, colorOf(parentOf(x)));
                // (2)將父節點設為黑色
                setColor(parentOf(x), BLACK);
                // (3)將兄弟節點的右子節點設為黑色
                setColor(rightOf(sib), BLACK);
                // (4)以父節點為支點進行左旋
                rotateLeft(parentOf(x));
                // (5)將root作為新的當前節點(退出迴圈)
                x = root;
            }
        } else { // symmetric
            // 如果當前節點是其父節點的右子節點
            // sib是當前節點的兄弟節點
            Entry<K,V> sib = leftOf(parentOf(x));

            // 情況1)如果兄弟節點是紅色
            if (colorOf(sib) == RED) {
                // (1)將兄弟節點設為黑色
                setColor(sib, BLACK);
                // (2)將父節點設為紅色
                setColor(parentOf(x), RED);
                // (3)以父節點為支點進行右旋
                rotateRight(parentOf(x));
                // (4)重新設置x的兄弟節點
                sib = leftOf(parentOf(x));
            }

            if (colorOf(rightOf(sib)) == BLACK &&
                    colorOf(leftOf(sib)) == BLACK) {
                // 情況2)如果兄弟節點的兩個子節點都是黑色
                // (1)將兄弟節點設置為紅色
                setColor(sib, RED);
                // (2)將x的父節點作為新的當前節點,進入下一次迴圈
                x = parentOf(x);
            } else {
                if (colorOf(leftOf(sib)) == BLACK) {
                    // 情況3)如果兄弟節點的左子節點為黑色
                    // (1)將兄弟節點的右子節點設為黑色
                    setColor(rightOf(sib), BLACK);
                    // (2)將兄弟節點設為紅色
                    setColor(sib, RED);
                    // (3)以兄弟節點為支點進行左旋
                    rotateLeft(sib);
                    // (4)重新設置x的兄弟節點
                    sib = leftOf(parentOf(x));
                }
                // 情況4)
                // (1)將兄弟節點的顏色設為父節點的顏色
                setColor(sib, colorOf(parentOf(x)));
                // (2)將父節點設為黑色
                setColor(parentOf(x), BLACK);
                // (3)將兄弟節點的左子節點設為黑色
                setColor(leftOf(sib), BLACK);
                // (4)以父節點為支點進行右旋
                rotateRight(parentOf(x));
                // (5)將root作為新的當前節點(退出迴圈)
                x = root;
            }
        }
    }

    // 退出條件為多出來的黑色向上傳遞到了根節點或者紅節點
    // 則將x設為黑色即可滿足紅黑樹規則
    setColor(x, BLACK);
}

刪除元素舉例

假設我們有下麵這樣一顆紅黑樹。

treemap-delete1

我們刪除6號元素,則從右子樹中找到了最小元素7,7又沒有子節點了,所以把7作為當前節點進行再平衡。

我們看到7是黑節點,且其兄弟為黑節點,且其兄弟的兩個子節點都是紅色,滿足情況4),平衡之後如下圖所示。

treemap-delete2

我們再刪除7號元素,則從右子樹中找到了最小元素8,8有子節點且為黑色,所以8的子節點9是替代節點,以9為當前節點進行再平衡。

我們發現9是紅節點,則直接把它塗成黑色即滿足了紅黑樹的特性,不需要再過多的平衡了。

treemap-delete3

這次我們來個狠的,把根節點刪除,從右子樹中找到了最小的元素5,5沒有子節點,所以把5作為當前節點進行再平衡。

我們看到5是黑節點,且其兄弟為紅色,符合情況1),平衡之後如下圖所示,然後進入情況2)。

treemap-delete4

對情況2)進行再平衡後如下圖所示。

treemap-delete5

然後進入下一次迴圈,發現不符合迴圈條件了,直接把x塗為黑色即可,退出這個方法之後會把舊x刪除掉(見deleteEntry()方法),最後的結果就是下麵這樣。

treemap-delete6


未完待續,下一節我們一起探討紅黑樹遍歷元素的操作。

現在公眾號文章沒辦法留言了,如果有什麼疑問或者建議請直接在公眾號給我留言。


歡迎關註我的公眾號“彤哥讀源碼”,查看更多源碼系列文章, 與彤哥一起暢游源碼的海洋。

qrcode


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

-Advertisement-
Play Games
更多相關文章
  • 概念重覆請求是指一個請求因為某些原因被多次提交,場景簡述如下:1)用戶快速多次點擊按鈕2)Nginx失敗重試機制3)服務框架失敗重試機制4)MQ消息重覆消費5)第三方支付支付成功後,因為異常原因導致的多次非同步回調; 冪等性是指同樣的請求參數,多次請求返回的結果相同。一般是因為重覆請求導致的重覆操作等 ...
  • 經過前兩個模式的學習,是不是對設計模式有了進一步的認識了呢,現在,我們繼續沖鴨。 本章可以稱為“給愛用繼承的人一個全新的設計眼界”。這裡我們即將再度探討典型的繼承濫用問題,我們將學到如何使用對象組合的方式,做到在運行時裝飾類。為什麼呢?一旦熟悉了裝飾的技巧,你將能夠在不修改任何底層代碼的情況下,給對 ...
  • 多線程 什麼是線程? - 能獨立運行的基本單位——線程(Threads)。 - 線程是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。 - 一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務。 - 就好比生產的工廠,一個車 ...
  • 前言 開心一刻 女兒: “媽媽,你這麼漂亮,當年怎麼嫁給了爸爸呢?” 媽媽: “當年你爸不是窮嘛!‘ 女兒: “窮你還嫁給他!” 媽媽: “那時候剛剛畢業參加工作,領導對我說,他是我的扶貧對象,我年輕理解錯了,就嫁給他了!” 女兒...... @Import註解應用 應用開發中,當我們的功能模塊比較 ...
  • 列表推導式和生成表達式有相似,但是卻有本質不不同簡單的把它們之間的關係理一理吧:# 列表推導式: res=[i for i in range(6)] print(res) 結果:[0, 1, 2, 3, 4, 5] # 生成表達式: res=(i for i in range(6)) print(r ...
  • Python 簡介 Python 是一個高層次的結合瞭解釋性、編譯性、互動性和麵向對象的腳本語言。 Python 的設計具有很強的可讀性,相比其他語言經常使用英文關鍵字,其他語言的一些標點符號,它具有比其他語言更有特色語法結構。 Python 是一個高層次的結合瞭解釋性、編譯性、互動性和麵向對象的腳 ...
  • Python基礎數據類型之int,str,bool及常用方法 ...
  • 回到上次編輯位置 Ctrl + Alt + <- (向後) Ctrl + Alt + -> (向前) 這個快捷鍵有時和電腦桌面快捷鍵衝突。解決辦法: win + D 回到電腦桌面,右鍵—>圖像選項—>快捷鍵—>禁用 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...