紅黑樹(RBTree)

紅黑樹

  • 1. 紅黑樹的概念
  • 2. 紅黑樹的性質
  • 3. 紅黑樹節點的定義
  • 4. 紅黑樹的插入操作
  • 5. 紅黑樹與AVL樹的比較

1. 紅黑樹的概念

紅黑樹是一種自平衡二叉查找樹,是在計算機科學中用到的一種數據結構,典型用途是實現關聯數組。它在1972年由魯道夫·貝爾發明,被稱為「對稱二叉B樹」,它現代的名字源于Leo J. Guibas和羅伯特·塞奇威克于1978年寫的一篇論文。紅黑樹的結構復雜,但它的操作有著良好的最壞情況運行時間,并且在實踐中高效:它可以在O(log n)時間內完成查找、插入和刪除,這里的n是樹中元素的數目。紅黑樹是每個節點都帶有顏色屬性的二叉查找樹,顏色為紅色或黑色。在二叉查找樹強制一般要求以外,對于任何有效的紅黑樹我們增加了如下的額外要求:節點是紅色或黑色。 根是黑色。 所有葉子都是黑色(葉子是NIL節點)。 每個紅色節點必須有兩個黑色的子節點。 從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點。
在紅黑樹中,所有的葉子結點都是黑色的空節點(NIL節點)。NIL節點是紅黑樹額外引入的結點,在計算紅黑數高度時NIL結點也會被計算在內。NIL結點指的是葉結點空的左右子結點延伸出來的的結點,也包括了葉子節點本身。
在這里插入圖片描述

2. 紅黑樹的性質

  1. 每個結點不是紅色就是黑色 ;
  2. 根節點是黑色的 ;
  3. 如果一個節點是紅色的,則它的兩個孩子結點是黑色的 ;
  4. 對于每個結點,從該結點到其所有后代葉結點的簡單路徑上,均包含相同數目的黑色結點 ;
  5. 每個葉子結點都是黑色的( 此處的葉子結點指的是空結點(NIL節點) )。

這些約束確保了紅黑樹的關鍵特性從根到葉子的最長的可能路徑不多于最短的可能路徑的兩倍長。結果是這個樹大致上是平衡的。 因為操作比如插入、刪除和查找某個值的最壞情況時間都要求與樹的高度成比例,這個在高度上的理論上限允許紅黑樹在最壞情況下都是高效的,而不同于普通的二叉查找樹。

3. 紅黑樹節點的定義

// 節點的顏色
enum Color{RED, BLACK};
// 紅黑樹節點的定義
template<class K, class V>
struct RBTreeNode
{RBTreeNode<K, V>* _left;   // 節點的左孩子RBTreeNode<K, V>* _right;  // 節點的右孩子RBTreeNode<K, V>* _parent; // 節點的雙親(紅黑樹需要旋轉,為了實現簡單給出該字段)pair<K, V> _kv;            // 節點的值Colour _col;               // 節點的顏色RBTreeNode(const pair<K, V>& kv):_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _col(RED)  //默認為紅節點{}
};

插入紅色節點樹的性質可能不會改變,而插入黑色節點每次都會違反性質4,所以 將節點設置為紅色在插入時對紅黑樹造成的影響是小的,而黑色的影響是最大的

4. 紅黑樹的插入操作

紅黑樹是在二叉搜索樹的基礎上加上其平衡限制條件,因此紅黑樹的插入可分為兩步:

  1. 按照二叉搜索的樹規則插入新節點;
  2. 檢測新節點插入后,紅黑樹的性質是否造到破壞,如過性質被破壞,就要進行旋轉或變色的操作,此時需要對紅黑樹分情況來討論:
    以下為父節點在祖父的左邊
    對于紅黑樹的插入來說,如果插入的父節點為黑,并且插入的節點默認為紅節點,所以如果父節點為黑色時,插入是不需要進行調節的,如圖1->2:

在這里插入圖片描述
有2->3圖可以看出cur、p兩個紅節點相連,所以需要調整,這種情況計為情況一:cur為紅,p為紅,g為黑,u存在且為紅。(約定:cur為當前節點,p為父節點,g為祖父節點,u為叔叔節點)
unlce節點的作用: 在紅黑樹中,uncle節點是指當前節點的父節點的兄弟節點。它的作用是在插入新節點時,如果當前節點的父節點和uncle節點都是紅色,那么需要將當前節點的父節點和uncle節點染成黑色,將當前節點的祖父節點染成紅色,然后以祖父節點為當前節點進行下一輪操作。這樣可以保證紅黑樹的性質4(每個紅色節點必須有兩個黑色的子節點)不被破壞。
對圖3進行調整,需要保證紅色節點不能相連,并且從該結點到其所有后代葉結點的簡單路徑上,均包含相同數目的黑色結點。如果只將cur改為黑色,那么違反規則4;將cur和uncle改為黑色,也會違反規則四,因為如果這棵樹是一個子樹,那么g結點的左右路徑是會多一個黑色結點。那么如何調整?
解決方式:將p,u改為黑,g改為紅,然后把g當成cur,繼續向上調整(因為g的父節點可能為紅色)。 這樣保證了每條路徑上的黑色結點的個數都相同。
對于變色來說,不論cur在parent的左邊還是右邊,變色后各個節點的位置都沒有改變,所以不需要分類討論。
代碼如下:

while (parent && parent->_col == RED)
{Node* grandfather = parent->_parent;if (grandfather->_left == parent){//情況1:uncle存在且為紅Node* uncle = grandfather->_right;if (uncle && uncle->_col == RED){grandfather->_col = RED;parent->_col = uncle->_col = BLACK;//繼續向上調整cur = grandfather;parent = cur->_parent;}}
}

cur為parent的左邊,如下圖又該怎樣調整?
在這里插入圖片描述

說明: u的情況有兩種
1.如果u節點不存在,則cur一定是新插入節點,因為如果cur不是新插入節點,則cur和p一定有一個節點的顏色是黑色,就不滿足性質4:每條路徑黑色節點個數相同;這時就需要旋轉+變色處理。
2.如果u節點存在,則其一定是黑色的,那么cur節點原來的顏色一定是黑色的,現在看到其是紅色的原因是因為cur的子樹在調整的過程中將cur節點的顏色由黑色改成紅色。(如上圖下半部分所示)
計上述情況為情況二: cur為紅,p為紅,g為黑,u不存在/u存在且為黑
對于上述兩種情況我們發現怎么變色都是不行的,所以可以旋轉+變色處理。旋轉的詳解如本篇博客(AVL樹)
p為g的左孩子,cur為p的左孩子,則進行右單旋轉;相反,p為g的右孩子,cur為p的右孩子,則進行左單旋轉
p、g變色–p變黑,g變紅

cur為parent的右邊,如下圖又該怎樣調整?
在這里插入圖片描述
上述情況記為情況三: cur為紅,p為紅,g為黑,u不存在/u存在且為黑
p為g的左孩子,cur為p的右孩子,則針對p做左單旋轉;相反,p為g的右孩子,cur為p的左孩子,則針對p做右單旋轉則轉換成了情況2。
如下圖進行旋轉+變色:
在這里插入圖片描述

代碼如下:

// 情況2和情況三3:u不存在/u存在且為黑,旋轉+變色//     g//   p   u// c 
//cur為父親節點的左邊
if (cur == parent->_left)
{RotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;
}
//cur為父親節點的右邊,在右邊,需要雙旋(如AVL樹中的旋轉)
else
{//     g//   p   u//    cRotateL(parent);RotateR(grandfather);cur->_col = BLACK;grandfather->_col = RED;
}

對于父節點在祖父節點的右邊,這種情況和上述大致相同,就不在多畫了,完整代碼如下。
RBTree.h文件中的代碼:

#pragma onceenum Colour
{RED,BLACK,
};template<class K, class V>
struct RBTreeNode
{RBTreeNode<K, V>* _left;   // 節點的左孩子RBTreeNode<K, V>* _right;  // 節點的右孩子RBTreeNode<K, V>* _parent; // 節點的雙親(紅黑樹需要旋轉,為了實現簡單給出該字段)pair<K, V> _kv;            // 節點的值Colour _col;               // 節點的顏色RBTreeNode(const pair<K, V>& kv):_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _col(RED){}
};template<class K, class V>
class RBTree
{typedef RBTreeNode<K, V> Node;
public:Node* Find(const K& key){Node* cur = _root;while (cur){if (cur->_kv.first > key){cur = cur->_left;}else if (cur->_kv.first < key){cur = cur->_right;}else{return cur;}}return nullptr;}bool Insert(const pair<K, V>& kv){if (_root == nullptr){_root = new Node(kv);_root->_col = BLACK;return true;}//尋找插入的位置Node* parent = nullptr;Node* cur = _root;while (cur){parent = cur;if (cur->_kv > kv){cur = cur->_left;}else if (cur->_kv < kv){cur = cur->_right;}else{return false;}}//插入新節點cur = new Node(kv);if (parent->_kv > kv){parent->_left = cur;}else{parent->_right = cur;}cur->_parent = parent;//進行調整這棵紅黑樹while (parent && parent->_col == RED){Node* grandfather = parent->_parent;//父節點在祖父的左邊if (grandfather->_left == parent){//情況1:uncle存在且為紅Node* uncle = grandfather->_right;if (uncle && uncle->_col == RED){grandfather->_col = RED;parent->_col = uncle->_col = BLACK;//繼續向上調整cur = grandfather;parent = cur->_parent;}else  //叔叔不存在或叔叔存在且為黑,旋轉+變黑{//cur為父親節點的左邊if (cur == parent->_left){//     g//   p   u// c RotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else  //cur為父親節點的右邊{//     g//   p   u//    c RotateL(parent);RotateR(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}//父節點在祖父的右邊else // (grandfather->_right == parent){//    g//  u   p//        cNode* uncle = grandfather->_left;// 情況1:u存在且為紅,變色處理,并繼續往上處理if (uncle && uncle->_col == RED){parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 繼續往上調整cur = grandfather;parent = cur->_parent;}else // 情況2+3:u不存在/u存在且為黑,旋轉+變色{//    g//  u   p//        cif (cur == parent->_right){RotateL(grandfather);grandfather->_col = RED;parent->_col = BLACK;}else{//    g//  u   p//     cRotateR(parent);RotateL(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}}//確保每次調整完后根節點為黑色_root->_col = BLACK;return true;}//因為紅黑樹的結點是new出來的,所以需要釋放,也就是需要寫析構函數~RBTree(){_Destroy(_root);_root = nullptr;}int Height(){return _Height(_root);}void InOrder(){_InOrder(_root);}
private:int _Height(Node* root){if (root == NULL)return 0;int leftH = _Height(root->_left);int rightH = _Height(root->_right);return leftH > rightH ? leftH + 1 : rightH + 1;}void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right);}void RotateL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL)subRL->_parent = parent;Node* ppnode = parent->_parent;subR->_left = parent;parent->_parent = subR;if (ppnode == nullptr){_root = subR;_root->_parent = nullptr;}else{if (ppnode->_left == parent){ppnode->_left = subR;}else{ppnode->_right = subR;}subR->_parent = ppnode;}}void RotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR)subLR->_parent = parent;Node* ppnode = parent->_parent;subL->_right = parent;parent->_parent = subL;if (parent == _root){_root = subL;_root->_parent = nullptr;}else{if (ppnode->_left == parent){ppnode->_left = subL;}else{ppnode->_right = subL;}subL->_parent = ppnode;}}void _Destroy(Node* root){if (root == nullptr)return;_Destroy(root->_left);_Destroy(root->_right);delete(root);}
private:Node* _root = nullptr;
};

