紅黑樹——一種自平衡的二叉樹

来源:https://www.cnblogs.com/touch-fish/archive/2023/02/16/17128613.html
-Advertisement-
Play Games

JOSN處理和HttpMessageConverter 1.JSON處理-@ResponseBody 說明:在實際開發中,我們往往需要伺服器返回的數據都是 JSON 格式。 SpringMVC 提供了 @ResponseBody 註解,用來標註 Controller 方法的返回的格式為 JSON,將 ...


紅黑樹——一種自平衡的二叉樹

一、紅黑樹簡介

普通二叉樹在數據不夠均勻的情況下,可能導致左右子樹高度會相差比較大,最壞情況下樹的結構相當於一個鏈表,時間複雜度為n。為了使二叉樹在最壞情況下也能有log(n)的性能,需要對二叉樹進行平衡操作,相應的演算法有很多,紅黑樹就是其中一種演算法。紅黑樹是一種自平衡的二叉搜索樹,它每一個節點有一個存儲位表示顏色。通過對路徑上的顏色約束,紅黑樹保證沒有一條路徑比其他路徑長2倍,因而是接近平衡的。相對於普通的二叉搜索樹,紅黑樹在最壞的情況下保證插入和刪除操作的時間複雜度是log(n)

一顆紅黑樹需要滿足下列5條規則:

  1. 每一個節點是紅色和黑色的一種
  2. 根節點是黑色的
  3. null節點是黑色,也就是所以的葉子節點都是黑色
  4. 紅色節點不能相鄰(也就是紅色節點的孩子必定是黑色)
  5. 從任意一個節點到葉子節點(不包含這個節點),黑色節點的數量相同,這個規則也叫紅黑樹的黑高

紅黑樹的搜索與一般的搜索二叉樹沒有其他區別,主要的區別是插入和刪除操作,因為這兩個操作會修改二叉樹結構,導致紅黑樹規則被破壞。紅黑樹通過變色、左旋、右旋來修複規則被破壞的情況,下麵簡單介紹一下這三個規則。

1、變色

變色,顧名思義就是改變紅黑樹的顏色,也就是紅變黑,黑變紅。變色作用的是單個節點,一些文章把變色總結成了複合操作其實是不對的,變色規則需要結合情況具體分析,死記不利於理解紅黑樹的(說不定還記不住)。實際上變色目的是改變節點路徑上黑色數量,把一個節點變成黑色,可以增加這個節點路徑上黑色節點的數量,反之則減少黑色節點的數量。

變色示意圖:

2、左旋

某個節點左旋,這個節點的右孩子不能為空。把一個節點向左旋轉,“右孩子”替換節點位置,把節點作為“右孩子”新左節點,“右孩子”的“左孩子”變成節點新的右孩子。規則有點繞,看一下示意圖就明白了:
image-20230213121958296

圖中B是A的右孩子,提升B的位置,A作為B的左孩子,然後把C作為A的右孩子。左旋只會改變旋轉節點右邊的子樹,不會改變節點的左子樹,如圖D相對於A的位置還是不變的。左旋的一個特性是會把右子樹的高度變低,左子樹變高。另一個特性是可以調換左右子樹的元素。

3、右旋

某個節點右旋,這個節點的左孩子不能為空。把一個節點向右旋轉,“左孩子”替換節點位置,把節點作為“左孩子”新右節點,“左孩子”的“右孩子”變成節點新的左孩子。示意圖如下:

image-20230213121924887

圖中B是A的左孩子,提升B的位置,A作為B的右孩子,然後把C作為A的左孩子。右旋只會改變旋轉節點左邊的子樹,不會改變節點的右子樹,如圖D相對於A的位置還是不變的。右旋的一個特性是會把左子樹的高度變低,右子樹變高。另一個特性是可以調換左右子樹的元素。


二、紅黑樹的插入

1、叔叔節點

首先介紹一下叔叔節點,叔叔節點顧名思義就是父親節點的兄弟。例如下麵這個結構,C節點的父親為B,叔叔節點為D:

叔叔節點

2、新節點的顏色分析

首先可以證明插入的新節點一定是紅節點。如果插入節點是黑節點,那麼當插入第二個節點,規則5肯定無法滿足了(例如從根節點到左右葉節點的黑高不一樣)。由於插入黑色節點必然會破壞紅黑樹的規則,所以不如一開始插入紅色節點高效,所以插入的節點需要是紅節點。

image-20230215170510872

如上圖,插入的新節點0如果是黑色,會導致根節點5到葉子節點的距離不一致。此時只能把新節點0變回紅色,同理多層節點情況下如果新節點是黑色也一定要執行變色或旋轉才能修複規則

3、插入位置是根節點

