《数据结构、算法与应用 —— C++语言描述》学习笔记 — 平衡搜索树 — 红黑树

《数据结构、算法与应用 —— C++语言描述》学习笔记 — 平衡搜索树 — 红黑树

  • 一、基本概念
  • 二、红黑树操作
    • 1、红黑树的搜索
    • 2、红黑树的插入
      • (1)XYr类型不平衡
      • (2)XYb类型不平衡
    • 3、红黑树的删除
      • (1)Rb型
      • (2)Rr型
  • 三、实现
    • 1、AVL问题修改
      • (1)BST 修改
      • (2)AVL 修改
    • 2、节点类修改
    • 3、BST 交换后继接口修改
    • 4、RBTree 声明
    • 5、插入
    • 6、删除
    • 7、打印函数
    • 8、测试代码

一、基本概念

红黑树是这样的一棵二叉搜索树:树中每一个节点的颜色或者是红色或者是黑色。红-黑树的其他特征可以用相应的扩充二叉树来说明:
① 根节点和所有外部节点都是黑色。
② 在根至外部节点路径上,没有连续两个节点是红色。
③ 在所有根节点至外部节点的路径上,黑色节点的数目相同。

红黑树还有另一种等价,它取决于父子节点间的指针颜色。从父节点指向黑色孩子的指针是黑色,从父节点指向红色孩子的指针是红色。另外还有:
① 从内部节点指向外部节点的指针是黑色的。
② 在根至外部节点路径上,没有两个连续的红色指针。
③ 在所有根至外部节点路径上,黑色指针的数目都相同。
因此,如果我们知道指针的颜色,就能推断节点的颜色,反之亦然。

一棵红黑树的构造如图(把外部节点当做黑色节点):
《数据结构、算法与应用 —— C++语言描述》学习笔记 — 平衡搜索树 — 红黑树_第1张图片
令红黑树的一个节点的阶,是从该节点到一外部节点的路径上黑色指针的数量。因此,一个外部节点的阶是零,上图中根节点的阶是2。

根据上面的性质,我们可以得到以下两条定理:

① 设从根到外部节点的路径长度(length)是该路径上的指针数量。如果P和Q是红黑树中的两条从根至外部节点的路径,那么 l e n g t h ( P ) ≤ 2 l e n g t h ( Q ) length(P)\le2length(Q) length(P)2length(Q)

任何一条路径上的红色指针个数都不可能超过黑色指针个数,因此任何一条路径的长度都不可能是另一条路径长度的2倍以上。

② 令h是一棵红黑树的高度,n是树的内部节点数量,而r是根节点的阶,则:
(1) h ≤ 2 r h\le2r h2r
(2) h ≤ 2 r − 1 h\le2^r-1 h2r1
(3) h ≤ 2 log ⁡ 2 ( n + 1 ) h\le2\log_2(n+1) h2log2(n+1)

由于红黑树的高度最多为 2 log ⁡ 2 ( n + 1 ) 2\log_2(n+1) 2log2(n+1),所以,在O(h)时间内可以完成的搜索、插入和删除操作,其复杂度为 O(logn)。

二、红黑树操作

1、红黑树的搜索

我们使用普通二叉搜索树的搜索来实现红黑树的搜索。其时间复杂度为 O(logn)。

2、红黑树的插入

红黑树的插入使用普通二叉树的插入算法。对于插入的新元素,我们需要上色。如果插入前的树是空的,那么新节点是根节点,颜色应该是黑色。假设插入前的树是非空的。如果新节点的颜色是黑色,那么在它所属的从根到外部节点的路径上,黑色节点的数量将会发生变化。这导致特征③一定被破坏。如果新节点的颜色是红色,我们有可能出现两个相连的红色节点,也有可能没有。因此特征②有可能被保留。因此,我们将新节点赋为红色。

