演算法導論學習筆記(九):紅黑樹
前言
前面已經學完了二叉查詢樹,這是我們學習紅黑樹的基礎,必須要熟練掌握,不然學習紅黑樹會很吃力的。雖然前面
已經學習了二叉查詢樹,但感覺學習紅黑樹的時候還是沒那麼輕鬆。
紅黑樹是一類特殊的二叉查詢樹,是一顆平衡的二叉查詢樹,但只是接近平衡。它能保證在最壞情況下,基本的動態
集合操作的時間為O(lgN)。
我們知道在二叉查詢樹上執行的基本操作的時間和樹的高度成正比。對於一顆含有N個結點的完全二叉樹,在有些情
況下,可能會形成一個斜樹,這樣的話其高度就是結點個數,這樣時間複雜度為O(N),顯然我們並不希望出現這樣的
情況,所以上章中也有通過隨機插入結點來降低這個情況的出現,但還是不能保證這種情況的不出現。那麼這時,我
們的紅黑樹就派上用場了。它能保證在最壞情況下,基本的動態集合操作的時間為O(lgN)。
性質
一顆二叉查詢樹如果滿足下面的紅黑性質,則為一顆紅黑樹:
1、每個結點或是紅的,或是黑的
2、根結點是黑的
3、每個葉結點(NIL)是黑的
4、如果一個結點是紅的,那麼它的兩個子結點都是黑的
5、對每個結點,從該結點到其子孫結點的所有路徑上包含相同數目的黑結點
幾點說明:
上面提到的葉結點(NIL)和我們之前講樹的葉子結點是不同的。這裡的NIL是一個哨兵,是為了方便處理紅黑樹程式碼中
的邊界條件。
4
1 5
NIL NIL NIL NIL
像上面的二叉查詢樹,如果是一般的二叉查詢樹的話,那麼1和5就是葉子結點了,其指標域的左右孩子指標都指向
NULL。對於紅黑樹的話,四個NIL就是葉子結點了。還有就是根結點的p指標也是指向NIL的。
如果這裡的葉結點的概念沒搞清楚的話,對後面的理解以及程式碼的編寫都會造成干擾。
基本操作
旋轉
對紅黑樹的主要操作就是查詢、插入和刪除了。因為紅黑樹本身就是一顆二叉查詢樹,而且查詢操作不會對樹作修
改,所以查詢和二叉查詢樹是一樣的,直接遞迴即可。但插入和刪除對樹作了修改,這時新的二叉查詢樹可能已經不
是紅黑樹了,所以我們必須進行一些調整來使其保持紅黑樹的性質。 而為了保持這些性質,就要改變樹中某些結點的
顏色以及指標結構。
旋轉操作的示意圖如下:
上圖分別是左旋轉和右旋轉的操作。這裡我以左旋轉來說,右旋轉類似。這裡在結點x上做左旋轉,我們假設它的右
孩子是y不是NIL,x可以為樹內任意右孩子不是NIL的結點。左旋轉以x到y之間的鏈為"支軸"進行。它使y成為該子樹新
的根,x成為y的左孩子,而y的左孩子則成為x的右孩子。
不難看出,這樣的旋轉不但調整了樹的結構,而且可以使樹依然滿足二叉查詢樹的性質。所以我們使用這種旋轉來調
整紅黑樹插入或刪除結點後的指標結構。下面是左旋轉和右旋轉的程式碼;
void left_rotate (BiTree* T, node* x)
{
node* y = x->right;
x->right = y->left;
if (y->left != NIL)
y->left->p = x;
y->p = x->p;
if (x->p == NIL)
*T = y;
else if (x == x->p->left)
x->p->left = y;
else
x->p->right = y;
y->left = x;
x->p = y;
}
void right_rotate (BiTree* T, node* x)
{
node* y = x->left;
x->left = y->right;
if (y->right != NIL)
y->right->p = x;
y->p = x->p;
if (x->p == NIL)
*T = y;
else if (x == x->p->left)
x->p->left = y;
else
x->p->right = y;
y->right = x;
x->p = y;
}
插入
講完了旋轉操作,接下來就是紅黑樹基本操作中的主角了:插入和刪除。我們先從較簡單的插入操作開始。紅黑樹的
插入操作和我們前面的二叉查詢樹的插入是很像的,但也有一些區別。首先就是葉結點的不同了,當我們插入一個結
點z時,紅黑樹中要設定left[z]和right[z]為NIL,一般二叉查詢樹中設定left[z]和right[z]為NULL來保持正確的樹結構。我
們將新插入的結點顏色設定為紅色,這樣可能會違反某條紅黑性質,如果插入結點的父結點是黑色,那麼不會違反任
何一條紅黑性質,不用作調整。如果插入結點的父節點是紅色,顯然違反了性質4,所以最後我們需要調整新的樹的
結構,這通過函數RB_Insert_Fixup (BiTree* T, node* z)來完成。
當使用RB_Insert_Fixup (BiTree* T, node* z)來調整時,主要有6種情況。其中父結點是其祖父結點左右子時各有3種
情況,而且是對稱的,故這裡就以父結點是其祖父結點的左孩子來進行說明。
情況1:z的叔叔結點y是紅色的
如上圖所示:(a)圖z為右孩子,(b)圖z為左孩子
只有當p[z]和y都為紅色時,才會發生情況1,處理方法就是將p[z]和y著黑色,但這樣修改之後c到葉結點所經歷路徑的
所有黑結點個數增加了1,違反了紅黑性質5。為了保持性質5,將c著紅色即可。
情況2:z的叔叔y是黑色的,而且z是右孩子
情況3:z的叔叔y是黑色的,而且z是左孩子
可以看出,情況2、3都違反了性質4(如果一個結點是紅的,那麼它的兩個子結點都是黑的)。當我們遇到情況3時,違
反了性質4,以C點進行右旋轉,並修改結點顏色,便得到上圖中的第三個圖,符合性質4。當我們遇到情況2時,只要
以A結點進行左旋轉,便可得到情況3。
/*調整樹結構*/
void RB_Insert_Fixup (BiTree* T, node* z)
{
while (z->p->color == RED)
{
if (z->p == z->p->p->left) //父結點是其祖父結點的左孩子
{
node* y = z->p->p->right;
if (y->color == RED) //情況1
{
z->p->color = BLACK;
z->p->p->color = RED;
y->color = BLACK;
z = z->p->p;
}
else
{
if (z == z->p->right) //情況2
{
z = z->p;
left_rotate (T, z);
}
//情況3
z->p->color = BLACK;
z->p->p->color = RED;
right_rotate (T, z->p->p);
}
}
else if (z->p == z->p->p->right)
{
node* y = z->p->p->left;
if (y->color == RED)
{
z->p->color = BLACK;
z->p->p->color = RED;
y->color = BLACK;
z = z->p->p;
}
else
{
if (z == z->left)
{
z = z->p;
right_rotate (T, z);
}
z->p->color = BLACK;
z->p->p->color = RED;
left_rotate (T, z->p->p);
}
}
}
(*T)->color = BLACK;
}
/*插入結點*/
void RB_Insert (BiTree* T, node* z)
{
node* y = NIL;
node* x = *T;
while (x != NIL)
{
y = x;
if (z->key < x->key)
x = x->left;
else
x = x->right;
}
z->p = y;
if (y == NIL)
*T = z;
else if (z->key < y->key)
y->left = z;
else
y->right = z;
z->color = RED;
RB_Insert_Fixup (T, z);
}
刪除
接下來繼續講最難的刪除操作。刪除操作也是在二叉查詢樹的刪除操作基礎上進行小修改的。如果實際刪除的結點是
紅色的,肯定不會違反性質5,不需要對樹進行調整。當如果實際刪除的結點是黑色的話,刪除後違反性質5,這就需
要使用函式RB_Delete_Fixup (BiTree* T, node* z)來完成調整啦,其中z是實際刪除結點的子結點。
刪除操作會遇到4種情況(以z是p[z]的左孩子為例):
情況1:z的兄弟w是紅色的
情況2:z的兄弟w是黑色的,而且w的兩個孩子都是黑色的
情況3:z的兄弟w是黑色的,w的左孩子是紅色的,w的右孩子是黑色的
情況4:z的兄弟w是黑色的,而且w的右孩子是紅色的
下面來具體看看4種情況以及對應的解決辦法(下面的圖中x就是z)
情況1:z的兄弟w是紅色的
處理方案:改變w和p[z]的顏色,再對p[z]做一次左旋轉,而且紅黑性質得以繼續保持。這樣下來,x有了新的兄弟節點
w,這時我們已經將情況1轉為情況2、3或4了。
情況2:z的兄弟w是黑色的,而且w的兩個孩子都是黑色的
處理方案:因為結點B左邊黑色高度比右邊少1,只要將w變為紅色,就可以實現B結點右邊黑色高度減少1以滿足性質
5。
情況3:z的兄弟w是黑色的,w的左孩子是紅色的,w的右孩子是黑色的
處理方案:交換left[w](即C)和w的顏色,並對w進行右旋轉,從而得到情況4。
情況4:z的兄弟w是黑色的,而且w的右孩子是紅色的
處理方案:這裡要修改三個結點的顏色:w改為p[z](B)的顏色,p[z](B)改為黑色,right[z](E)改為黑色。之後對p[z]做一
次左旋轉。最後記得將z置為根。
上面所說的以z是p[z]的左孩子為例的四種情況加上以z是p[z]的右孩子一共八種並不是所有情況,上面只所以單獨講他
們是因為他們是對稱的。還有兩種其它情況。
1、y原來是根結點,而y的一個紅色的孩子成為了新的根。這種情況破壞了性質2,直接把根結點著黑色即可。
2、z和p[y]都是紅色的。這種情況破壞了性質2和性質4,直接把z著黑色即可。
void RB_Delete_Fixup (BiTree* T, node* z)
{
node* w;
while ((z != *T) && (z->color == BLACK))
{
if (z == z->p->left)
{
w = z->p->right;
if (w->color == RED) //情況1
{
w->color = BLACK;
z->p->color = RED;
left_rotate (T, z->p);
w = z->p->right;
}
if ((w->left->color == BLACK) && (w->right->color == BLACK)) //情況2
{
w->color = RED;
z = z->p;
}
else
{
if (w->right->color == BLACK) //情況3
{
w->color = RED;
w->left->color = BLACK;
right_rotate (T, w);
w = z->p->right;
}
//情況4
w->color = z->p->color;
z->p->color = BLACK;
w->right->color = BLACK;
left_rotate (T, z->p);
z = *T;
}
}
else if (z == z->p->right)
{
w = z->p->left;
if (w->color == RED)
{
w->color = BLACK;
z->p->color = RED;
right_rotate (T, z->p);
w = z->p->left;
}
if ((w->left->color == BLACK) && (w->right->color == BLACK))
{
w->color = RED;
z = z->p;
}
else
{
if (w->left->color == BLACK)
{
w->color = RED;
w->right->color = BLACK;
left_rotate (T, w);
w = z->p->left;
}
w->color = z->p->color;
z->p->color = BLACK;
w->left->color = BLACK;
right_rotate (T, z->p);
z = *T;
}
}
}
z->color = BLACK;
}
void RB_Delete (BiTree* T, BiTree z)
{
BiTree x, y;
if ((z->left == NIL) || (z->right == NIL))
y = z;
else
y = RB_Successor (z);
if (y->left != NIL)
x = y->left;
else
x = y->right;
x->p = y->p;
if (y->p == NIL)
*T = x;
else if (y == y->p->left)
y->p->left = x;
else
y->p->right = x;
if (y != z)
z->key = y->key;
if (y->color == BLACK)
RB_Delete_Fixup (T, x);
delete y;
}
完整測試程式碼
#include<iostream>
using namespace std;
#define RED 0
#define BLACK 1
typedef struct node
{
node* left;
node* right;
node* p;
int key;
bool color;
node (node* x, int k):left(x),right(x),p(x),key(k),color(BLACK){}
}*BiTree;
node* NIL = new node(NULL, -1);
void left_rotate (BiTree* T, node* x)
{
node* y = x->right;
x->right = y->left;
if (y->left != NIL)
y->left->p = x;
y->p = x->p;
if (x->p == NIL)
*T = y;
else if (x == x->p->left)
x->p->left = y;
else
x->p->right = y;
y->left = x;
x->p = y;
}
void right_rotate (BiTree* T, node* x)
{
node* y = x->left;
x->left = y->right;
if (y->right != NIL)
y->right->p = x;
y->p = x->p;
if (x->p == NIL)
*T = y;
else if (x == x->p->left)
x->p->left = y;
else
x->p->right = y;
y->right = x;
x->p = y;
}
BiTree RB_Minimum (BiTree x)
{
while (x->left != NIL)
x = x->left;
return x;
}
BiTree RB_Successor (BiTree x)
{
if (x->right != NIL)
return RB_Minimum (x->right);
BiTree y = x->p;
while ((y != NIL) && (x == y->right))
{
x = y;
y = y->p;
}
return y;
}
node* RB_Search (BiTree T, int key)
{
if ((T->key == -1) || (key == T->key))
return T;
if (key < T->key)
return RB_Search (T->left, key);
else
return RB_Search (T->right, key);
}
/*調整樹結構*/
void RB_Insert_Fixup (BiTree* T, node* z)
{
while (z->p->color == RED)
{
if (z->p == z->p->p->left) //父結點是其祖父結點的左孩子
{
node* y = z->p->p->right;
if (y->color == RED) //情況1
{
z->p->color = BLACK;
z->p->p->color = RED;
y->color = BLACK;
z = z->p->p;
}
else
{
if (z == z->p->right) //情況2
{
z = z->p;
left_rotate (T, z);
}
//情況3
z->p->color = BLACK;
z->p->p->color = RED;
right_rotate (T, z->p->p);
}
}
else if (z->p == z->p->p->right)
{
node* y = z->p->p->left;
if (y->color == RED)
{
z->p->color = BLACK;
z->p->p->color = RED;
y->color = BLACK;
z = z->p->p;
}
else
{
if (z == z->left)
{
z = z->p;
right_rotate (T, z);
}
z->p->color = BLACK;
z->p->p->color = RED;
left_rotate (T, z->p->p);
}
}
}
(*T)->color = BLACK;
}
/*插入結點*/
void RB_Insert (BiTree* T, node* z)
{
node* y = NIL;
node* x = *T;
while (x != NIL)
{
y = x;
if (z->key < x->key)
x = x->left;
else
x = x->right;
}
z->p = y;
if (y == NIL)
*T = z;
else if (z->key < y->key)
y->left = z;
else
y->right = z;
z->color = RED;
RB_Insert_Fixup (T, z);
}
void RB_Delete_Fixup (BiTree* T, node* z)
{
node* w;
while ((z != *T) && (z->color == BLACK))
{
if (z == z->p->left)
{
w = z->p->right;
if (w->color == RED) //情況1
{
w->color = BLACK;
z->p->color = RED;
left_rotate (T, z->p);
w = z->p->right;
}
if ((w->left->color == BLACK) && (w->right->color == BLACK)) //情況2
{
w->color = RED;
z = z->p;
}
else
{
if (w->right->color == BLACK) //情況3
{
w->color = RED;
w->left->color = BLACK;
right_rotate (T, w);
w = z->p->right;
}
//情況4
w->color = z->p->color;
z->p->color = BLACK;
w->right->color = BLACK;
left_rotate (T, z->p);
z = *T;
}
}
else if (z == z->p->right)
{
w = z->p->left;
if (w->color == RED)
{
w->color = BLACK;
z->p->color = RED;
right_rotate (T, z->p);
w = z->p->left;
}
if ((w->left->color == BLACK) && (w->right->color == BLACK))
{
w->color = RED;
z = z->p;
}
else
{
if (w->left->color == BLACK)
{
w->color = RED;
w->right->color = BLACK;
left_rotate (T, w);
w = z->p->left;
}
w->color = z->p->color;
z->p->color = BLACK;
w->left->color = BLACK;
right_rotate (T, z->p);
z = *T;
}
}
}
z->color = BLACK;
}
void RB_Delete (BiTree* T, BiTree z)
{
BiTree x, y;
if ((z->left == NIL) || (z->right == NIL))
y = z;
else
y = RB_Successor (z);
if (y->left != NIL)
x = y->left;
else
x = y->right;
x->p = y->p;
if (y->p == NIL)
*T = x;
else if (y == y->p->left)
y->p->left = x;
else
y->p->right = x;
if (y != z)
z->key = y->key;
if (y->color == BLACK)
RB_Delete_Fixup (T, x);
delete y;
}
int main()
{
BiTree T = NIL;
for (int i = 0; i < 20; i++)
{
int n;
cin >> n;
node* z = new node (NIL, n);
RB_Insert (&T, z);
}
for (int i = 0; i < 10; i++)
{
int m;
cin >> m;
node* x = RB_Search (T, m);
RB_Delete (&T, x);
}
return 0;
}
具體執行結果最好通過斷點除錯來觀察。
總結
對於紅黑樹的所有操作,都必須圍繞著其5個紅黑性質。當我們插入或刪除結點後,呼叫RB_Insert_Fixup (BiTree* T,
node* z)或RB_Delete_Fixup (BiTree* T, node* z)都是為了保持紅黑性質。
至於為什麼對於那些情況所採用的那些方式能有效保障紅黑性質,就我現在的水平還證明不出來,只是有個大概的理
解,只好先記錄下來各個情況下的解決方案,
相關推薦
演算法導論學習筆記(九):紅黑樹
前言 前面已經學完了二叉查詢樹,這是我們學習紅黑樹的基礎,必須要熟練掌握,不然學習紅黑樹會很吃力的。雖然前面 已經學習了二叉查詢樹,但感覺學習紅黑樹的時候還是沒那麼輕鬆。 紅黑樹是一類特殊的二叉查詢樹,是一顆平衡的二叉查詢樹,但只是接近平衡。它能保證在最壞情況下,基本的動態
TypeScript學習筆記(九):裝飾器(Decorators)
標註 時裝 als cal () 操作 enume 筆記 文檔 裝飾器簡介 裝飾器(Decorators)為我們在類的聲明及成員上通過元編程語法添加標註提供了一種方式。 需要註意的是:裝飾器是一項實驗性特性,在未來的版本中可能會發生改變。 若要啟用實驗性的裝飾器特性
java學習筆記(九):Java 流(Stream)、文件(File)和IO
用戶輸入 public 文件內容 輸出流 out 單個 java 我們 ready Java 的控制臺輸入由 System.in 完成。 為了獲得一個綁定到控制臺的字符流,你可以把 System.in 包裝在一個 BufferedReader 對象中來創建一個字符流。需要i
javaweb學習筆記(九):JavaScript(2)
目錄 1.BOM 1.1window物件 1.2history物件 1.3location物件 2. DOM 2.1dom節點及獲取 2.2Event 1.BOM 1.1window物件 一般來說,Window 物件的方法都是對瀏覽器視窗或框架進行某種
學習筆記(五):使用決策樹演算法檢測POP3暴力破解
1.資料蒐集 載入KDD 99中的資料: def load_kdd99(filename): x=[] with open(filename) asf: for line in f: line=line.st
學習筆記(九):使用支援向量機識別XSS
1.特徵化:提取特徵,對特徵進行向量化,標準化,均方差縮放,去均值操作 def get_len(url): return len(url) def get_url_count(url): if re.search('(http://)|(http://)',url,re.IGNO
機器學習筆記(九):Tensorflow 實戰一 (Tensorflow入門)
1 - TsensorFlow計算模型 ——計算圖 1.1- 計算圖的概念 計算圖是TensorFlow中最基本的一個概念,TensorFlow中的所有計算都會被轉化為計算圖上的節點。 在TensorFlow中,張量可以簡單地理解為多為陣列。如果說TensorFlow的第一個詞T
python 學習筆記(九): 資料庫壓測程式設計
這個程式碼是利用多執行緒多mysql資料庫批量插入資料,可用於mysql壓測 #!/usr/bin/python # -*- coding: utf-8 -*- from __future__ import print_function import argp
ARM aarch64彙編學習筆記(九):使用Neon指令(一)
NEON是一種基於SIMD思想的ARM技術。 SIMD, Single Instruction Multiple Data,是一種單條指令處理多個數據的並行處理技術,相比於一條指令處理一個數據,運算速度將會大大提高。 ARMv8 有31 個64位暫存器,1個不同
21天學通C++學習筆記(九):類和對象
行為 邏輯 在一起 編譯 特征 str 不能 的人 學習 1. 類和對象 現實中的人等事物往往具備一些特征並且可以做某些事情,要在程序中模擬這些事物,需要一個結構,將定義其屬性(數據)以及其可用這些屬性執行的操作(函數)整合在一起。這種結構就是類,而這種結構的每一個實例就
各種音視訊編解碼學習詳解之 編解碼學習筆記(九):QuickTime系列
最近在研究音視訊編解碼這一塊兒,看到@bitbit大神寫的【各種音視訊編解碼學習詳解】這篇文章,非常感謝,佩服的五體投地。奈何大神這邊文章太長,在這裡我把它分解成很多小的篇幅,方便閱讀。大神部落格傳送門:https://www.cnblogs.com/skyofbitbit/p/3651
機器學習筆記(九):聚類
有學者按照機器學習發生場景的不同,將機器學習劃分為三種正規化,它們分別是有監督學習、無監督學習與強化學習。有監督學習指的是用來訓練模型的資料是帶有標籤的,訓練過程可簡單概括為根據“資料帶有的標籤”與“模型產生的輸出”之間的誤差來調整模型的引數。無監督學習則適用於無標籤的資料集,它往往通過對訓練集進行
TensorFlow學習筆記(九):CIFAR-10訓練例子報錯解決
以下報錯主要是由於TensorFlow升級1.0後與以前程式碼不相容所致。 1.AttributeError: 'module' object has noattribute 'random_crop' 解決方案: 將distorted_image= tf.ima
Python3《機器學習實戰》學習筆記(九):支援向量機實戰篇之再撕非線性SVM
一 前言 上篇文章講解的是線性SVM的推導過程以及簡化版SMO演算法的程式碼實現。本篇文章將講解SMO演算法的優化方法以及非線性SVM。 二 SMO演算法優化 在幾百個點組成的小規模資料集上,簡化版SMO演算法的執行是沒有什麼問題
python OpenCV學習筆記(九):圖片的幾何變形
縮放 import numpy as np import cv2 as cv img = cv.imread('test.jpg') res = cv.resize(img, None, fx=2, fy=2, interpolation=
Cesium學習筆記(九):匯入3D模型(obj轉gltf)
在用cesium的過程中難免需要匯入別人做好的3D模型,這時候就需要將這些模型轉成gltf格式了 當然,官方也給了我們一個網頁版的轉換器,但是畢竟是網頁版的,效率極其低下,檔案還不能太大,所以我們就需要一個格式轉換器了 現在只支援obj和dae轉gltf
Java for Web學習筆記(九):Servlet(7)上傳檔案
上傳檔案 Servlet的引數設定 採用annotation方式如下: @WebServlet( name = "TicketServlet", urlPatterns = {"/tickets"}, loadOnStartup = 1 ) /* MultipartConfig配置了本Servlet
Kubernetes學習筆記(九):StatefulSet--部署有狀態的多副本應用
## StatefulSet如何提供穩定的網路標識和狀態 ReplicaSet中的Pod都是無狀態,可隨意替代的。又因為ReplicaSet中的Pod是根據模板生成的多副本,無法對每個副本都指定單獨的PVC。 來看一下StatefulSet如何解決的。 ### 提供穩定的網路標識 StatefulSet建
學習筆記(九)——數據庫存儲結構:頁、聚集索引、非聚集索引
分享 style end 宋體 blog lec storage rop cas 1、頁 SQL Server用8KB 的頁來存儲數據,並且在SQL Server裏磁盤 I/O 操作在頁級執行。也就是說,SQL Server 讀取或寫入所有數據頁。頁有不同的類型,像
學習筆記(二):使用K近鄰演算法檢測Web異常操作
使用全量比較,而不是最頻繁和最不頻繁的比較。 1.資料蒐集 我們使用詞集的模型,將全部命令去重後形成一個大型向量空間,每個命令代表一個特徵,首先通過遍歷全部命令,生成對應詞集。 with open(filename) as f: fo