C++ - RBTree

前面的文章中我们讲述了以二叉搜索树为基础的AVL树,本文中我们将继续讲一种二叉搜索树为基础的红黑树。

红黑树的概念

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或
Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路
径会比其他路径长出俩倍,因而是接近平衡的。下图就是一颗红黑树的例子:C++ - RBTree_第1张图片

红黑树的性质

  1. 每个结点不是红色就是黑色
  2. 根节点是黑色的
  3. 如果一个节点是红色的,则它的两个孩子结点是黑色的->不能出现连续的红色节点
  4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点->每条路径上都有着相同数量的黑色节点。
  5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)

通过上述的性质就可以满足,红黑树确保没有一条路径会比其他路径长出俩倍这个限制。红黑树的最短路径为全黑,最长路径为一黑一红, 红黑相间。NIL结点就是空节点,我们上面图中的树一共有11条路径,计算黑色节点的数量需要计算到空节点,这样才能够对红黑树形成稳定的控制。

假设全部的黑色结点有N个,最短的路径就是logN,整棵树的结点数量就在N到2N之间,最长的路径为2longN,红黑树与AVL树进行比较如果是十亿个数据,AVL树的话大概需要查找30次,而红黑树大概需要60次,相对于CPU来说差距并不大,而AVL树需要的是严格的平衡,但是红黑树并不需要非常的严格,例如下面的就是一颗红黑树它不需要处理,但如果变为了AVL树就需要进行旋转。C++ - RBTree_第2张图片

红黑树结点的定义

// 结点的颜色
enum Colour {
	RED,
	BLACK,
};

// 红黑树结点的定义
template
struct RBTreeNode {
	RBTreeNode* _left;
	RBTreeNode* _right;
	RBTreeNode* _parent;
	pair _kv;
	Colour _col;