如果插入的位置是根節點,則直接把這個節點變成黑色,即可滿足紅黑樹所有性質

4、插入位置的父親節點是黑色

在一個黑色節點的下方插入一個紅色節點,由於紅色節點不提供黑色數量,此時直接插入新節點即可。

image-20230215172337548

如圖,在一個黑色節點(5)下方插入一個紅色節點,不會破壞紅黑樹任何規則

5、插入位置的父節點是紅色

如果插入節點的父親節點是紅色,那麼很明顯此時紅黑樹不滿足規則4(紅色節點不能相鄰),此時就需要通過變色/旋轉操作來修複破壞的紅黑樹規則。由於規則4(紅色節點不能相鄰)的約束,因為插入前是滿足紅黑樹規則的,所以此時爺爺節點必定是黑色。所以此時只有兩種情況:

  1. 節點的叔叔節點是紅節點
  2. 節點的叔叔節點是黑節點

5.1、節點的叔叔節點是紅節點

已知插入前父親節點和叔叔節點是紅色,那麼根據紅黑樹規則,爺爺節點必定是黑色。此時只要把爺爺節點的黑色分給父親和叔叔就行了,也就是把父親節點和叔叔節點變成黑色,爺爺節點變成紅色。如下圖所示:

image-20230215174733523

如圖,插入了一個新節點15,破壞了規則4(紅色節點不能相鄰)。因為叔叔節點0是紅色的,可以把爺爺節點5的顏色分給叔叔節點0父親節點10。由於爺爺節點的左右子樹同時增加的1的黑色,所以此時規則5也可以被滿足。

但是由於我們不知道爺爺節點5的父親是什麼顏色,我們需要把檢查的標記定位到爺爺節點5上,再次檢查紅黑樹是否因為此次變色操作破壞了其他規則。

5.2、節點的叔叔節點是黑節點

如果叔叔節點是黑色,所以通過拆分爺爺節點顏色來修複規則(拆分顏色後叔叔相當於有2層黑)。那可不可以把新節點直接換到叔叔節點下呢?答案是不可以,因為節點的不光要滿足紅黑樹的規則,也要滿足搜索二叉樹的規則,我們不可以隨便交換節點的位置,否則搜索的時候就會出問題。雖然直接調換位置不行,但是可以通過旋轉來間接調換位置。

以節點插入的子樹是其爺爺的右子樹為例,此時插入的新節點15

5.2.1是插入後紅黑樹情況、5.2.2是以爺爺節點5左旋後紅黑樹情況、5.2.2是變色後紅黑樹情況

因為要換一個節點到另一邊,所以對爺爺節點5進行左旋,然而左旋會使叔叔節點的子樹的黑色數量+1,父親節點的子樹黑色數量少1,雖然修複了規則4,但是又新破壞了規則5,所以這時需要一個變色操作來修複再次被破壞的規則5。很顯然旋轉後只需要之前爺爺節點5變紅,父親節點10變黑就能修複規則5

但是假設插入的節點值是7,直接左旋會把新節點7也帶到左子樹,所以此時先要以父親節點進行一次右旋,轉換為圖5.2.1中類似的情況。

image-20230215183412590

然後再以爺爺節點5進行左旋和變色即可恢復規則

同理,如果節點插入的子樹是其爺爺的左子樹也是類似的情況,這裡就不多贅述。

6、插入操作偽代碼

//node的屬性color、left、right,parent分別為節點的顏色、節點的左子節點、節點的右子節點、節點的父親
//枚舉RED為紅,BLACK為黑
//leftRotate為左旋函數
//rightRotate為左旋函數
//Root為根節點的指針
//fixInsertRule的node參數為新插入的節點
fixInsertRule(node){
    while(node != ROOT and node.parent.color == RED){
        grandparent = node.parent.parent//爺爺節點,由於父親節點是紅色,爺爺節點顏色必定是黑色
        if(node.parent == grandparent.right){
            uncle = grandparent.left;//叔叔節點
            if(uncle.color == RED){//叔叔節點是紅色
                uncle.color = BLACK;
                node.parent.color = BLACK;
                grandparent.color = RED;
                //把檢查節點定位到爺爺節點上,繼續下一次的迴圈
                node = grandparent;
            }else{//叔叔節點是黑色
                if(node == node.parent.left){
                    //先要以父親進行一次右旋
                    //這裡需要註意的是右旋後原來的父親變成孩子,原來的孩子變成父親
                    //所以爺爺節點和叔叔節點還是不變的
                    node = node.parent;
                    rightRotate(node);
                }
                //為了方便,先變顏色
                node.parent.color = BLACK;
                grandparent.color = RED;
                //以爺爺節點左旋
                leftRotate(grandparent);
            }
        }else{
            //節點位置是爺爺節點的左子樹的情況,這裡省略...
        }
    }
    //旋轉和變色後可能會導致根節點會變紅,重新設置根節點為黑色即可
    ROOT.color = BLACK;
}

