主頁 > 軟體設計 > 紅黑樹(C++實作)

紅黑樹(C++實作)

2021-12-07 11:14:37 軟體設計

文章目錄

  • 紅黑樹的概念
  • 紅黑樹的性質
  • 紅黑樹結點的定義
  • 紅黑樹的插入
  • 紅黑樹的驗證
  • 紅黑樹的查找
  • 紅黑樹的洗掉
  • 紅黑樹與AVL樹的比較

紅黑樹的概念

紅黑樹是一種二叉搜索樹,但在每個結點上增加了一個存盤位用于表示結點的顏色,這個顏色可以是紅色的,也可以是黑色的,因此我們稱之為紅黑樹,

紅黑樹通過對任何一條從根到葉子的路徑上各個結點著色方式的限制,確保沒有一條路徑會比其他路徑長出兩倍,因此紅黑樹是近似平衡的,
在這里插入圖片描述

紅黑樹的性質

紅黑樹有以下五點性質:

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

紅黑樹如何確保從根到葉子的最長可能路徑不會超過最短可能路徑的兩倍?

根據紅黑樹的性質3可以得出,紅黑樹當中不會出現連續的紅色結點,而根據性質4又可以得出,從某一結點到其后代葉子結點的所有路徑上包含的黑色結點的數目是相同的,

我們假設在紅黑樹中,從根到葉子的所有路徑上包含的黑色結點的個數都是 N N N個,那么最短路徑就是全部由黑色結點構成的路徑,即長度為 N N N
在這里插入圖片描述
而最長可能路徑就是由一黑一紅結點構成的路徑,該路徑當中黑色結點與紅色結點的數目相同,即長度為 2 N 2N 2N
在這里插入圖片描述
因此,紅黑樹從根到葉子的最長可能路徑不會超過最短可能路徑的兩倍,

紅黑樹結點的定義

我們這里直接實作KV模型的紅黑樹,為了方便后序的旋轉操作,將紅黑樹的結點定義為三叉鏈結構,除此之外還新加入了一個成員變數,用于表示結點的顏色,

template<class K, class V>
struct RBTreeNode
{
	//三叉鏈
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;

	//存盤的鍵值對
	pair<K, V> _kv;

	//結點的顏色
	int _col; //紅/黑

	//建構式
	RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _col(RED)
	{}
};

在這里我們可以使用列舉來定義結點的顏色,這樣可以增加代碼的可讀性和可維護性,并且便于后序的除錯操作,

//列舉定義結點的顏色
enum Colour
{
	RED,
	BLACK
};

為什么構造結點時,默認將結點的顏色設定為紅色?

當我們向紅黑樹插入結點時,若我們插入的是黑色結點,那么插入路徑上黑色結點的數目就比其他路徑上黑色結點的數目多了一個,即破壞了紅黑樹的性質4,此時我們就需要對紅黑樹進行調整,

若我們插入紅黑樹的結點是紅色的,此時如果其父結點也是紅色的,那么表明出現了連續的紅色結點,即破壞了紅黑樹的性質3,此時我們需要對紅黑樹進行調整;但如果其父結點是黑色的,那我們就無需對紅黑樹進行調整,插入后仍滿足紅黑樹的要求,

總結一下:

  • 插入黑色結點,一定破壞紅黑樹的性質4,必須對紅黑樹進行調整,
  • 插入紅色結點,可能破壞紅黑樹的性質3,可能對紅黑樹進行調整,

權衡利弊后,我們在構造結點進行插入時,默認將結點的顏色設定為紅色,

紅黑樹的插入

紅黑樹插入結點的邏輯分為三步:

  1. 按二叉搜索樹的插入方法,找到待插入位置,
  2. 將待插入結點插入到樹中,
  3. 若插入結點的父結點是紅色的,則需要對紅黑樹進行調整,

其中前兩步與二叉搜索樹插入結點時的邏輯相同,紅黑樹的關鍵在于第三步對紅黑樹的調整,

紅黑樹在插入結點后是如何調整的?

