Bootstrap

【C++进阶】红黑树的介绍及实现

【C++进阶】红黑树的介绍及实现

🥕个人主页:开敲🍉

🔥所属专栏:C++🥭

🌼文章目录🌼

1. 红黑树的概念

    1.1 红黑树的规则

    1.2 红黑树如何确保最长路径不会超过最短路径的2倍?

    1.3 红黑树的效率

2. 红黑树的实现

    2.1 红黑树的结构

    2.2 红黑树的插入

        2.2.1 红黑树插入一个值的大概过程

        2.2.2 情况1:变色

        2.2.3 情况2:单旋+变色

        2.2.4 情况3:双旋+变色

    2.3 红黑树插入代码实现

    2.4 红黑树的查找

    2.5 红黑树的验证

1. 红黑树的概念

  红黑树是一颗二叉搜索树,他的每个节点增加一个存储位来表示节点颜色,红色或者黑色。通过对任何一条从根到叶子的路径上各个节点的颜色进行约束,红黑树能够确保没有任何一条路比其他任何一条路的路径长2倍以上,因而红黑树非常接近平衡二叉搜索树。

    1.1 红黑树的规则

  ① 每个节点不是红色就是黑色。

  ② 根节点必须是黑色的。

  ③ 如果一个节点是红色,则它的两个孩子节点必须是黑色,也就是不能够出现连续的两个红色节点。

  ④ 对于任意一个节点,从该节点到其所有NULL节点的简单路径上,均包含相同数量的黑色节点。

    1.2 红黑树如何确保最长路径不会超过最短路径的2倍?

  由规则 ④ 可以知道,从根到NULL节点的每条路径都含有相同数量的黑色节点,因此在极端情况下,最短路径就是全黑节点,记最短路径为 bh (black height);由规则2 和规则3 可以知道,任意一条路径不会有连续的红色节点,因此在极端情况下,最长路径就是一黑一红交替形成,那么最长的路径就是 2*bh

    1.3 红黑树的效率

  假设 N 是红黑树中的节点数量,h 是最短路径的长度,那么 2^h - 1 <= N <= 2^2*h - 1,由此推出h ≈ logN,也就意味着红黑树查找时最坏的情况就是走最长路径 2*logN,那么时间复杂度依旧是 O(logN)。

  红黑树的表达相对 AVL 树要更加抽象。AVL 树通过平衡因子直观地控制平衡;而红黑树通过4条规则来约束颜色从而实现近似平衡。它们的效率都在 O(logN) 这一量级,但是相对于 AVL 树,红黑树在插入节点时的旋转次数要少许多,因为红黑树对平衡的控制没有 AVL树 严格。