如果将新节点赋为红色引起了特征②被破坏,我们就说树的平衡被破坏了。不平衡的类型可以通过检查新节点 nn 的父亲 pp 的父亲 g 来确定。我们知道特征②被破坏意味着 np 一定都为红色节点。因为 p 是红色节点,所以它一定不是根节点。所以 g 节点必然存在,且为黑色。当 pg 的左孩子,np 的左孩子且 g 的另一个孩子是黑色时,我们称该不平衡是LLb型。其它的不平衡类型还包括LLr、LRb、LRr、RLb、RLr、RRb、RRr。

(1)XYr类型不平衡

XYr类型的不平衡可以通过改变节点颜色来处理。以LLr为例:我们需要将节点 p 的颜色变为黑色,将其兄弟节点的颜色变为红色;如果 g 不是根节点,我们还需要将其染为红色。这样以后,在以 g 节点为根的子树中,g 节点到外部节点的黑色节点数量没有发生变化。因此我们在保证了特征③的条件下,复原了 np 导致的不平衡。染色方式如下图:
《数据结构、算法与应用 —— C++语言描述》学习笔记 — 平衡搜索树 — 红黑树_第2张图片
不难看出,当 g 不是根节点时,由于其颜色会变为红色,这将可能引起新的双红缺陷。因此,以上的过程需要递归进行,直到到达根节点或者祖父节点的颜色变化不再引起失衡。

(2)XYb类型不平衡

XYb类型的不平衡可以通过旋转来处理。这种旋转的方式和AVL树类似,只是需要增加染色的处理。在旋转之后,需要将该子树新的根节点染为黑色,npg 中的另外两个节点染为红色。以LLb和LRb为例:
《数据结构、算法与应用 —— C++语言描述》学习笔记 — 平衡搜索树 — 红黑树_第3张图片
《数据结构、算法与应用 —— C++语言描述》学习笔记 — 平衡搜索树 — 红黑树_第4张图片
我们可以看出,整个旋转和染色过程,子树的根节点颜色没有变化,子树的根节点到该子树的所有外部节点的黑色节点数量没有发生变化。因此,这种变化不会引起祖先节点的失衡。

3、红黑树的删除

类似地,我们先使用普通二叉树的删除操作删除一个节点 n,然后修复其带来的不平衡。如果我们删除的是一个红色节点,显而易见,没有任何特征会被破坏,因此我们不需要做任何操作。如果我们删除的是一个黑色节点,且其后继节点 s 也为黑色,则会导致不平衡。当 n 是其父节点 p 的右孩子时,我们称不平衡是R型的,否则是L型的。不难分析出,这种情况下 n 的兄弟节点 v 一定不是外部节点(否则 n 本身也应该是外部节点或者红节点才能保证在 p 处左右子树黑节点长度相同)。如果 v 是黑色节点,则不平衡是Rb或Lb型的;如果是红色节点,则是Rr或Lr型的。下面我们以R型为例讨论各种情况。

(1)Rb型

根据 v 的子节点和 p 的颜色,我们可以将Rb型不平衡分为三种情况:
v 至少有一个红孩子。这种情况我们可以通过3+4重构来解决。重构所用的节点为 pvv 的任一红孩子节点(白色节点既可能为黑色,也可能为红色):
《数据结构、算法与应用 —— C++语言描述》学习笔记 — 平衡搜索树 — 红黑树_第5张图片
v 没有红孩子节点,p 为红色。此时我们不需要旋转,只需进行染色:
《数据结构、算法与应用 —— C++语言描述》学习笔记 — 平衡搜索树 — 红黑树_第6张图片
v 没有红孩子节点,p 为黑色。同样需要染色,不同的是,此时 p 子树的黑色节点深度会发生变化,进而可能会导致其祖先节点失衡。因此,此过程需要递归处理:
《数据结构、算法与应用 —— C++语言描述》学习笔记 — 平衡搜索树 — 红黑树_第7张图片

(2)Rr型