實際上,在插入結點后并不是一定會對紅黑樹進行調整,若插入結點的父結點是黑色的,那么我們就不用對紅黑樹進行調整,因為本次結點的插入并沒有破壞紅黑樹的五點性質,

只有當插入結點的父結點是紅色時才需要對紅黑樹進行調整,因為我們默認插入的結點就是紅色的,如果插入結點的父結點也是紅色的,那么此時就出現了連續的紅色結點,因此需要對紅黑樹進行調整,

因為插入結點的父結點是紅色的,說明父結點不是根結點(根結點是黑色的),因此插入結點的祖父結點(父結點的父結點)就一定存在,

紅黑樹調整時具體應該如何調整,主要是看插入結點的叔叔(插入結點的父結點的兄弟結點),根據插入結點叔叔的不同,可將紅黑樹的調整分為三種情況,

情況一:插入結點的叔叔存在,且叔叔的顏色是紅色,

此時為了避免出現連續的紅色結點,我們可以將父結點變黑,但為了保持每條路徑黑色結點的數目不變,因此我們還需要將祖父結點變紅,再將叔叔變黑,這樣一來既保持了每條路徑黑色結點的數目不變,也解決了連續紅色結點的問題,
在這里插入圖片描述
但調整還沒有結束,因為此時祖父結點變成了紅色,如果祖父結點是根結點,那我們直接再將祖父結點變成黑色即可,此時相當于每條路徑黑色結點的數目都增加了一個,

但如果祖父結點不是根結點的話,我們就需要將祖父結點當作新插入的結點,再判斷其父結點是否為紅色,若其父結點也是紅色,那么又需要根據其叔叔的不同,進而進行不同的調整操作,

因此,情況一的抽象圖表示如下:
在這里插入圖片描述
注意: 叔叔存在且為紅時,cur結點是parent的左孩子還是右孩子,調整方法都是一樣的,

情況二:插入結點的叔叔存在,且叔叔的顏色是黑色,

這種情況一定是在情況一繼續往上調整的程序中出現的,即這種情況下的cur結點一定不是新插入的結點,而是上一次情況一調整程序中的祖父結點,如下圖:
在這里插入圖片描述
我們將路徑中祖父結點之上黑色結點的數目設為 x x x,將叔叔結點之下黑色結點的數目設為 y y y,則在插入結點前,圖示兩條路徑黑色結點的數目分別為 x + 1 x+1 x+1 x + 2 + y x+2+y x+2+y,很明顯 x + 2 + y x+2+y x+2+y 是一定大于 x + 1 x+1 x+1的,因此在插入結點前就不滿足紅黑樹的要求了,所以說叔叔結點存在且為黑這種情況,一定是由情況一往上調整程序中才會出現的一種情況,

需要注意:

  1. 從根結點一直走到空位置就算一條路徑,而不是從根結點走到左右結點均為空的葉子結點時才算一條路徑,
  2. 情況二和情況三均需要進行旋轉處理,旋轉處理后無需繼續往上進行調整,所以說情況二一定是由情況一往上調整的程序中出現的,

出現叔叔存在且為黑時,單純使用變色已經無法處理了,這時我們需要進行旋轉處理,若祖孫三代的關系是直線(cur、parent、grandfather這三個結點為一條直線),則我們需要先進行單旋操作,再進行顏色調整,顏色調整后這棵被旋轉子樹的根結點是黑色的,因此無需繼續往上進行處理,

抽象圖表示如下:
在這里插入圖片描述
說明一下: 當直線關系為,parent是grandfather的右孩子,cur是parent的右孩子時,就需要先進行左單旋操作,再進行顏色調整,

若祖孫三代的關系是折現(cur、parent、grandfather這三個結點為一條折現),則我們需要先進行雙旋操作,再進行顏色調整,顏色調整后這棵被旋轉子樹的根是黑色的,因此無需繼續往上進行處理,

抽象圖表示如下:
在這里插入圖片描述
說明一下: 當折現關系為,parent是grandfather的右孩子,cur是parent的左孩子時,就需要先進行右左雙旋操作,再進行顏色調整,