測試代碼:

#include <iostream>
using namespace std;#include "RBTree.h"void Test_RBTree1()
{int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14, 16, 3, 7, 11, 9, 26, 18, 14, 15 };RBTree<int, int> t1;for (auto e : a){t1.Insert(make_pair(e, e));}t1.InOrder();
}
int main()
{Test_RBTree1();return 0;
}

運行結果如下:
在這里插入圖片描述

5. 紅黑樹與AVL樹的比較

1.紅黑樹和AVL樹都是高效的平衡二叉樹,增刪改查的時間復雜度都是O(log N),但它們的實現方式不同。
2.紅黑樹的查詢性能略微遜色于AVL樹,因為其比AVL樹會稍微不平衡最多一層,也就是說紅黑樹的查詢性能只比相同內容的AVL樹最多多一次比較
3.紅黑樹不追求絕對平衡,其只需保證最長路徑不超過最短路徑的2倍,紅黑樹在插入和刪除上優于AVL樹,AVL樹每次插入刪除會進行大量的平衡度計算,而紅黑樹為了維持紅黑性質所做的紅黑變換和旋轉的開銷,相較于AVL樹為了維持平衡的開銷要小得多。 所以在經常進行增刪的結構中性能比AVL樹更優,而且紅黑樹實現比較簡單,所以實際運用中紅黑樹更多。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/38353.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/38353.shtml
英文地址,請注明出處:http://en.pswp.cn/news/38353.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【前端二次開發框架關于關閉eslint】