Rr型的不平衡可以通过一次旋转转化为Rb型不平衡,进而借助Rb型不平衡的方法解决问题。v 及其左孩子是满足黑色节点深度要求的,现在的失衡转化为 v 的右子树 p 的失衡,而其为Rb型失衡:
《数据结构、算法与应用 —— C++语言描述》学习笔记 — 平衡搜索树 — 红黑树_第8张图片

三、实现

1、AVL问题修改

(1)BST 修改

插入节点后应该将之返回用于重平衡处理:

template<typename Key, typename Value>
inline auto BinarySearchTree<Key, Value>::insertElementWithParent(const ValueType& element, NodeType* parentNode) -> NodeType*
{
	auto newNode = new NodeType(element, nullptr, nullptr, parentNode);
	...
	return newNode;
}

(2)AVL 修改

template<typename Key, typename Value>
inline void AVL<Key, Value>::insert(const ValueType& element)
{
	auto node = this->findNode(element.first);
	...
	node = this->insertElementWithParent(element, this->hotParent);
	this->treeSize++;
	// 	node = new NodeType(element, nullptr, nullptr, this->hotParent);
}

2、节点类修改

我们需要为节点类增加一个属性,表示节点的颜色;提供两个方法分别用于获取节点的左右孩子颜色。这种实现方式可以避免直接获取节点颜色时需要堆外部节点进行的特殊处理;增加了一个方法用于获取兄弟节点。因为在删除时对各种情况的分类取决于兄弟节点及其子节点的颜色。

template<typename T>
class binaryTreeNode
{
public:
	...
	enum Color
	{
		Red,
		Black,
	};
	int color;

	binaryTreeNode(const T& element, binaryTreeNode* leftChild = nullptr, binaryTreeNode* rightChild = nullptr, 
				   binaryTreeNode* parent = nullptr, int color = Black) :
		element(element)
	{
		...
		this->color = color;
	}
	...
}

template<typename T>
inline int binaryTreeNode<T>::leftChildColor()
{
	return leftChild ? leftChild->color : Black;
}

template<typename T>
inline int binaryTreeNode<T>::rightChildColor()
{
	return rightChild ? rightChild->color : Black;
}

template<typename T>
inline binaryTreeNode<T>* binaryTreeNode<T>::sibling()
{
	if (this->parent == nullptr)
	{
		return nullptr;
	}

	if (this == this->parent->leftChild)
	{
		return this->parent->rightChild;
	}

	return this->parent->leftChild;
}

这里我们可以发现,其实各种搜索树的节点类可以独立出来。我本来尝试通过继承原节点类实现红黑树的节点类。但是这需要在孩子操作时,先进行静态类型转换;除此之外,还要将析构函数声明为虚函数。因此,我们采取一种简单的实现方案。

3、BST 交换后继接口修改

需要在交换时保留原节点颜色。

template<typename Key, typename Value>
inline void BinarySearchTree<Key, Value>::swapNodeWithSuccessor(NodeType*& node)
{
	if (node->leftChild != nullptr && node->rightChild != nullptr)
	{
		...
		auto swapNode = new binaryTreeNode(successor->element, node->leftChild, node->rightChild, parent, node->color);
		...
	}
}

4、RBTree 声明

#pragma once
#include "../binarySearchTree/BinarySearchTree.h"
#include 

template<typename Key, typename Value>
class RBTree : public BinarySearchTree<Key, Value>
{
public:
	using typename BinarySearchTree<Key, Value>::ValueType;
	using typename BinarySearchTree<Key, Value>::NodeType;

	virtual void erase(const Key& key) override;
	virtual void insert(const ValueType& element) override;
	void print();

protected:
	void dealWithRR(NodeType* node);

	void dealWithBB(NodeType* node);

	NodeType* connect34(NodeType* node0, NodeType* node1, NodeType* node2, NodeType* t0, NodeType* t1, NodeType* t2, NodeType* t3);

	NodeType* rotateAt(NodeType* node);
};

不难发现,这里的 connect34rotateAt 实现是重复的。唯一我想到解决这个问题的办法就是在 AVL 和 红黑树 之上抽象出来一层用于实现这两个接口。