情況三:插入結點的叔叔不存在,

在這種情況下的cur結點一定是新插入的結點,而不可能是由情況一變化而來的,因為叔叔不存在說明在parent的下面不可能再掛黑色結點了,如下圖:
在這里插入圖片描述
如果插入前parent下面再掛黑色結點,就會導致圖中兩條路徑黑色結點的數目不相同,而parent是紅色的,因此parent下面自然也不能掛紅色結點,所以說這種情況下的cur結點一定是新插入的結點,

和情況二一樣,若祖孫三代的關系是直線(cur、parent、grandfather這三個結點為一條直線),則我們需要先進行單旋操作,再進行顏色調整,顏色調整后這棵被旋轉子樹的根結點是黑色的,因此無需繼續往上進行處理,

抽象圖表示如下:
在這里插入圖片描述
說明一下: 當直線關系為,parent是grandfather的右孩子,cur是parent的右孩子時,就需要先進行左單旋操作,再進行顏色調整,

若祖孫三代的關系是折現(cur、parent、grandfather這三個結點為一條折現),則我們需要先進行雙旋操作,再進行顏色調整,顏色調整后這棵被旋轉子樹的根是黑色的,因此無需繼續往上進行處理,

抽象圖表示如下:
在這里插入圖片描述
說明一下: 當折現關系為,parent是grandfather的右孩子,cur是parent的左孩子時,就需要先進行右左雙旋操作,再進行顏色調整,

代碼如下:

//插入函式
pair<Node*, bool> Insert(const pair<K, V>& kv)
{
	if (_root == nullptr) //若紅黑樹為空樹,則插入結點直接作為根結點
	{
		_root = new Node(kv);
		_root->_col = BLACK; //根結點必須是黑色
		return make_pair(_root, true); //插入成功
	}
	//1、按二叉搜索樹的插入方法,找到待插入位置
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (kv.first < cur->_kv.first) //待插入結點的key值小于當前結點的key值
		{
			//往該結點的左子樹走
			parent = cur;
			cur = cur->_left;
		}
		else if (kv.first > cur->_kv.first) //待插入結點的key值大于當前結點的key值
		{
			//往該結點的右子樹走
			parent = cur;
			cur = cur->_right;
		}
		else //待插入結點的key值等于當前結點的key值
		{
			return make_pair(cur, false); //插入失敗
		}
	}

	//2、將待插入結點插入到樹中
	cur = new Node(kv); //根據所給值構造一個結點
	Node* newnode = cur; //記錄新插入的結點(便于后序回傳)
	if (kv.first < parent->_kv.first) //新結點的key值小于parent的key值
	{
		//插入到parent的左邊
		parent->_left = cur;
		cur->_parent = parent;
	}
	else //新結點的key值大于parent的key值
	{
		//插入到parent的右邊
		parent->_right = cur;
		cur->_parent = parent;
	}

	//3、若插入結點的父結點是紅色的,則需要對紅黑樹進行調整
	while (parent&&parent->_col == RED)
	{
		Node* grandfather = parent->_parent; //parent是紅色,則其父結點一定存在
		if (parent == grandfather->_left) //parent是grandfather的左孩子
		{
			Node* uncle = grandfather->_right; //uncle是grandfather的右孩子
			if (uncle&&uncle->_col == RED) //情況1:uncle存在且為紅
			{
				//顏色調整
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;

				//繼續往上處理
				cur = grandfather;
				parent = cur->_parent;
			}
			else //情況2+情況3:uncle不存在 + uncle存在且為黑
			{
				if (cur == parent->_left)
				{
					RotateR(grandfather); //右單旋

					//顏色調整
					grandfather->_col = RED;
					parent->_col = BLACK;
				}
				else //cur == parent->_right
				{
					RotateLR(grandfather); //左右雙旋

					//顏色調整
					grandfather->_col = RED;
					cur->_col = BLACK;
				}
				break; //子樹旋轉后,該子樹的根變成了黑色,無需繼續往上進行處理
			}
		}
		else //parent是grandfather的右孩子
		{
			Node* uncle = grandfather->_left; //uncle是grandfather的左孩子
			if (uncle&&uncle->_col == RED) //情況1:uncle存在且為紅
			{
				//顏色調整
				uncle->_col = parent->_col = BLACK;
				grandfather->_col = RED;

				//繼續往上處理
				cur = grandfather;
				parent = cur->_parent;
			}
			else //情況2+情況3:uncle不存在 + uncle存在且為黑
			{
				if (cur == parent->_left)
				{
					RotateRL(grandfather); //右左雙旋

					//顏色調整
					cur->_col = BLACK;
					grandfather->_col = RED;
				}
				else //cur == parent->_right
				{
					RotateL(grandfather); //左單旋

					//顏色調整
					grandfather->_col = RED;
					parent->_col = BLACK;
				}
				break; //子樹旋轉后,該子樹的根變成了黑色,無需繼續往上進行處理
			}
		}
	}
	_root->_col = BLACK; //根結點的顏色為黑色(可能被情況一變成了紅色,需要變回黑色)
	return make_pair(newnode, true); //插入成功
}