前端二次開發框架關于關閉eslint 方法一方法二方法三方法四&#xff1a;以下是若想要關閉項目中的部分代碼時&#xff1a; 方法一 在vue.config.js里面進行配置&#xff1a; module.exports {lintOnSave:false,//是否開啟eslint保存檢測 ,它的有效值為 true || false || err…

會一點stm32,只后是做嵌入式Linux還是轉JAVA?

選擇嵌入式Linux還是轉向JAVA&#xff0c;取決于你的興趣、職業規劃和就業市場的需求。以下是一些考慮因素&#xff1a;興趣和擅長&#xff1a;首先&#xff0c;你應該考慮自己對嵌入式Linux和JAVA的興趣和擅長程度。如果你對嵌入式系統、硬件交互和底層編程更感興趣&#xff0…

GPT-4 如何為我編寫測試

ChatGPT — 每個人都在談論它,每個人都有自己的觀點,玩起來很有趣,但我們不是在這里玩— 我想展示一些實際用途,可以幫助您節省時間并提高效率。 我在本文中使用GPT-4 動機 我們以前都見過這樣的情況——代碼覆蓋率不斷下降的項目——部署起來越來越可怕,而且像朝鮮一樣…

基于C#UI Automation自動化測試

步驟 UI Automation 只適用于&#xff0c;標準的win32和 WPF程序 需要添加對UIAutomationClient、 UIAutomationProvider、 UIAutomationTypes的引用 代碼 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.D…

C++11并發與多線程筆記(2)

C11并發與多線程筆記&#xff08;2&#xff09; 線程啟動、結束&#xff0c;創建線程多法、join&#xff0c;detach 1. 范例演示線程運行的開始1.1 創建一個線程&#xff1a;1.2 join1.3 datch1.4 joinable 2. 其他創建線程的方法2.1 用類 重載了函數調用運算符2.2 lambda表達式…

ubuntu 安裝 python3.9

一、 相關背景 之前在dockerfile里面一直使用的是python3.8&#xff08;忘記為什么選擇這個版本了&#xff09;&#xff0c;想用python3.9&#xff0c;因為覺得3.8有點老了&#xff0c;而且3.9一個重要的feature&#xff0c;是把list作為默認的類型&#xff0c;不需要從typing…

Redis數據結構——Redis簡單動態字符串SDS

定義 眾所周知&#xff0c;Redis是由C語言寫的。 對于字符串類型的數據存儲&#xff0c;Redis并沒有直接使用C語言中的字符串。 而是自己構建了一個結構體&#xff0c;叫做“簡單動態字符串”&#xff0c;簡稱SDS&#xff0c;比C語言中的字符串更加靈活。 SDS的結構體是這樣的…

Python-OpenCV中的圖像處理-視頻分析

Python-OpenCV中的圖像處理-視頻分析 視頻分析Meanshift算法Camshift算法光流 視頻分析 學習使用 Meanshift 和 Camshift 算法在視頻中找到并跟蹤目標對象: Meanshift算法 Meanshift 算法的基本原理是和很簡單的。假設我們有一堆點&#xff08;比如直方 圖反向投影得到的點&…

ASR 語音識別接口封裝和分析

這個文檔主要是介紹一下我自己封裝了 6 家廠商的短語音識別和實時流語音識別接口的一個包&#xff0c;以及對這些接口的一個對比。分別是&#xff0c;阿里&#xff0c;快商通&#xff0c;百度&#xff0c;騰訊&#xff0c;科大&#xff0c;字節。 zxmfke/asrfactory (github.c…

ubuntu 安裝 cuda

ubuntu 安裝 cuda 初環境與設備在官網找安裝方式 本篇文章將介紹ubuntu 安裝 CUDA Toolkit CUDA Toolkit 是由 NVIDIA&#xff08;英偉達&#xff09;公司開發的一個軟件工具包&#xff0c;用于支持并優化 GPU&#xff08;圖形處理器&#xff09;上的并行計算和高性能計算。它…