5、插入

这里的实现和前面的算法一致。dealWithRR 接口的第一行判断为该方法的递归基。

template<typename Key, typename Value>
inline void RBTree<Key, Value>::insert(const ValueType& element)
{
	auto node = this->findNode(element.first);
	if (node != nullptr)
	{
		node->element.second = element.second;
		return;
	}

	node = this->insertElementWithParent(element, this->hotParent);
	this->treeSize++;

	if (this->hotParent == nullptr)
	{
		node->color = NodeType::Black;
		return;
	}

	node->color = NodeType::Red;

	dealWithRR(node);
}

template<typename Key, typename Value>
inline void RBTree<Key, Value>::dealWithRR(NodeType* node)
{
	if (node->parent == nullptr || node->parent->color == NodeType::Black)
	{
		return;
	}

	auto sibling = node->parent->sibling();
	if (sibling == nullptr || sibling->color == NodeType::Black)
	{
		auto parent = this->rotateAt(node);
		parent->leftChild->color = NodeType::Red;
		parent->rightChild->color = NodeType::Red;
		parent->color = NodeType::Black;
	}
	else
	{
		if (node->parent->parent != this->root)
		{
			node->parent->parent->color = NodeType::Red;
		}

		sibling->color = NodeType::Black;
		node->parent->color = NodeType::Black;
		dealWithRR(node->parent->parent);
	}
}

6、删除

相比而言,双黑缺陷的处理较为复杂。需要注意,我们应该在处理完双黑缺陷之后再删除节点。因为在处理双黑缺陷时,我们可能进行递归,而这种递归需要根据结构改变后被删除节点的兄弟节点状态来确定下一步走向。

template<typename Key, typename Value>
inline void RBTree<Key, Value>::erase(const Key& key)
{
	auto node = this->findNode(key);
	if (node == nullptr)
	{
		return;
	}

	if (node->leftChild != nullptr && node->rightChild != nullptr)
	{
		this->swapNodeWithSuccessor(node);
	}

	if (node == nullptr || node->color != NodeType::Red)
	{
		dealWithBB(node);
	}	

	this->removeNodeWithNoOrSingleChild(node);
	
	delete node;
	this->treeSize--;	
}

template<typename Key, typename Value>
inline void RBTree<Key, Value>::dealWithBB(NodeType* node)
{
	auto sibling = node->sibling();
	auto parent = node->parent;
	if (sibling->color == NodeType::Black)
	{
		if (sibling->leftChildColor() == NodeType::Red || sibling->rightChildColor() == NodeType::Red)
		{
			int parentColor = node->parent->color;
			NodeType* newParent = nullptr;

			if (sibling->leftChildColor() == NodeType::Red)
			{
				newParent = this->rotateAt(sibling->leftChild);

			}
			else
			{
				newParent = this->rotateAt(sibling->rightChild);
			}

			newParent->color = parentColor;
			newParent->leftChild->color = NodeType::Black;
			newParent->rightChild->color = NodeType::Black;
		}
		else
		{
			int parentColor = parent->color;
			sibling->color = NodeType::Red;
			parent->color = NodeType::Black;
			if (parentColor == NodeType::Black && parent != this->root)
			{
				dealWithBB(parent);
			}
		}
	}
	else
	{
		auto newParent = this->rotateAt(sibling == parent->leftChild ? sibling->leftChild : sibling->rightChild);
		newParent->color = NodeType::Black;
		node->parent->color = NodeType::Red;
		dealWithBB(node);
	}
}

7、打印函数

为了更好地观察红黑树处理过程中的状态,我们提供一个打印函数用于打印每层的节点及颜色,这样我们可以很简单的画出红黑树的结构:

template<typename Key, typename Value>
inline void RBTree<Key, Value>::print()
{
	using namespace std;

	std::deque<NodeType*> queueNodesInSameLevel;
	std::deque<NodeType*> queueNodesInNextLevel;
	auto node = this->root;
	int layer = 0;
	while (1)
	{
		cout << "layer " << layer << " : ";

		while (node != nullptr)
		{
			cout << "(" << node->element.first << ", " << node->element.second << ", "
				<< (node->color == NodeType::Black ? "Black" : "Red") << ") ";

			if (node->leftChild != nullptr)
			{
				queueNodesInNextLevel.push_back(node->leftChild);
			}

			if (node->rightChild != nullptr)
			{
				queueNodesInNextLevel.push_back(node->rightChild);
			}

			if (queueNodesInSameLevel.empty())
			{
				break;
			}

			node = queueNodesInSameLevel.front();
			queueNodesInSameLevel.pop_front();
		}

		if (queueNodesInNextLevel.empty())
		{
			break;
		}

		cout << endl;
		
		queueNodesInSameLevel = queueNodesInNextLevel;
		layer++;
		queueNodesInNextLevel.clear();
		node = queueNodesInSameLevel.front();
		queueNodesInSameLevel.pop_front();
	}
}

8、测试代码

这里对于插入接口的各种情况都做了验证;删除接口只验证了一半的情况,因为代码中的处理逻辑都相同,其余的就不在验证了:

#pragma once
#include 
#include 
#include 
#include "RBTree.h"

using namespace std;

void testInsert(RBTree<int, char>& y, const vector<pair<int, char>>& vec)
{
	cout << "insert elements : ";
	for_each(vec.begin(), vec.end(), [&y](pair<int, char> element)
		{
			y.insert(element);
			cout << "(" << element.first << ", " << element.second << ")";
		}
	);
	cout << endl;

	cout << "Tree size is " << y.size() << endl;
	cout << "Elements in level order are : " << endl;
	y.print();
	cout << endl << endl;
}

void testErase(RBTree<int, char>& y, const vector<int>& vec)
{
	cout << "erase elements with key : ";
	for_each(vec.begin(), vec.end(), [&y](int key)
		{
			y.erase(key);
			cout << key << " ";
		}
	);
	cout << endl;

	cout << "Tree size is " << y.size() << endl;
	cout << "Elements in level order are :" << endl;
	y.print();
	cout << endl << endl;
}

void test()
{
	RBTree<int, char> y;
	testInsert(y, { {26, 'c'}, {12, 'b'}, {48, 'd'} }); // init
	testInsert(y, { {26, 'a'} }); // test repeating insert
	testInsert(y, { {4, 'z'} });  // test LLr change color
	testInsert(y, { {6, 'e'} });  // test LRb rotate
	testInsert(y, { {5, 'e'} });  // test LRr change color
	testInsert(y, { {10, 'e'}, {8, 'e'} });  // test LLb rotate
	testInsert(y, { {11, 'e'} });  // test RLr change color and recursive
	testInsert(y, { {17, 'e'}, {19, 'e'} });  // test RRr change color and recursive
	testInsert(y, { {18, 'e'} });  // test RLb rotate
	testInsert(y, { {60, 'e'}, {80, 'e'} });  // test RLb rotate

	testInsert(y, { {50, 'e'} });  
	testInsert(y, { {70, 'e'} });
	testInsert(y, { {30, 'e'} });
	testInsert(y, { {43, 'e'} });
	testInsert(y, { {13, 'e'} });
	testInsert(y, { {29, 'e'} });
	testInsert(y, { {97, 'e'} });

	testErase(y, { 5 }); // test delete red leaf
	testErase(y, { 80 }); // test delete black node with red successor
	testErase(y, { 26 }); // test delete red node with red successor
	testErase(y, { 50 }); // test RB① with right red child rotate 
	testErase(y, { 19 }); // test RB① with left red child rotate 
	testErase(y, { 18 }); // test RB② rotate 
	testErase(y, { 17 }); // test RB③ rotate 
	testErase(y, { 8 }); // test Lr rotate (in recursion)
}

你可能感兴趣的:(算法,读书笔记,#,《数据结构,算法与应用——C++语言描述》,数据结构,算法,c++,b树,二叉树)