//左單旋
void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	Node* parentParent = parent->_parent;

	//建立subRL與parent之間的聯系
	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;

	//建立parent與subR之間的聯系
	subR->_left = parent;
	parent->_parent = subR;

	//建立subR與parentParent之間的聯系
	if (parentParent == nullptr)
	{
		_root = subR;
		_root->_parent = nullptr;
	}
	else
	{
		if (parent == parentParent->_left)
		{
			parentParent->_left = subR;
		}
		else
		{
			parentParent->_right = subR;
		}
		subR->_parent = parentParent;
	}
}

//右單旋
void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	Node* parentParent = parent->_parent;

	//建立subLR與parent之間的聯系
	parent->_left = subLR;
	if (subLR)
		subLR->_parent = parent;

	//建立parent與subL之間的聯系
	subL->_right = parent;
	parent->_parent = subL;

	//建立subL與parentParent之間的聯系
	if (parentParent == nullptr)
	{
		_root = subL;
		_root->_parent = nullptr;
	}
	else
	{
		if (parent == parentParent->_left)
		{
			parentParent->_left = subL;
		}
		else
		{
			parentParent->_right = subL;
		}
		subL->_parent = parentParent;
	}
}

//左右雙旋
void RotateLR(Node* parent)
{
	RotateL(parent->_left);
	RotateR(parent);
}

//右左雙旋
void RotateRL(Node* parent)
{
	RotateR(parent->_right);
	RotateL(parent);
}

注意: 在紅黑樹調整后,需要將根結點的顏色變為黑色,因為紅黑樹的根結點可能在情況一的調整程序中被變成了紅色,

紅黑樹的驗證

紅黑樹也是一種特殊的二叉搜索樹,因此我們可以先獲取二叉樹的中序遍歷序列,來判斷該二叉樹是否滿足二叉搜索樹的性質,

代碼如下:

//中序遍歷
void Inorder()
{
	_Inorder(_root);
}
//中序遍歷子函式
void _Inorder(Node* root)
{
	if (root == nullptr)
		return;
	_Inorder(root->_left);
	cout << root->_kv.first << " ";
	_Inorder(root->_right);
}

但中序有序只能證明是二叉搜索樹,要證明二叉樹是紅黑樹還需驗證該二叉樹是否滿足紅黑樹的性質,

代碼如下:

//判斷是否為紅黑樹
bool ISRBTree()
{
	if (_root == nullptr) //空樹是紅黑樹
	{
		return true;
	}
	if (_root->_col == RED)
	{
		cout << "error:根結點為紅色" << endl;
		return false;
	}
	
	//找最左路徑作為黑色結點數目的參考值
	Node* cur = _root;
	int BlackCount = 0;
	while (cur)
	{
		if (cur->_col == BLACK)
			BlackCount++;
		cur = cur->_left;
	}

	int count = 0;
	return _ISRBTree(_root, count, BlackCount);
}
//判斷是否為紅黑樹的子函式
bool _ISRBTree(Node* root, int count, int BlackCount)
{
	if (root == nullptr) //該路徑已經走完了
	{
		if (count != BlackCount)
		{
			cout << "error:黑色結點的數目不相等" << endl;
			return false;
		}
		return true;
	}

	if (root->_col == RED&&root->_parent->_col == RED)
	{
		cout << "error:存在連續的紅色結點" << endl;
		return false;
	}
	if (root->_col == BLACK)
	{
		count++;
	}
	return _ISRBTree(root->_left, count, BlackCount) && _ISRBTree(root->_right, count, BlackCount);
}