2. 红黑树的实现
    2.1 红黑树的结构

  

	template <class T>
	class RBTreeNode
	{
	public:
		T _val;//存储的值
		RBTreeNode<T>* _left;//左孩子
		RBTreeNode<T>* _right;//右孩子
		RBTreeNode<T>* _parent;//父亲
		Color _col;//颜色

		RBTreeNode(const T& val,Color col = RED)//构造函数,这里默认构造RED节点
			:_left(nullptr)
			,_right(nullptr)
			,_parent(nullptr)
			,_val(val)
			,_col(col)
		{}
    private:
        Node* _root = nullptr;//根节点
	};
    2.2 红黑树的插入
        2.2.1 红黑树插入一个值的大概过程

  ① 插入一个值按照二叉搜索树的规则插入,插入后我们再根据红黑树的 4条 规则进行 变色或者旋转 操作。

  ② 如果空树插入,则插入的节点必须是黑色,并且将该节点作为根;如果是非空树插入,新增节点必须是红色节点,否则会破坏规则④,这点很好证明。

  ③ 新增节点插入后,如果其父亲节点是黑色,则无事发生,插入结束。

  ④ 新增节点插入后,如果其父亲节点是红色,则违反了规则③。此时需要我们进行变色、旋转操作恢复树的结构。

  这里需要我们进一步分析,下图我们用 cur 代表新增节点,parent代表新增节点的父亲,uncle代表父亲的兄弟,grandfather 代表父亲的父亲:

  首先需要明白一点:c为红色,p为红色,则grandfather必定为黑色。因为如果grandfather是红色,则 parent 和 grandfather 就已经违反了规则③,根本没机会轮到 cur 和 parent 违反。因此,cur、parent、grandfather的颜色都是确定的,此时只有 uncle 的颜色是未知,因此关键就在于 uncle 的变化,根据 uncle 的变化我们可以分为以下几种情况处理。

  注:下面我们用 c 表示 cur,p 表示 parent,u表示uncle,g表示 grandfather。

        2.2.2 情况1:变色

  c为红,p为红,g为黑,u存在并且u为红,则将 p、u 变黑,g 变红。再把 g 当作新的 c,继续往上更新。

  分析:因为p和u都是红色,g是黑色,把p和u变黑,左边子树路径各增加⼀个黑色结点,g再变红,相当于保持g所在⼦树的黑色结点的数量不变,同时解决了c和p连续红色结点的问题,需要继续往上更新是因为,g是红色,如果g的父亲还是红色,那么就还需要继续处理;如果g的父亲是黑色,则处理结束了;如果g就是整棵树的根,再把g变回黑色。

  情况1只变色,不旋转。所以无论c是p的左还是右,p是g的左还是右,都是上面的变色处理方式。

  ① 跟 AVL 树类似,上图我们展示了一种具体情况,但是实际中需要这样处理的有很多种情况。

  ② 下图将上面类似的处理进行了抽象表达,d/e/f代表每条路径拥有nb个黑色节点的子树,a/b代表每条路拥有 hb-1 个 黑色节点的根为红的子树,hb>=0。

  ③ 图1/图2/图3,分别展示了 hb ==0/hb == 1/hb == 2 的具体情况组合分析,当 hb == 2时,组合情况就来到了上百亿种。当然,这些样例并不是想要说明红黑树有多么复杂,恰恰相反,这里想要说明不论情况多么复杂,处理方式都是一样的,变色再继续向上处理,因此我们只需要对着抽象图来操作即可。

        2.2.3 情况2:单旋+变色

  c为红,p为红,g为黑,u不存在或者u存在且为黑,u不存在,则c⼀定是新增结点,u存在且为黑,则c⼀定不是新增,c之前是黑色的,是在c的子树中插⼊,符合情况1,变⾊将c从黑色变成红色,更新上来的。

  分析:p必须变黑,才能解决,连续红色结点的问题,u不存在或者是黑色的,这⾥单纯的变色无法解决问题,需要旋转+变色。

  如果p是g的左,c是p的左,那么以g为旋转点进行右单旋,再把p变黑,g变红即可。p变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父亲是黑色还是红色或者空都不违反规则。

  

  如果p是g的右,c是p的右,那么以g为旋转点进行左单旋,再把p变黑,g变红即可。p变成课这颗树新的根,这样⼦树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父亲是黑色还是红色或者空都不违反规则。

  

        2.2.4 情况3:双旋+变色

  c为红,p为红,g为黑,u不存在或者u存在且为黑,u不存在,则c⼀定是新增结点,u存在且为黑,则c⼀定不是新增,c之前是黑色的,是在c的子树中插入,符合情况1,变色将c从黑色变成红色,更新上来的。

  分析:p必须变黑,才能解决,连续红色结点的问题,u不存在或者是黑色的,这里单纯的变色无法解决问题,需要旋转+变色。

  

  如果p是g的左,c是p的右,那么先以p为旋转点进行左单旋,再以g为旋转点进行右单旋,再把c变黑,g变红即可。c变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为c的父亲是黑色还是红色或者空都不违反规则。

  如果p是g的右,c是p的左,那么先以p为旋转点进行右单旋,再以g为旋转点进行左单旋,再把c变黑,g变红即可。c变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为c的父亲是黑色还是红色或者空都不违反规则。

    2.3 红黑树插入代码实现
