C++高級數據結構算法 | RBTree(紅黑樹)

之前我們有介紹高級數據結構中的BST樹與AVL樹:
《C++高級數據結構算法 | Binary Search Tree(二叉查找樹)》
《C++高級數據結構算法 | AVL(自平衡二叉查找樹)》

但是,即使AVL樹擁有平衡特性,但它是通過其不斷的旋轉操作來實現的,最壞情況下將每回溯一層都要進行旋轉調整。即數據量大了以後,AVL樹的旋轉操作就拖慢了插入和刪除的時間。

爲此引入了紅黑樹,它具有良好的旋轉次數,不至於影響結點插入和刪除的時間效率。



紅黑樹的概念與重要性質

紅黑樹是一種近似平衡的二叉查找樹,它能夠確保任何一個節點的左右子樹的高度差不會超過二者中較低子樹的一倍。具體來說,紅黑樹是滿足如下條件的二叉查找樹(Binary Search Tree):

  • 性質1 : 每一個節點是要麼是紅色要麼是黑色
  • 性質2 : 根節點必須是黑色
  • 性質3 :葉子節點都是黑色(指葉子節點的地址域null爲黑色,一般null節點默認顏色是黑色)
  • 性質4 : 每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)
  • 性質5 : 從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點

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

什麼這些特性確保了這個結果?

我們注意到性質4要求任一路徑不能有兩個連續的紅色節點最短的可能路徑都是黑色節點,最長的可能路徑有交替的紅色和黑色節點。因爲根據性質4所有最長的路徑都有相同數目的黑色節點,這就表明了沒有路徑能多於任何其他路徑的兩倍長。

在進行紅黑樹的插入以及刪除操作的時候,會涉及節點的旋轉和重新着色問題,紅黑樹的旋轉次數要比AVL少很多,紅黑樹插入最多旋轉兩次,刪除最多旋轉三次。

例如C++ STL庫中的map,multimap,set,multiset等容器,Linux的虛擬內存管理,epoll 的內核實現都應用到了紅黑樹這種數據結構,其增刪查時間複雜度能達到 O(log2n)O(log_2n)


紅黑樹的結構定義

紅黑樹的定義比我們之前講解的AVL樹、BST樹要稍微複雜一點,因爲涉及到結點着色問題,因此我們需要爲每個結點定義顏色,並且由於插入刪除時我們需要知道結點的叔父結點(指該節點的父節點的兄弟節點),因此我們需要定義指針指向該結點的父節點

enum Color
{
	BLACK,RED
};

template<typename T>
class RBTree
{
public:
	RBTree() :_root(nullptr) {}
	
private:
	struct RBNode
	{
		RBNode(T data = T(),
			Color color = BLACK,
			RBNode* parent = nullptr)
			:_data(data)
			, _left(nullptr)
			, _right(nullptr)
			, _parent(parent)
			, _color(color)
		{}
		T _data;
		RBNode* _left;
		RBNode* _right;
		RBNode* _parent;
		Color _color;
	};
	RBNode* _root; // 指向紅黑樹的根節點
	
	// 獲取結點顏色
	Color color(RBNode* node)
	{
		return node == nullptr ? BLACK : node->_color;
	}
	
	// 設置結點顏色
	void setColor(RBNode* node, Color color)
	{
		node->_color = color;
	}
	
	// 獲取左孩子結點
	RBNode* left(RBNode* node)
	{
		return node->_left;
	}
	
	// 獲取右孩子結點
	RBNode* right(RBNode* node)
	{
		return node->_right;
	}
	
	// 獲取父節點
	RBNode* parent(RBNode* node)
	{
		return node->_parent;
	}
};

紅黑樹的旋轉操作

當查找樹的結構發生改變時,紅黑樹的條件可能被破壞,需要通過調整使得查找樹重新滿足紅黑樹的條件。調整可以分爲兩類:一類是顏色調整,即改變某個節點的顏色另一類是結構調整,即改變樹中結點的結構關係。結構調整過程包含兩個基本操作:左旋(Rotate Left),右旋(RotateRight)。

上述我們給出了紅黑樹的結構定義,儘管我們只添加了一個指向其父節點的指針域,但是對於紅黑樹的旋轉操作來說,就較爲複雜了,因爲我們需要對其父節點進行修改。


左旋轉操作

左旋轉操作的整體流程是與我們之前介紹的AVL樹的旋轉操作是相同的,我們唯一需要考慮的就是有關父節點的修改問題。
在這裏插入圖片描述
完整代碼如下:

/**
 * 左旋轉操作
 * 拿到當前結點的右孩子結點child,它即將成爲新的根節點
 * 1、那麼我們需要將原結點的父節點寫成新的根節點child的父節點
 * 	  當然我們也需要修改祖先結點的孩子結點,修改爲child結點
 *    我們需要判斷祖先結點是否爲空nullptr,若爲空,這說明原結點node
 *    就是樹的根節點,因此我們直接將指向根節點的指針root指向child即可
 *    若祖先結點不爲空,那麼我們就判斷原結點時祖先結點左孩子或右孩子
 *    最後將祖先結點的孩子結點置爲child即可。
 * 2、將child結點的左孩子(有可能爲nullptr)置爲原結點node的右孩子
 *    然後,我們需要判斷child結點是否有左孩子,如果有左孩子的話我們還要
 *    修改該左孩子的父節點爲node結點
 * 3、最後一步就是將node結點置爲新根結點child的左孩子
 *    當然還要更改一下node結點的父節點,其新的父節點就爲新根child。
 */

void leftRotate(RBNode* node)
{
	RBNode* child = node->_right;
	child->_parent = node->_parent;
	if (node->_parent == nullptr)
	{
		_root = child;
	}
	else
	{
		if (node->_parent->_left == node)
		{
			node->_parent->_left = child;
		}
		else
		{
			node->_parent->_right = child;
		}
	}

	node->_right = child->_left;
	if (child->_left != nullptr)
	{
		child->_left->_parent = node;
	}

	child->_left = node;
	node->_parent = child;
}

右旋轉操作

在這裏插入圖片描述

/**
 * 右旋轉操作
 * 右旋轉操作與左旋轉操作是成鏡像關係的,還是分3步:
 * 拿到當前結點的左孩子結點child,它即將成爲新的根節點
 * 1、將原結點的父節點寫成新的根節點child的父節點
 * 	  當然我們也需要修改祖先結點的孩子結點,修改爲child結點
 *    我們需要判斷祖先結點是否爲空nullptr,若爲空,這說明原結點node
 *    就是樹的根節點,因此我們直接將指向根節點的指針root指向child即可
 *    若祖先結點不爲空,那麼我們就判斷原結點時祖先結點左孩子或右孩子
 *    最後將祖先結點的孩子結點置爲child即可。
 * 2、將child結點的右孩子(有可能爲nullptr)置爲原結點node的左孩子
 *    然後,我們需要判斷child結點是否有右孩子,如果有右孩子的話我們還要
 *    修改該右孩子的父節點爲node結點
 * 3、最後一步就是將node結點置爲新根結點child的右孩子
 *    當然還要更改一下node結點的父節點,其新的父節點就爲新根child。
 */
void rightRotate(RBNode* node)
{
	RBNode* child = node->_left;
	child->_parent = node->_parent;
	if (node->_parent == nullptr)
	{
		_root = child;
	}
	else
	{
		if (node->_parent->_left == node)
		{
			node->_parent->_left = child;
		}
		else
		{
			node->_parent->_right = child;
		}
	}

	node->_left = child->_right;
	if (child->_right != nullptr)
	{
		child->_right->_parent = node;
	}

	child->_right = node;
	node->_parent = child;
}

紅黑樹的插入

我們要向紅黑樹中插入結點,若樹爲空,那麼我們將其插入到根節點位置,調整根節點指針,並將其着色成黑色,直接結束

否則,我們都是將紅色結點插入到樹中,因爲這樣不會影響路徑中黑色結點的數量改變,那麼如果其父節點是黑色的,那麼直接插入結束,因爲我們該結點的插入並沒有導致紅黑樹性質的改變,包括任一簡單的路徑的黑色結點數目相同,並且沒有兩個連續的紅色結點等。

但是,如果我們插入結點的父節點是紅色的,那麼就破壞了紅黑樹的性質,即不能出現連續的兩個紅色結點。此時,我們需要對紅黑樹進行結點結構調整,顏色調整等一系列插入修復操作。

整體來說,紅黑樹的插入的整體代碼架構和BST樹是相同的,我們修改的地方是,在插入完成後,我們判斷插入結點的父節點的顏色,若是黑色,則插入結束;若是紅色,則調用插入修複函數。


插入修複函數是紅黑樹的插入操作的核心。我們將其分爲三種情況:

情況一:如下左圖,插入結點(N)的叔叔結點(U)是紅色的。

修復操作:將父節點與叔叔結點都改爲黑色,將祖父結點改爲紅色,繼續向上檢查,直到父結點黑色結點停止。