紅黑樹的查找

紅黑樹的查找函式與二叉搜索樹的查找方式一模一樣,邏輯如下:

  1. 若樹為空樹,則查找失敗,回傳nullptr,
  2. 若key值小于當前結點的值,則應該在該結點的左子樹當中進行查找,
  3. 若key值大于當前結點的值,則應該在該結點的右子樹當中進行查找,
  4. 若key值等于當前結點的值,則查找成功,回傳對應結點,

代碼如下:

//查找函式
Node* Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (key < cur->_kv.first) //key值小于該結點的值
		{
			cur = cur->_left; //在該結點的左子樹當中查找
		}
		else if (key > cur->_kv.first) //key值大于該結點的值
		{
			cur = cur->_right; //在該結點的右子樹當中查找
		}
		else //找到了目標結點
		{
			return cur; //回傳該結點
		}
	}
	return nullptr; //查找失敗
}

紅黑樹的洗掉

紅黑樹的洗掉要比插入更加難以理解,但是只要仔細一點也還行,

第一步:找到實際待洗掉的結點

找結點的程序與二叉搜索樹尋找待洗掉結點的方法一樣,若找到的待洗掉結點的左右子樹均不為空,則需要使用替換法進行洗掉,因此我們最終需要洗掉的都是左右子樹至少有一個為空的結點,

找到實際待洗掉結點后,先不洗掉該結點,否則調整紅黑樹時不容易控制,找到實際待洗掉結點后立即進行紅黑樹的調整,

第二步:調整紅黑樹

調整紅黑樹之前,我們先判斷一下本次結點的洗掉是否會破壞了紅黑樹的性質,若破壞了我們才需要對紅黑樹進行調整,

若實際洗掉的結點是紅色結點,那么本次洗掉操作不會破壞紅黑樹的性質,因此我們不需要對紅黑樹進行調整,反之,若洗掉的結點是黑色結點,我們就需要對紅黑樹進行調整,因為黑色結點的洗掉將會使得一些路徑中黑色結點的數目減少,此時便破壞了紅黑樹的性質四,

我們先來說最簡單的一種情況,即待洗掉結點只有一個孩子為空的情況,

在這種情況下,待洗掉結點要么是只有左孩子,要么是有只右孩子,但不管是左孩子還是右孩子,這個孩子一定是紅色的,因為若這個孩子是黑色的,那么此時圖示長藍色路徑的黑色結點數目比短藍色路徑的黑色結點數目多,不符合紅黑樹的性質,
在這里插入圖片描述
又因為紅黑樹當中不允許出現連續的紅色結點,因此在這種情況下實際上就只有圖示兩種實際情況,這時我們直接將待洗掉結點的那個紅孩子變成黑色就行了,因為在后面實際洗掉結點時會將這個孩子連接到洗掉結點的父結點下面,連接后相當于我們洗掉的是一個紅色結點,紅黑樹調整完成,

下面再來說比較復雜的情況,即待洗掉結點的左右孩子均為空,

我們以待洗掉結點是其父結點的左孩子為例,分為以下四種情況:

圖示說明:

  1. 若parent結點為白色,表明parent結點可能是紅色結點也可能是黑色結點,
  2. 若bL或bR結點為白色,表明其可能是紅色結點或黑色結點甚至該結點不存在,
  3. bL和bR結點為黑色時,表明他們可能是黑色結點或該結點不存在,