三、紅黑樹的刪除

相對於插入操作,二叉樹的節點刪除的方式有很多種。比如下麵這顆樹,需要刪除節點20。既可以使用節點10來替換被刪除的20節點,然後把節點30連接到左子樹最大的節點後面。也可以使用節點30來替換被刪除節點,然後把節點10連接到右子樹最小的節點後面

image-20230216101539136

但是這兩種方案都不太適合紅黑樹,紅黑樹在插入時會平衡左右子樹的高度,上面兩種方案會讓刪除節點左右子樹變成極大的不平衡,所以紅黑樹的刪除需要找一個影響結構最小的刪除方案。

使用後繼節點替代被刪除節點是一個很好的方案(前繼節點也行,本文以後繼節點進行分析),由於是後繼節點,那麼後繼節點最多只會有一個右孩子,使用後繼節點替換刪除節點,並不會影響前繼節點的子樹。所以只需要對後繼節點右子樹進行向上檢查即可修複規則。

image-20230216102543396

使用後繼節點的刪除方案。只會影響後繼節點所在的子樹。

使用後繼節點替換刪除節點,只需要把後繼節點的顏色變成和刪除節點一樣。然後從後繼節點的右孩子開始檢查紅黑樹是否滿足。由於刪除前是滿足紅黑樹規則的,所以後繼節點和後繼右孩子的顏色只會有這幾種情況。

  1. 後繼節點是紅色,後繼右孩子是黑色。此時後繼節點的父親一定為黑
  2. 後繼節點是黑色,後繼右孩子是紅色。後繼節點的父親的顏色不確定
  3. 後繼節點是黑色,後繼右孩子是黑色。後繼節點的父親的顏色不確定

1、後繼節點是紅色

因為後繼節點是紅色,說明後繼右孩子必定是黑色,父親節點必定也是黑色。使用一個黑色節點替換紅色節點的位置,既不會破壞規則4,也不會破壞規則5。此時無需對樹的結構進行修複。

2、後繼節點是黑色,後繼右孩子是紅色

把後繼右孩子變成黑色,即可滿足規則5

3、後繼節點是黑色,後繼右孩子是黑色

因為後繼右孩子本身是黑色,無法繼續變黑,如果要滿足規則5,需要後繼右孩子具有2層的黑色屬性才行。既然一個節點不能有2層黑色,可以想辦法把多餘的這層黑色移到合適的位置來修複規則5

根據後繼右孩子的兄弟節點顏色,又可以分為這幾種情況:

  1. 兄弟節點是紅色
  2. 兄弟節點是黑色,兄弟節點的孩子都是黑色。父親節點顏色不確定
  3. 兄弟節點是黑色,兄弟節點的其中一個孩子是紅色。父親節點顏色不確定

3.1、後繼右孩子的兄弟節點是紅色

因為兄弟節點是紅色,那麼父親節點必定為黑。此時對父親節點進行旋轉和變色操作即可把兄弟節點變成黑色,也就是情況1可以轉化成情況2和情況3。

image-20230216214552436

以上圖為例,假設節點0為後繼右孩子節點,10為兄弟節點。以父親節點5進行左旋+變色可以把節點0的兄弟節點變成黑色。此時轉化為3.2或3.3的場景,這個旋轉+變色操作並不會使黑色節點數量發生變化,需要繼續執行後續場景的操作。

3.2、後繼右孩子的兄弟節點是黑色,兄弟節點的孩子都是黑色

可以把兄弟節點設置成紅色,相當於提取了兄弟和自己的一層黑色給父親,因為無法確定父親節點的顏色,需要繼續以父親節點作為檢查節點重新規則檢查。

3.2、後繼右孩子的兄弟節點是黑色,兄弟節點的其中一個孩子是紅色

加上後繼節點的顏色是父親節點的左孩子,兄弟節點的右孩子是紅色。此時以父親節點進行左旋,把父親節點和兄弟節點的右孩子的顏色也變成黑色,此時相當於後繼節點這邊的子樹多了一個黑色節點,且兄弟節點這邊的黑色節點數量不變,規則5被修複。

image-20230216221336469

旋轉後把原來的父親節點5染黑,兄弟節點10的顏色改成和原父親節點5一樣的顏色。白色的節點表示顏色不確定

如果兄弟節點的左孩子是紅色,可以先以兄弟節點進行右旋,這樣就轉換成上面的“兄弟節點的右孩子是紅色”的情況了。

image-20230216220811017

旋轉兄弟節點,把情況轉換“兄弟節點的右孩子是紅色”。白色的節點表示顏色不確定