bool Insert(const T& data)
{
	if (!_root)
	{
		_root = new Node(data,BLACK);
		return true;
	}
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)//插入节点
	{
		if (cur->_val > data)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (cur->_val < data)
		{
			parent = cur;
			cur = cur->_right;
		}
		else return false;
	}

	cur = new Node(data);
	cur->_parent = parent;
	if (parent->_val > data) parent->_left = cur;
	else parent->_right = cur;

	//处理两个红连在一起的情况
	while (parent && parent->_col == RED)
	{
		if (parent->_left == cur)//新增节点插入在parent左边的情况
		{
			Node* grandfather = parent->_parent;
			Node* uncle = grandfather->_right;
			if (grandfather->_right == parent) uncle = grandfather->_left;

			if (uncle && uncle->_col == RED)//uncle存在并且颜色为红
			{
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;

				cur = grandfather;
				parent = cur->_parent;
			}
			else//uncle不存在或者颜色为黑
			{
				if (grandfather->_left == parent)
				{
					RotateR(grandfather);
					parent->_col = BLACK;
					grandfather->_col = RED;
				}

				else
				{
					RotateR(parent);
					RotateL(grandfather);
					cur->_col = BLACK;
					grandfather->_col = RED;
				}

				break;
			}
		}
		else//新增节点插入在parent右边的情况
		{
			Node* grandfather = parent->_parent;
			Node* uncle = grandfather->_right;
			if (grandfather->_right == parent) uncle = grandfather->_left;

			if (uncle && uncle->_col == RED)//uncle存在并且颜色为红
			{
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;

				cur = grandfather;
				parent = cur->_parent;
			}

			else
			{
				if (grandfather->_right == parent)
				{
					RotateL(grandfather);
					parent->_col = BLACK;
					grandfather->_col = RED;
				}
				else
				{
					RotateL(parent);
					RotateR(grandfather);
					cur->_col = BLACK;
					grandfather->_col = RED;
				}

				break;
			}
		}
	}
	_root->_col = BLACK;
	return true;
}
    2.4 红黑树的查找

  按二叉搜索树的逻辑查找即可,查找效率为 O(logN)。

void Find(Node* cur,const T& data)//查找
{
	while (cur)
	{
		if (cur->_val > data) cur = cur->_left;
		else if (cur->_val < data) cur = cur->_right;
		else
		{
			cout << data << "->" << cur->_val << endl;
			return;
		}
	}

	cout << "找不到" << endl;
}
    2.5 红黑树的验证

  这里获取最长路径和最短路径,检查最长路径不超过最短路径的2倍是不可行的,因为就算满足这个条件,红黑树也可能颜色不满足规则,当前暂时没出问题,后续继续插入还是会出问题的。所以我们还是去检查4点规则,满足这4点规则,⼀定能保证最长路径不超过最短路径的2倍。

  ① 规则①:我们实现采用枚举颜色类型,天然实现保证了颜色不是红色就是黑色。

  ② 规则②:直接检查根即可。

  ③ 规则③:前序遍历检查,遇到红⾊结点查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲的颜色就方便多了。

  ④ 规则④:前序遍历,遍历过程中用形参记录跟到当前结点的BlackNum(黑色结点数量),前序遍历遇到黑色结点就BlackNum++,走到空就计算出了⼀条路径的黑色结点数量。再任意⼀条路径黑色结点数量作为参考值,依次比较即可。



#include "RBTree.h"

int main()
{
	gjk::RBTree<int> rb;

	常规测试用例
	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	特殊的带有双旋场景的测试⽤例
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	for (auto e : a)
	{
		rb.Insert(e);
	}
	rb.printf();

	rb.find(4);
	rb.find(100);


	bool ret = rb.isRBTree();
	cout << ret << endl;


	int nodesize = rb.nodesize();
	cout << nodesize << endl;

	int leafsize = rb.leafnodesize();
	cout << leafsize << endl;

	vector<int> arr;
	rb.blacknodenum(arr);
	for (auto i : arr) cout << i << " ";
	cout << endl;




	return 0;
}

                                                    创作不易,点个赞呗,蟹蟹啦~ 

;