情況一:brother為紅色,
在這里插入圖片描述
當待洗掉結點的brother為紅色時,我們先以parent為旋轉點進行一次左單旋,再將brother的顏色變為黑色,將parent的顏色變為紅色,此時我們再對待洗掉結點cur進行情況分析,情況一就轉換成了情況二、三或四,

情況二:brother為黑色,且其左右孩子都是黑色結點或為空,
在這里插入圖片描述
在該情況下,我們直接將brother的顏色變成紅色,此時根據parent的顏色決定紅黑樹的調整是否結束,若parent的顏色是紅色,則我們將parent變為黑色后即可結束紅黑樹的調整;若parent的顏色原本就是黑色,則我們需要將parent結點當作下一次調整時的cur結點進行情況分析,并且情況二在下一次調整時可能會是情況一、二、三、四當中的任何一種,

情況三:brother為黑色,且其左孩子是紅色結點,右孩子是黑色結點或為空,
在這里插入圖片描述
出現該情況時,我們先以brother為旋轉點進行一次右單旋,再將brother結點變為紅色,將brotherLeft變為黑色,此時我們再對待洗掉結點cur進行情況分析,情況三就轉換成了情況四,

情況四:brother為黑色,且其右孩子是紅色結點,
在這里插入圖片描述
經過情況四的處理后,紅黑樹就一定調整結束了,在情況四當中,我們先以parent為旋轉點進行一次左單旋,然后將parent的顏色賦值給brother,再將parent的顏色變為黑色,最后將brotherRight變為黑色,此時紅黑樹的調整便結束了,

說明一下:

  1. 待洗掉結點是其父結點的右孩子時的四種情況與上面四種情況類似,這里就不列舉出來了,
  2. 若待洗掉結點沒有父結點,即待洗掉結點是根結點時,在找到該結點時就進行了洗掉,這里不用考慮,具體看代碼,

這里有必要對各種情況的切換進行說明,你可能會擔心調整紅黑樹時在這四種情況當中一直來回切換而不能跳出,下面我們來對此進行分析:
在這里插入圖片描述
首先,進入情況四后紅黑樹就一定調整結束了,其次,進入情況三后,下次也一定會進入情況四,紅黑樹的調整也會結束,所以情況三和情況四是沒有問題的,你們最糾結的只能是情況一和情況二了,

情況一又會切換為情況二、三、四,因此只要情況二能夠有辦法退出,那么所有情況就都能退出了,

在情況二當中我們說,如果parent的顏色是紅色,那么我們將parent變為黑色后就可以結束紅黑樹的調整,那會不會每次進入情況二時parent的顏色都不是紅色,而一直是黑色的呢?

當然有可能,但是我們若一直往上進行調整時,那么總會調整到紅黑樹的根結點,當調整到根結點后我們便不用進行調整了,此時根結點雖然是黑色的,但是不影響,這僅僅意味著每條從根到葉子的路徑上包含的黑色結點的個數都增加了一個,此時也沒有破壞紅黑樹的性質,也就完成了紅黑樹的調整,因此在調整程序中不會出現一直在這四種情況來回切換而不能跳出的問題,

第三步:進行結點的實際洗掉

在紅黑樹調整完畢后,我們就可以進行結點的洗掉了,洗掉結點的方式很簡單,若待洗掉結點有左孩子或右孩子,我們將其左孩子或右孩子連接到待洗掉結點父結點的下面即可,之后便可以將待洗掉結點洗掉了,

代碼如下:

//洗掉函式
bool Erase(const K& key)
{
	//用于遍歷二叉樹
	Node* parent = nullptr;
	Node* cur = _root;
	//用于標記實際的待洗掉結點及其父結點
	Node* delParentPos = nullptr;
	Node* delPos = nullptr;
	while (cur)
	{
		if (key < cur->_kv.first) //所給key值小于當前結點的key值
		{
			//往該結點的左子樹走
			parent = cur;
			cur = cur->_left;
		}
		else if (key > cur->_kv.first) //所給key值大于當前結點的key值
		{
			//往該結點的右子樹走
			parent = cur;
			cur = cur->_right;
		}
		else //找到了待洗掉結點
		{
			if (cur->_left == nullptr) //待洗掉結點的左子樹為空
			{
				if (cur == _root) //待洗掉結點是根結點
				{
					_root = _root->_right; //讓根結點的右子樹作為新的根結點
					if (_root)
					{
						_root->_parent = nullptr;
						_root->_col = BLACK; //根結點為黑色
					}
					delete cur; //洗掉原根結點
					return true;
				}
				else
				{
					delParentPos = parent; //標記實際洗掉結點的父結點
					delPos = cur; //標記實際洗掉的結點
				}
				break; //進行紅黑樹的調整以及結點的實際洗掉
			}
			else if (cur->_right == nullptr) //待洗掉結點的右子樹為空
			{
				if (cur == _root) //待洗掉結點是根結點
				{
					_root = _root->_left; //讓根結點的左子樹作為新的根結點
					if (_root)
					{
						_root->_parent = nullptr;
						_root->_col = BLACK; //根結點為黑色
					}
					delete cur; //洗掉原根結點
					return true;
				}
				else
				{
					delParentPos = parent; //標記實際洗掉結點的父結點
					delPos = cur; //標記實際洗掉的結點
				}
				break; //進行紅黑樹的調整以及結點的實際洗掉
			}
			else //待洗掉結點的左右子樹均不為空
			{
				//替換法洗掉
				//尋找待洗掉結點右子樹當中key值最小的結點作為實際洗掉結點
				Node* minParent = cur;
				Node* minRight = cur->_right;
				while (minRight->_left)
				{
					minParent = minRight;
					minRight = minRight->_left;
				}
				cur->_kv.first = minRight->_kv.first; //將待洗掉結點的key改為minRight的key
				cur->_kv.second = minRight->_kv.second; //將待洗掉結點的value改為minRight的value
				delParentPos = minParent; //標記實際洗掉結點的父結點
				delPos = minRight; //標記實際洗掉的結點
				break; //進行紅黑樹的調整以及結點的實際洗掉
			}
		}
	}
	if (delPos == nullptr) //delPos沒有被修改過,說明沒有找到待洗掉結點
	{
		return false;
	}

	//記錄待洗掉結點及其父結點(用于后續實際洗掉)
	Node* del = delPos;
	Node* delP = delParentPos;

	//調整紅黑樹
	if (delPos->_col == BLACK) //洗掉的是黑色結點
	{
		if (delPos->_left) //待洗掉結點有一個紅色的左孩子(不可能是黑色)
		{
			delPos->_left->_col = BLACK; //將這個紅色的左孩子變黑即可
		}
		else if (delPos->_right) //待洗掉結點有一個紅色的右孩子(不可能是黑色)
		{
			delPos->_right->_col = BLACK; //將這個紅色的右孩子變黑即可
		}
		else //待洗掉結點的左右均為空
		{
			while (delPos != _root) //可能一直調整到根結點
			{
				if (delPos == delParentPos->_left) //待洗掉結點是其父結點的左孩子
				{
					Node* brother = delParentPos->_right; //兄弟結點是其父結點的右孩子
					//情況一:brother為紅色
					if (brother->_col == RED)
					{
						delParentPos->_col = RED;
						brother->_col = BLACK;
						RotateL(delParentPos);
						//需要繼續處理
						brother = delParentPos->_right; //更新brother(否則在本回圈中執行其他情況的代碼會出錯)
					}
					//情況二:brother為黑色,且其左右孩子都是黑色結點或為空
					if (((brother->_left == nullptr) || (brother->_left->_col == BLACK))
						&& ((brother->_right == nullptr) || (brother->_right->_col == BLACK)))
					{
						brother->_col = RED;
						if (delParentPos->_col == RED)
						{
							delParentPos->_col = BLACK;
							break;
						}
						//需要繼續處理
						delPos = delParentPos;
						delParentPos = delPos->_parent;
					}
					else
					{
						//情況三:brother為黑色,且其左孩子是紅色結點,右孩子是黑色結點或為空
						if ((brother->_right == nullptr) || (brother->_right->_col == BLACK))
						{
							brother->_left->_col = BLACK;
							brother->_col = RED;
							RotateR(brother);
							//需要繼續處理
							brother = delParentPos->_right; //更新brother(否則執行下面情況四的代碼會出錯)
						}
						//情況四:brother為黑色,且其右孩子是紅色結點
						brother->_col = delParentPos->_col;
						delParentPos->_col = BLACK;
						brother->_right->_col = BLACK;
						RotateL(delParentPos);
						break; //情況四執行完畢后調整一定結束
					}
				}
				else //delPos == delParentPos->_right //待洗掉結點是其父結點的左孩子
				{
					Node* brother = delParentPos->_left; //兄弟結點是其父結點的左孩子
					//情況一:brother為紅色
					if (brother->_col == RED) //brother為紅色
					{
						delParentPos->_col = RED;
						brother->_col = BLACK;
						RotateR(delParentPos);
						//需要繼續處理
						brother = delParentPos->_left; //更新brother(否則在本回圈中執行其他情況的代碼會出錯)
					}
					//情況二:brother為黑色,且其左右孩子都是黑色結點或為空
					if (((brother->_left == nullptr) || (brother->_left->_col == BLACK))
						&& ((brother->_right == nullptr) || (brother->_right->_col == BLACK)))
					{
						brother->_col = RED;
						if (delParentPos->_col == RED)
						{
							delParentPos->_col = BLACK;
							break;
						}
						//需要繼續處理
						delPos = delParentPos;
						delParentPos = delPos->_parent;
					}
					else
					{
						//情況三:brother為黑色,且其右孩子是紅色結點,左孩子是黑色結點或為空
						if ((brother->_left == nullptr) || (brother->_left->_col == BLACK))
						{
							brother->_right->_col = BLACK;
							brother->_col = RED;
							RotateL(brother);
							//需要繼續處理
							brother = delParentPos->_left; //更新brother(否則執行下面情況四的代碼會出錯)
						}
						//情況四:brother為黑色,且其左孩子是紅色結點
						brother->_col = delParentPos->_col;
						delParentPos->_col = BLACK;
						brother->_left->_col = BLACK;
						RotateR(delParentPos);
						break; //情況四執行完畢后調整一定結束
					}
				}
			}
		}
	}
	//進行實際洗掉
	if (del->_left == nullptr) //實際洗掉結點的左子樹為空
	{
		if (del == delP->_left) //實際洗掉結點是其父結點的左孩子
		{
			delP->_left = del->_right;
			if (del->_right)
				del->_right->_parent = delP;
		}
		else //實際洗掉結點是其父結點的右孩子
		{
			delP->_right = del->_right;
			if (del->_right)
				del->_right->_parent = delP;
		}
	}
	else //實際洗掉結點的右子樹為空
	{
		if (del == delP->_left) //實際洗掉結點是其父結點的左孩子
		{
			delP->_left = del->_left;
			if (del->_left)
				del->_left->_parent = delP;
		}
		else //實際洗掉結點是其父結點的右孩子
		{
			delP->_right = del->_left;
			if (del->_left)
				del->_left->_parent = delP;
		}
	}
	delete del; //實際洗掉結點
	return true;
}

紅黑樹與AVL樹的比較

紅黑樹和AVL樹都是高效的平衡二叉樹,增刪查改的時間復雜度都是 O ( l o g N ) O(logN) O(logN),但紅黑樹和AVL樹控制二叉樹平衡的方式不同:

  • AVL樹是通過控制左右高度差不超過1來實作二叉樹平衡的,實作的是二叉樹的嚴格平衡,
  • 紅黑樹是通過控制結點的顏色,從而使得紅黑樹當中最長可能路徑不超過最短可能路徑的2倍,實作的是近似平衡,

相對于AVL樹來說,紅黑樹降低了插入結點時需要進行的旋轉的次數,所以在經常進行增刪的結構中性能比AVL樹更優,實際運用時也大多用的是紅黑樹,

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/374853.html

標籤:其他

上一篇:前端資料傳輸過去是亂碼,怎么辦?

下一篇:期末復習筆記——樹和二叉樹

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more