4、刪除操作的偽代碼

//node的屬性color、left、right,parent分別為節點的顏色、節點的左子節點、節點的右子節點、節點的父親
//枚舉RED為紅,BLACK為黑
//leftRotate為左旋函數
//rightRotate為左旋函數
//Root為根節點的指針
//nodeParent為後繼右節點的父節點,因為後繼右節點可能為空,需要傳入它的新父節點
//nodeParent為後繼右節點
//調用fixDeleteRule前已經確認後繼節點是黑色了
fixDeleteRule(nodeParent,node){
    while(node != ROOT and node.color == BLACK){
        if(node == nodeParent.left){
            //如果節點是父親節點的左孩子
            borther = nodeParent.right;//兄弟節點
            if(borther.color == RED){
                borther.color = black;
                nodeParent.color = RED;
                leftRotate(nodeParent);
                borther = nodeParent.left;
            }
            
            if(borther == null or (borther.left.color == BLACK and borther.right.color == BLACK)){
                borther.color = RED;
                //顏色被提取到父親節點上了,設置新的檢查節點為父親節點
                node = nodeParent;
                nodeParent = node.parent;
            }else{
                if(borther.left.color == RED){
                    borther.color = RED;
                    borther.left.color = BLACK;
                    rightRotate(borther);
                }
                //剩下的情況必定是兄弟節點的右孩子為紅色
                borther.color = nodeParent.color;
                nodeParent.color = BLACK;
                borther.right.color = BLACK;
                leftRotate(nodeParent);
                //規則已經修複,設置為根指針
                node = Root;
            }
        }else{
            //node是nodeParent的右孩子的情況,代碼略...
        }
    }
    //有可能node變成了根節點,需要修複一下顏色
    node.color = BLACK;
}

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

-Advertisement-
Play Games
更多相關文章
  • HTMLCollection 和 Nodelist 的異同? 1. w3 關於這兩者的定義 HTMLCollection: An HTMLCollection is a list of nodes. An individual node may be accessed by either ordin ...
  • 原文地址:我的稀土掘金 預設打包: 所有文件都放在了assets文件夾 期望: css,js.img等等進行歸類 解決辦法 vite.config.js build:{ rollupOptions:{ output:{ chunkFileNames: 'static/js/[name]-[hash] ...
  • 一 發新版本導致 問題的根源是伺服器js文件更新了,頁面還在請求以前的js文件。可以保留之前webpack打包的文件,但是時間久了文件體積會積累到很大,而且從產品角度更希望用戶訪問新的資源。所以最好的解決方式是在報錯時給用戶提示,用戶點擊確認後刷新頁面。前端如何能catch到這種錯誤? 目前還沒找到 ...
  • 直接上重點。 如果是定位不准,Web瀏覽器端, 1,要使用者必須要做個人認證或者企業認證,且通過審核。 2,請求的網頁必須是https協議。 3,請求的功能變數名稱必須是加入到應用的Referer白名單。進入到應用設置里查看。 4,申請的應用類型必須是瀏覽器端。且必須勾選對應的服務。 有時申請地圖服務的人和 ...
  • 談到java中的併發,我們就避不開線程之間的同步和協作問題,談到線程同步和協作我們就不能不談談jdk中提供的AbstractQueuedSynchronizer(翻譯過來就是抽象的隊列同步器)機制; (一)、AQS中的state和Node含義: AQS中提供了一個int volatile state ...
  • 組件設計是通過對功能及視覺表達中元素的拆解、歸納、重組,並基於可被覆用的目的,形成規範化的組件,通過多維度組合來構建整個設計方案,將這些組件整理在一起,便形成組件庫。本文我們主要講述基於Vant CLI的自建組件庫。Vant CLI 是一個基於 Vite 實現的 Vue 組件庫構建工具,通過 Van... ...
  • 進位之間的轉換 1.1 電腦硬體的基本認知 cpu: 中央處理器. 相當於人的大腦.運算中心,控制中心. 記憶體: 臨時存儲數據. 優點:讀取速度快。 缺點:容量小,造價高,斷電即消失. 硬碟: 長期存儲數據. 優點:容量大,造價相對低,斷電不消失。 缺點:讀取速度慢. 操作系統:統一管理電腦軟硬 ...
  • 前言 在app中經常會有發送公告的需求,告知用戶一些重大的事情。本文將使用FA重置版和qq收藏的筆記功能完成遠程公告的功能。 遠程公告的思路 在qq收藏新建筆記,設置好公告內容 分享筆記給好友,拿到外部鏈接地址 FA發送http請求,解析出公告內容 在qq收藏新建筆記,設置好公告內容 點擊頭像,在點 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...