情況二:叔叔結點爲黑色(或爲空),祖父結點、父節點、插入結點處在一條直線上。

修復操作:直接進行一個旋轉操作(左旋或右旋),把父節點置爲黑色,把祖父結點都置爲紅色。


情況三:叔叔結點爲黑色(或爲空),祖父結點、父節點、插入結點不在一條直線上。

修復操作:兩次旋轉操作(左-右旋轉 或 右-左旋轉),第一次針對父節點,第二次針對祖父結點,第二次旋轉與着色即爲情況二的情況。


下面是實現代碼:

首先是insert的函數,該函數與BST樹的非遞歸插入基本結構相同:

/**
 * 紅黑樹的插入
 * 樹不爲空,則插入的都是紅色結點
 * 插入完成後,需要判斷父節點的顏色,父節點是黑色,則插入結束
 * 父節點是紅色,則需要進行插入調整
 */
void insert(const T& val)
{
	if (_root == nullptr)
	{
		_root = new RBNode(val, BLACK);
		return;
	}

	RBNode* parent = nullptr;
	RBNode* cur = _root;
	while (cur != nullptr)
	{
		parent = cur;
		if (cur->_data > val)
		{
			cur = cur->_left;
		}
		else if (cur->_data < val)
		{
			cur = cur->_right;
		}
		else
		{
			return;
		}
	}

	// 以紅色結點插入到紅黑樹中
	RBNode* node = new RBNode(val, RED, parent);
	if (val < parent->_data)
	{
		parent->_left = node;
	}
	else
	{
		parent->_right = node;
	}
	
	// 父親結點爲紅色,紅黑樹性質被破壞,需要進行調整
	if (color(parent) == RED)
	{
		fixAfterInsert(node);
	}
}

/* 
 * 插入調整函數
 * 注意情況三的一次旋轉後就變爲了情況二
 */
void fixAfterInsert(RBNode* node)
{
	while (color(parent(node)) == RED)
	{
		// 插在了祖先節點的左子樹當中
		if (left(parent(parent(node))) == parent(node))
		{
			RBNode* uncle = right(parent(parent(node)));
			// 情況1 : 叔叔結點是紅色
			if (color(uncle) == RED)
			{
				setColor(parent(node), BLACK); // 父節點置黑色
				setColor(uncle, BLACK); // 叔叔節點置黑色
				setColor(parent(parent(node)), RED); // 祖父節點置紅色
				node = parent(parent(node)); // node指向祖父節點,繼續向根回溯
			}
			else
			{
				/* 情況3 : 叔叔結點是黑色,且該結點與其父親結點、		
				 * 祖父結點不在一條直線上
				 * 這裏我們爲了和後面情況二的代碼兼容,因此讓node
				 * 指向中間節點(父節點),進行旋轉操作後node指向
				 * 三個節點中最後一個結點。
				 */
				if (right(parent(node)) == node)
				{
					node = parent(node);
					leftRotate(node);// 以父節點爲根做左旋轉
				}

				/* 情況2 : 叔叔結點是黑色,且該結點與其父親結點、
				 * 祖父結點在一條直線上
				 */
				setColor(parent(node), BLACK); // 父節點置黑
				setColor(parent(parent(node)), RED); // 祖父結點置紅
				rightRotate(parent(parent(node))); // 對祖父節點做右旋轉
				break;
			}
		}
		else // 插在了祖先節點的右子樹當中,與上述過程是鏡像關係
		{
			RBNode* uncle = left(parent(parent(node)));
			// 情況1
			if (color(uncle) == RED)
			{
				setColor(parent(node), BLACK);
				setColor(uncle, BLACK);
				setColor(parent(parent(node)), RED);
				node = parent(parent(node));
			}
			else
			{
				// 情況3
				if (left(parent(node)) == node)
				{
					node = parent(node);
					rightRotate(parent(node));
				}

				// 情況2
				setColor(parent(node), BLACK);
				setColor(parent(parent(node)), RED);
				leftRotate(parent(parent(node)));
				break;
			}
		}
	}
	
	// 在調整的過程中有可能修改了根節點的顏色爲紅色,需要修改爲黑色
	setColor(_root, BLACK);
}

紅黑樹的刪除

紅黑樹的刪除操作是最爲複雜的操作,因爲涉及到需要調整的場景是比插入多的。

如果刪除的是紅色節點,直接刪除就可以,因爲刪除一個紅色結點不會改變紅黑樹的任何性質;如果刪除的是黑色節點,刪除完節點需要進行調整,因爲破壞了紅黑樹的性質,某一個分支路徑上的黑色節點少了一個。