	RBTreeNode(const pair& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _col(RED) // 新插入的节点如果时黑色一定会违反规则四,如果插入的是红色,有可能会违反规则三,因此在这里选择惩罚概率较小的规则三,选择新增插入的结点为红色
	{}
};

红黑树的插入操作

因为红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树首先按照二叉搜索树的规则进行插入。然后检测新节点插入之后是否红黑树的性质会造成破坏。

按照二叉搜索的树规则插入新节点

在插入是需要注意的是根节点一定是黑色的,那么新插入的节点的颜色该如何去定义,假设我们插入的新结点是黑色的,那么就一定会会违反红黑树性质的第四条,假如我们插入的新节点是红色的,此时就会分为不同的情况:如果父节点是黑色的就可以正常的进行插入过工作,如果父节点是红色的就会违反红黑树性质的第三条。因此综合上述的情况就可以得到,我们新插入的节点应该为红色的节点。

检测新节点插入后,红黑树的性质是否造到破坏

因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何
性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连
在一起的红色节点,此时需要对红黑树分情况来讨论:

约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔结点

首先我们以上面的红黑树为例子进行分析:

C++ - RBTree_第3张图片

在上图的位置进行插入操作,通过将父亲节点和叔叔节点变黑,将祖父节点变红即可完成一次修正,但是此时会发现祖父节点与其父节点又有连续两个红色结点的冲突,但是这个冲突与之前的插入相同,因此重复之前的操作即可,最后如果祖父结点已经是根节点了,就将根节点修正为黑色。

还有一个例子:就适当叔叔节点为空时即在6号结点的子树出插入新节点:C++ - RBTree_第4张图片

此时,由于有两个连续的红色结点的存在,可能会使最长路径大于两倍的最小路径的出现,那么就需要记性旋转操作将树的高度变低。上图演示的就是左单旋+变色处理。

下面我们来看一下抽象图:

第一种情况:cur为红,p为红,g为黑,u存在且为红

这种情况下的抽象图如下所示:

C++ - RBTree_第5张图片

将其具象进行分析可得下面的情况:C++ - RBTree_第6张图片

左边的树即为a、b、c、d、e都为空的情况,右边即为c、d、e是子树的情况。 

这里的c、d、e在第二种具象的情况下只要有下面四种情况的任意一种有一个黑色的结点即可。C++ - RBTree_第7张图片

这种情况的解决方式就是: 将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。

还需要注意的就是,除了从情况一可能变为情况一,从情况一还有可能会变为其他的情况,例如:C++ - RBTree_第8张图片

这就是从一种情况变为了另一种情况因此这需要不同的处理方法。 

第二种情况:cur为红,p为红,g为黑,u不存在/u存在且为黑

抽象图有如下的两种情况:

C++ - RBTree_第9张图片

cur原来是黑色的上图的右边的情况即为从情况一变过来的,c是有一盒黑色节点的子树,d、e是空或者有一个红色的结点。

解决方式:p为g的左孩子,cur为p的左孩子,则进行右单旋转;相反,p为g的右孩子,cur为p的右孩子,则进行左单旋转;p、g变色--p变黑,g变红。

第三种情况:cur为红,p为红,g为黑,u不存在/u存在且为黑

情况三与情况二是类似的不过在情况二中需要的是单旋,在情况三中需要进行的是双旋的操作。C++ - RBTree_第10张图片

上图中可以看出,第三种情况进行旋转之后就变为了第二种情况,进行一次单旋即可完成处理。 

代码实现:

bool Insert(const pair& kv) {
	if (_root == nullptr) {
		_root = new Node(kv);
		_root->_col = BLACK; // 将根节点设置为黑色
		return true;
	}

	Node* parent = nullptr;
	Node* cur = _root;
	while (cur) {
		if (cur->_kv.first < kv.first) {
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_kv.first > kv.first) {
			parent = cur;
			cur = cur->_left;
		}
		else {
			return false;
		}
	}

	cur = new Node(kv);
	if (parent->_kv.first > kv.first) {
		parent->_left = cur;
	}
	else {
		parent->_right = cur;
	}
	cur->_parent = parent;

	// 针对插入后的情况进行分析 
	// 如果插入的节点的父节点是黑色的该红黑树就不会产生问题,可以正常插入
	// 如果插入的节点的父节点是红色的就需要进行调整
	while (parent && parent->_col == RED) {
		Node* grandfather = parent->_parent;
		if (grandfather->_left == parent) {
			Node* uncle = grandfather->_right;
			// 情况1:u存在且为红,变色处理,并继续往上处理
			if (uncle && uncle->_col == RED) {
					//     g
					//   p   u
					// c       
				parent->_col = BLACK;
				uncle->_col = BLACK;
				grandfather->_col = RED;

				// 继续往上调整
				cur = grandfather;
				parent = cur->_parent;
			}
			else { // 情况2+3:u不存在/u存在且为黑,旋转+变色 在这种情况下就无法满足最短路径的两倍是大于最长路径的这个条件,因此需要进行旋转操作,从AVL树中可以得知旋转是有四种情况,需要分别对其进行处理
					//     g
					//   p   u
					// c 
				if (cur == parent->_left) {
					RotateR(grandfather);
					parent->_col = BLACK;
					grandfather->_col = RED;
				}
				else // cur == parent->_right {
					//     g
					//   p   u
					//     c
					RotateL(parent);
					RotateR(grandfather);
					cur->_col = BLACK;
					//parent->_col = RED;
					grandfather->_col = RED;
				}

				break;
			}
		}

		else { // grandfather->_right == parent
					//    g
					//  u   p
					//        c
			Node* 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
					//        c
				if (cur == parent->_right) {
					RotateL(grandfather);
					grandfather->_col = RED;
					parent->_col = BLACK;
				}
				else { // cur == parent->_left
					//    g
					//  u   p
					//    c
					RotateR(parent);
					RotateL(grandfather);
					cur->_col = BLACK;
					grandfather->_col = RED;
				}

				break;
			}
		}
	}

	_root->_col = BLACK;

	return true;
}

 红黑树的验证

 红黑树的检测分为两步:

  1. 检测其是否满足二叉搜索树(中序遍历是否为有序序列)
  2. 检测其是否满足红黑树的性质
bool IsBalance()
{
	// 检查根节点的颜色
	if (_root && _root->_col == RED) 
	{
		cout << "根节点颜色是红色" << endl;
		return false;
	}

	// 将最左路径作为基准值进行黑色节点的比较
	int benchmark = 0;
	Node* cur = _root;
	while (cur)
	{
		if (cur->_col == BLACK)
			++benchmark;
		cur = cur->_left;
	}

	// 连续红色节点
	return _Check(_root, 0, benchmark);
}

bool _Check(Node* root, int blackNum, int benchmark)
{
	// 检查黑色结点的数量,DFS走到空时将blackNum用来记录黑色结点的数量与benchmark进行比较
	if (root == nullptr)
	{
		if (benchmark != blackNum)
		{
			cout << "某条路径黑色节点的数量不相等" << endl;
			return false;
		}
		//cout << blackNum << endl; // 求每条路径黑色结点的数量
		return true;
	}

	if (root->_col == BLACK)
	{
		++blackNum;
	}

	// 检查连续的红色结点,从父亲开始检查儿子这种方法不是很好(孩子是有可能不存在的),从儿子开始检查父亲更加的便捷
	if (root->_col == RED
		&& root->_parent
		&& root->_parent->_col == RED)
	{
		cout << "存在连续的红色节点" << endl;
		return false;
	}

	return _Check(root->_left, blackNum, benchmark)
		&& _Check(root->_right, blackNum, benchmark);
}

你可能感兴趣的:(数据结构,C++,数据结构,c++)