解析TCP/IP協議的分層模型

了解ISO模型&#xff1a;構建通信的藍圖 為了促進網絡應用的普及&#xff0c;國際標準化組織&#xff08;ISO&#xff09;引入了開放式系統互聯&#xff08;Open System Interconnect&#xff0c;OSI&#xff09;模型。這個模型包括了七個層次&#xff0c;從底層的物理連接到頂…

一、Dubbo 簡介與架構

一、Dubbo 簡介與架構 1.1 應用架構演進過程 單體應用&#xff1a;JEE、MVC分布式應用&#xff1a;SOA、微服務化 1.2 Dubbo 簡介一種分布式 RPC 框架&#xff0c;對專業知識&#xff08;序列化/反序列化、網絡、多線程、設計模式、性能優化等&#xff09;進行了更高層的抽象和…

ArcGIS Maps SDK for JavaScript系列之三:在Vue3中使用ArcGIS API加載三維地球

目錄 SceneView類的常用屬性SceneView類的常用方法vue3中使用SceneView類創建三維地球項目準備引入ArcGIS API創建Vue組件在OnMounted中調用初始化函數initArcGisMap創建Camera對象Camera的常用屬性Camera的常用方法 要在Vue 3中使用ArcGIS API for JavaScript加載和展示三維地…

【JavaSE】面向對象之封裝

封裝的概念 封裝是把過程和數據包圍起來&#xff0c;對數據的訪問只能通過已定義的接口。面向對象計算始于這個基本概念&#xff0c;即現實世界可以被描繪成一系列完全自治、封裝的對象&#xff0c;這些對象通過一個受保護的接口訪問其他對象。封裝是一種信息隱藏技術&#xff…

Java旋轉數組中的最小數字(圖文詳解版)

目錄 1.題目描述 2.題解 分析 具體實現 方法一&#xff08;遍歷&#xff09;&#xff1a; 方法二&#xff08;排序&#xff09;&#xff1a; 方法三&#xff08;二分查找&#xff09;&#xff1a; 1.題目描述 有一個長度為 n 的非降序數組&#xff0c;比如[1,2,3,4,5]&a…

Linux基礎

Linux 一、基礎01- 執行環境準備02- linux的版本分類02.1 內核版本02.2 發行版本02.3 內核和發行版本的區別: 03- 虛擬機安裝04- 啟動linux 二、系統操作05- 幫助命令05.1 man 幫助05.2 help 幫助05.2.1 內部命令05.2.2 外部命令 05.3 info 幫助 06- ls命令06.1 -r06.2 -rt06.3…

npm install 中 --save 和 --save-dev 是什么?

npm&#xff0c;全名 Node Package Manager&#xff0c;套件管理工具&#xff0c;package.json 會記下你在項目中安裝的所有套件。 假設在項目中安裝 lodash npm i --save lodash這樣在 dependencies 中會出現&#xff1a; 如果修改了導入方式&#xff1a; npm i --save-dev …

在Linux中對docker 一鍵安裝,本地安裝,無網絡安裝,

在Linux中對docker 一鍵安裝 前提先準備好安裝包 非常絲滑 首先先把需要準備的文件準備好&#xff0c;/package/base.tar 和 /package/docker-20.10.10.tgz包 這兩個文件包必須放在 /package目錄下 再和/package同級的目錄下再準備conf目錄&#xff0c;conf目錄下放docker.se…

Labview解決“重置VI:xxx.vi”報錯問題

文章目錄 前言一、程序框圖二、前面板三、問題描述四、解決辦法 前言 在程序關閉前面板的時候小概率型出現了 重置VI&#xff1a;xxx.vi 這個報錯&#xff0c;并且發現此時只能通過任務管理器殺掉 LabVIEW 進程才能退出&#xff0c;這里介紹一下解決方法。 一、程序框圖 程序…

特征選擇 | 遞歸特征消除算法篩選最優特征

特征選擇 | 遞歸特征消除算法篩選最優特征 目錄 特征選擇 | 遞歸特征消除算法篩選最優特征寫在前面常規方法算法原理結果分析參考資料 寫在前面 在實際應用中&#xff0c;特征選擇作為機器學習和數據挖掘領域的重要環節&#xff0c;對于提高模型性能和減少計算開銷具有關鍵影響…