刪除操作的總體思想是從兄弟結點借調黑色結點使樹保持局部的平衡,如果局部的平衡達到了,就看整體的樹是否是平衡的,如果不平衡就接着向上回溯調整。


下面我們默認調整點是父節點的左孩子,刪除主要分爲四種情況:

情況一 : 待刪除結點的兄弟節點爲紅色節點,只能做選擇調整成其他的情況。

由於我們無法借調一個黑色節點過來,但是兄弟節點的孩子節點肯定都是黑色的,我們可以進行一次旋轉操作,把黑色的兄弟節點提上來,就可以借調黑色節點了。

調整方案:把兄弟節點顏色改爲黑色,把父節點改爲紅色,然後以父節點爲根節點進行左旋。


情況二 :待刪除結點的兄弟節點是黑色且兄弟節點的左右孩子均爲黑色。

調整方案:直接把兄弟節點設置成紅色,然後從父節點開始繼續回溯調整。


情況三 :待刪除結點的兄弟節點是黑色且兄弟節點左孩子爲紅色右孩子爲黑色。

調整方案:將兄弟結點的左孩子與兄弟結點交換顏色,即將兄弟結點設置爲紅色,其左孩子顏色設置爲黑色,然後以兄弟結點爲根進行右旋操作。


情況四 :待刪除結點的兄弟節點是黑色且兄弟節點左孩子是任意顏色右孩子爲紅色。

我們可以發現情況四就是情況三調整之後的結果,因此我們代碼可以順序執行。

調整方案:將兄弟結點與其父結點交換顏色,將父節點設置爲黑色,將兄弟結點的右孩子設置爲黑色,以父節點爲根進行左旋轉操作。


完整代碼如下:

/**
 * 紅黑樹的刪除
 */
void remove(const T& val)
{
	if (_root == nullptr)
	{
		return;
	}
	
	/* 搜索待刪除結點位置 */
	RBNode* curNode = _root;
	while (curNode != nullptr)
	{
		if (curNode->_data > val)
		{
			curNode = curNode->_left;
		}
		else if (curNode->_data < val)
		{
			curNode = curNode->_right;
		}
		else
		{
			break;
		}
	}

	/* 沒有找到直接返回 */
	if (curNode == nullptr)
	{
		return;
	}

	/* 待刪除結點的左右孩子均不爲空,將前驅結點值賦給待刪除結點,刪除前驅 */
	if (curNode->_left != nullptr && curNode->_right != nullptr)
	{
		RBNode* preNode = curNode->_left;
		while (preNode->_right != nullptr)
		{
			preNode = preNode->_right;
		}
		curNode->_data = preNode->_data;
	}

	/* 待刪除結點至少有一個孩子結點,或沒有孩子結點 */
	RBNode* childNode = curNode->_left;
	if (childNode == nullptr)
	{
		childNode = curNode->_right;
	}
	
	/* 待刪除結點至少有一個孩子結點的情況,我們需要根據判斷條件修改父節點指針 */
	if (childNode != nullptr)
	{
		/* 將待刪除結點的孩子結點改爲待刪除結點的父節點 */
		childNode->_parent = curNode->_parent;
		if (curNode->_parent == nullptr)
		{
			_root = childNode;
		}
		else if (curNode->_parent->_left == curNode)
		{
			curNode->_parent->_left = childNode;
		}
		else
		{
			curNode->_parent->_right = childNode;

		}
		
		/* 待刪除結點爲黑色,需要進行結點調整 */
		if (curNode->_color == BLACK)
		{
			fixAfterRemove(childNode);
		}
	}
	else // 待刪除結點沒有孩子結點
	{
		/* 待刪除結點是否爲根節點 */
		if (curNode->_parent == nullptr)
		{
			_root = nullptr;
		}
		else
		{
			/* 待刪除結點爲黑色,需要進行結點調整 */
			if (curNode->_color == BLACK)
			{
				fixAfterRemove(curNode);
			}
			
			/* 進行紅黑樹的刪除調整完成後,把curNode節點刪除掉 */
			if (curNode->_parent->_left == curNode)
			{
				curNode->_parent->_left = nullptr;
			}
			else
			{
				curNode->_parent->_right = nullptr;
			}
		}
	}
}

/**
 * 紅黑樹的刪除調整
 */
void fixAfterRemove(RBNode* node)
{
	while (node != _root && node->_color == BLACK)
	{
		/* 調整點是父節點的左孩子 */
		if (node->_parent->_left == node)
		{
			/* 兄弟結點在父節點右邊,保存以備用 */
			RBNode* broNode = node->_parent->_right;
			/* 情況一:兄弟結點是紅色 */
			if (broNode->_color == RED)
			{
				broNode->_color = BLACK; // 兄弟結點置爲黑色
				node->_parent->_color = RED; // 父節點置爲紅色
				leftRotate(node->_parent); //以對父節點爲根進行左旋操作
				broNode = node->_parent->_right; // 更新兄弟結點
			}
			
			/* 情況二:兄弟結點爲黑色並且其左右孩子也是黑色 */
			if (broNode->_left->_color == BLACK
				&& broNode->_right->_color == BLACK)
			{
				broNode->_color = RED; // 將兄弟結點置爲紅色
				node = node->_parent; // 將node改爲其父親結點,繼續向根回溯
			}
			else
			{
				/* 情況三:兄弟結點爲黑色並且其左孩子爲紅色右孩子爲黑色 */
				if (broNode->_right->_color == BLACK)
				{
					broNode->_left->_color = BLACK; // 兄弟結點左孩子置爲黑色
					broNode->_color = RED; // 兄弟結點置爲紅色
					rightRotate(broNode); // 以兄弟結點爲根進行右旋轉
					broNode = node->_parent->_right; // 更新兄弟結點
				}
				
				/* 情況四:兄弟結點爲黑且右孩子爲紅色,左孩子可爲任意顏色*/
				broNode->_color = node->_parent->_color; // 兄弟結點與其父節點交換顏色
				node->_parent->_color = BLACK; // 兄弟結點的父節點置黑
				broNode->_right->_color = BLACK; // 兄弟結點的右孩子置黑
				leftRotate(node->_parent); // 以父節點爲根進行左旋操作
				
				//在執行完這一步後,一定會調整好,直接設置爲根節點下次退出
				node = _root;
			}
		}
		else /* 調整點是父節點的右孩子,具體操作時上面的鏡像 */
		{
		 	/* 兄弟結點在父節點左邊 */
			RBNode* broNode = node->_parent->_left;
			/* 情況一:兄弟結點是紅色 */
			if (broNode->_color == RED)
			{
				broNode->_color = BLACK;
				node->_parent->_color = RED;
				rightRotate(node->_parent);
				broNode = node->_parent->_left;
			}
			
			/* 情況二:兄弟結點爲黑色並且其左右孩子也是黑色 */
			if (broNode->_left->_color == BLACK
				&& broNode->_right->_color == BLACK)
			{
				broNode->_color = RED;
				node = node->_parent;
			}
			else
			{
				/* 情況三:兄弟結點爲黑色並且其左孩子爲紅色右孩子爲黑色 */
				if (broNode->_left->_color == BLACK)
				{
					broNode->_right->_color = BLACK;
					broNode->_color = RED;
					leftRotate(broNode);
					broNode = node->_parent->_left;
				}
				
				/* 情況四:兄弟結點爲黑且右孩子爲紅色,左孩子可爲任意顏色*/
				broNode->_color = node->_parent->_color;
				node->_parent->_color = BLACK;
				broNode->_left->_color = BLACK;
				rightRotate(node->_parent);
				node = _root;
			}
		}
	}
	
	 /**
      * 刪除黑色節點後,其孩子節點是紅色,上面循環無法進入
      * 我們直接將孩子結點直接調成黑色,即可保持黑色節點數量不變;
      * 刪除黑色節點後,其孩子節點是黑色,但是向上回溯的時候,
      * 遇到紅色節點,直接將其改成黑色節點即可。
      */
	node->_color = BLACK;
}

應用場景

紅黑樹往往出現由於樹的深度過大而造成磁盤IO讀寫過於頻繁,進而導致效率低下的情況在數據較小,可以完全放到內存中時,紅黑樹的時間複雜度比B樹低。反之,數據量較大,外存中佔主要部分時,B樹因其讀磁盤次數少,而具有更快的速度。

  • 著名的linux進程調度Completely Fair Scheduler,用紅黑樹管理進程控制塊;
  • epoll在內核中的實現,內核事件表就是一顆紅黑樹;
  • nginx中,用紅黑樹管理timer等;
  • Java的TreeMap實現;
  • 廣泛用在C++的STL中。map、multimap、set、multiset 都是用紅黑樹實現的

《從頭到尾的插入和刪除操作案例插圖》
《nginx 紅黑樹的實現》
《 “算法導論” 紅黑樹的代碼實現》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章