【数据结构】红黑树的原理及其实现

红黑树的特性

红黑树最常用的平衡二叉搜索树。跟AVL树不同的是,红黑树是依靠节点的颜色来维护平衡的。虽然任意节点不具备严格平衡,但是数据的查找、插入、删除等操作效率依旧出色。下面是红黑树的一些特性:

  1. 每个节点的颜色要么是红色要么是黑色
  2. 根节点的颜色是黑色
  3. 如果一个节点的颜色是红色的,那么它的两个孩子节点的颜色一定是黑色的
  4. 任意节点到其所能到达的叶子节点之间的黑色节点的数量相同
  5. 叶节点是黑色的空节点

根据以上红黑树的特性,我们可以总结出以下结论:

结论:红黑树中最长路径的节点数量不超过最短路径节点数量的2倍
证明:假设每条路径黑色节点的数量为n(假设不包括空叶子节点),则红色节点的数量最多是n。任意路径节点数量最少为n(只有黑节点),最多为2*n

这样一来,任意一条路径的长度之差都保证在了一个有限的范围内,这也是红黑树具有平衡性的原因。

红黑树的时间复杂度

由于红黑树底层还是一颗二叉搜索树,根据二叉搜索树的特性:查找的效率取决于树的高度

推导:

现假设红黑树的某一路径黑色节点数量为bh。由于任意路径黑色节点数目相同,我们可以把路径上所有的红色节点删除,将删除节点的父节点和其子节点相连,于是我们得到了一颗纯黑色节点的树(四叉树),如下图:
在这里插入图片描述
(该图截自b站动画讲编程)
这颗黑树的高度显然就是bh,且我们可以得到之前红黑树的节点数最少为2^bh-1(最少就是全是黑色)。假设这棵红黑树的节点总数为N,则N>=2^bh-1,两边取对数得bh<=log(N+1)(以2为底)。

设h为红黑树得最大高度,则有h=2*hb=2log(N+1)(最长路径节点数是2*bh)

结论

红黑树的高度最大为2log(N+1),N表示树的节点总数。这个证明基于红黑树的性质。
得到了节点数与树的高度的关系,我们也就能得到红黑树查找数据的效率为O(logN)。此外,红黑树的插入效率和删除效率取决于查找效率,也是O(logN)。

红黑树与AVL树比较

相对来说,红黑树的内部实现细节没有AVL树这么复杂,虽然AVL树保证具备严格平衡性,但是其插入元素时调整平衡的操作相对也就繁琐。插入和删除的时候,AVL树可能会进行更多的旋转操作来维持平衡,导致实际的执行时间可能高于红黑树

总的来说,红黑树适用于读写均衡的场景,插入和删除操作较为高效。而AVL树适用于查询频繁的场景,查询效率更高,但是插入和删除的维护成本较高。即使是这样,两者的差别依旧不大,查找、删除、插入的时间复杂度都是OlogN。

红黑树的插入

上面我们已经了解了红黑树的特性,接下来谈谈红黑树插入数据时是如何维护上述特性的。

红黑树的节点定义

与AVL实现类似,红黑树的节点依旧有三个指针分别指向父节点、左孩子节点、右孩子节点。且有一个变量表示当前节点的颜色。初始默认是红色。为什么默认设置为红色呢?这是因为插入节点时,新节点一定与空叶子节点相邻。而根据红黑树的性质,空叶子节点的颜色是黑色,这就使得,如果新插入节点是黑色,那么每插入一次这条路径就一定会多出来一个黑色节点,即一定要调整。虽然新节点是红色也可能会需要调整,但影响会少很多。

template<class K,class V>
struct RBTreeNode {
	RBTreeNode<T, V>* _parent;
	RBTreeNode<T, V>* _left;
	RBTreeNode<T, V>* _right;
	pair<K, V> _kv;//键值对

	Colour _col;//颜色

	RBTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		,_parent(nullptr)
		,_left(nullptr)
		,_right(nullptr)
		,_col(RED)
	{

	}
};

调整策略

如果新插入一个节点,破坏了红黑树的性质,那么我们需要进行调整。具体需要调整的情况如下:

  1. 如果插入节点是根节点:那么只需要将根节点变黑
  2. 插入节点的叔叔节点是红色:父亲节点和叔叔节点变成黑色,爷爷变成红色,且爷爷节点变成新插入节点
  3. 插入节点的叔叔节点是黑色(为空也是黑色):需要先旋转,然后变色

上述2,3情况一定是出现两个相邻节点是红色。

思考情况2:

为什么我们要把父亲节点和叔叔节点变成黑色,爷爷变成红色,且爷爷节点变成新插入节点?为什么这样做一定能维护红黑树的性质呢?
在这里插入图片描述

  • 首先,把父亲节点变黑是因为相邻两个节点是红色,那为什么要把叔叔节点也变色呢
    这是因为新节点插入的这条路径可能会多一个黑色节点。为了让所有路径黑色节点数都能加一,每次调节父节点颜色的同时,也要调节叔叔节点颜色。这样一来,即使最后新节点插入的这条路径黑色节点数加一,其它所有路径的黑色节点也能同步加一。

  • 其实将爷爷节点变色也是为了维护所有路径黑色节点数一致这一特性
    设想我们不将爷爷节点变成红色,此时整个红黑树确实没有出现“相邻的红色节点”,但是经过爷爷节点的路径的黑色节点数目就会比其它路径黑色节点数目多(parent和uncle都是变黑了)。除非此时爷爷节点是根节点(所有路径都经过根节点)。所以我们选择继续向上调整,把爷爷节点变成红色,并更新cur指针指向grandparent节点,看是否还会出现“红红”的俩节点。把爷爷节点变红是基于贪心思路:先不让路径的黑色节点数加一试试能不能平衡

在这里插入图片描述

什么时候停止调整呢?父节点的颜色是黑色为止。此时红黑树平衡

思考情况3:

如果叔叔节点本来就是黑色的呢?(空节点也是黑色)此时无论如何调节叔叔节点颜色,都有可能使得其它路径(比如叔叔这条路径)黑色节点数少于当前新节点插入路径的黑色节点数。此时考虑旋转。旋转的方式和AVL树是一致的。
旋转一共有四种方式:左旋、右旋、左右双旋、右左双旋。
细分情况3,一共有以下四种情况:
1.curparent的左孩子,parentgrandparent的左孩子。
在这里插入图片描述
将grandparent节点右旋转,旋转之后parent顶替了原来的grandparent,并变成黑色。grandparent成为parent的右孩子,变成红色
在这里插入图片描述
旋转是不会改变二叉搜索树的特性。那么思考这样一个问题,旋转之后还是平衡的红黑树吗?答案是–是的。因为我们观察旋转之后经过parent的所有路径的黑色节点数压根就没有变。

2.curparent的右孩子,parentgrandparent的右孩子
这种情况和情况1一样,只不过方向相反,是左旋。不做过多徐述。
在这里插入图片描述
3.curparent孩子,parentgrandparent孩子

此时这种情况对grandparent节点进行左单旋还是右单旋都不能保证红黑树平衡,于是考虑双旋,即旋转两次。
在这里插入图片描述
先对parent节点左旋,发现左旋之后就变成了情况1
在这里插入图片描述
此时我们就可以像情况1一样,对grandparent进行右旋,对cur节点和grandparent节点进行变色
在这里插入图片描述
这样红黑树就平衡了,整个过程并没有增加路径的黑色节点的数量,因此也就不需要继续向上调整。

  1. curparent孩子,parentgrandparent孩子
    思路跟情况3一致,只不过旋转方向相反。向对parent节点进行右旋,再对grandparent节点进行左旋,再对cur和grandparent节点进行变色。

在这里插入图片描述

代码实现

myBTRee.h

封装了红黑树的类模板,包括各种功能的声明于定义

#pragma once

#include<vector>
#include<iostream>
using namespace std;

enum Colour{
	RED,
	BLACK
};

template<class K,class V>
struct RBTreeNode {
	RBTreeNode<K, V>* _parent;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	pair<K, V> _kv;//键值对

	Colour _col;//颜色

	RBTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		,_parent(nullptr)
		,_left(nullptr)
		,_right(nullptr)
		,_col(RED)
	{

	}
};

template<class K,class V>
class RBTree {
	typedef RBTreeNode<K, V> Node;
	typedef Node* pNode;
public:
	bool Insert(const pair<K, V>& kv) {
		if (_root == nullptr) {
			_root = new Node(kv);
			_root->_col = BLACK;
			return true;
		}
		//找到一个合适的插入位置
		pNode cur = _root;
		pNode parent = nullptr;
		while (cur) {
			if (cur->_kv.first > kv.first) {
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_kv.first < kv.first) {
				parent = cur;
				cur = cur->_right;
			}
			else {
				return false;
			}
		}
		//此时cur为nullptr
		cur = new Node(kv);
		if (cur->_kv.first < parent->_kv.first) {//插入节点
			parent->_left = cur;
			cur->_parent = parent;
		}
		else {
			parent->_right = cur;
			cur->_parent = parent;
		}

		//插入之后需要调节颜色平衡

		while (parent && parent->_col == RED) {
			//找叔叔节点
			pNode grandparent = parent->_parent;//祖父节点一定不为空,因为父节点为红色
			if (parent == grandparent->_left) {//如果叔叔在右边
				pNode uncle = grandparent->_right;
				//叔叔存在且为红,直接都变成黑色
				if (uncle && uncle->_col == RED) {
					parent->_col = BLACK;
					uncle->_col = BLACK;
					grandparent->_col = RED;
					//继续往上处理
					cur = grandparent;//cur往上跳两个节点
					parent = cur->_parent;
				}//叔叔节点不存在或者为黑色
				else {
					if (cur == parent->_left) {
						RotateR(grandparent);
						parent->_col = BLACK;
						grandparent->_col = RED;
					}
					else if (cur == parent->_right) {
						RotateL(parent);
						RotateR(grandparent);
						cur->_col = BLACK;
						grandparent->_col = RED;
					}
					break;
				}
			}
		    //如果叔叔在左边
			if (parent == grandparent->_right) {
				pNode uncle = grandparent->_left;
				//叔叔存在且为红,直接都变成黑色
				if (uncle && uncle->_col == RED) {
					parent->_col = BLACK;
					uncle->_col = BLACK;

					//继续往上处理
					grandparent->_col = RED;
					cur = grandparent;//cur往上跳两个节点
					parent = cur->_parent;
				}//叔叔节点不存在或者为黑色
				else {
					if (cur == parent->_left) {
						RotateR(parent);
						RotateL(grandparent);
						cur->_col = BLACK;
						grandparent->_col= RED;
					}
					else if (cur == parent->_right) {
						RotateL(grandparent);
						parent->_col = BLACK;
						grandparent->_col = RED;
					}
					break;
				}
			}
		}
		_root->_col = BLACK;
		return true;
	}

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

		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;

		subL->_right = parent;

		Node* ppNode = parent->_parent;
		parent->_parent = subL;

		if (parent == _root)
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subL;
			}
			else
			{
				ppNode->_right = subL;
			}

			subL->_parent = ppNode;
		}
	}

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

		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

		subR->_left = parent;
		Node* ppNode = parent->_parent;

		parent->_parent = subR;

		if (parent == _root)
		{
			_root = subR;
			_root->_parent= nullptr;
		}
		else
		{
			if (ppNode->_right == parent)
			{
				ppNode->_right = subR;
			}
			else
			{
				ppNode->_left = subR;
			}
			subR->_parent = ppNode;
		}
	}
	bool IsBalance() {
		if (_root->_col == RED)return false;

		int targetnum = 0;
		pNode cur = _root;
		while (cur) {
			if (cur->_col == BLACK)targetnum++;
			cur = cur->_left;
		}
		return _Check(_root, targetnum, 0);
	}
	void InOrder() {
		_InOrder(_root);
	}
private:

	bool _Check(pNode root,int targetnum,int blacknum) {
		if (root == nullptr) {
			if (blacknum != targetnum) {//路径的黑色节点数不相等
				return false;
			}
			else return true;
		}
		if (root->_left && root->_col == RED && root->_left->_col == RED)return false;
		if (root->_right && root->_col == RED && root->_right->_col == RED)return false;
		
		if (root->_col == BLACK)blacknum++;

		return _Check(root->_left,targetnum,blacknum) && _Check(root->_right,targetnum,blacknum);
	}

	void _InOrder(pNode root) {
		if (root == nullptr) {
			return;
		}
		_InOrder(root->_left);
		cout << root->_kv.first << " " << root->_kv.second << endl;
		_InOrder(root->_right);
	}

	pNode _root=nullptr;

};

test.cpp

用于测试代码的正确性。给出一组随机数,插入到红黑树中之后进行平衡检查。平衡检查内容为,检查是否出现连个相邻的红色节点,且所有路径的黑节点·数目是否相等。
代码:

void TestRBTree2()
{
	const int N = 100000;
	vector<int> v;
	v.reserve(N);
	srand((unsigned int)time(0));

	for (size_t i = 0; i < N; i++)
	{
		v.push_back(rand() + i);
		//cout << v.back() << endl;
	}

	size_t begin2 = clock();
	RBTree<int, int> t;
	for (auto e : v)
	{
		t.Insert(make_pair(e, e));
	}
	size_t end2 = clock();
	cout << t.IsBalance() << endl;
}

在这里插入图片描述

  • 34
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
红黑树是一种自平衡的二叉查找树,它在插入和删除节点时能够保持树的平衡。红黑树的概念可以参考。在Java实现红黑树,可以按照以下步骤进行: 1. 首先将红黑树当作一颗二叉查找树,将新节点插入到适当的位置上。 2. 将插入的节点着色为"红色"。 3. 根据红黑树的特性,通过一系列的旋转和着色等操作,使树重新保持红黑树的性质。 具体的插入过程可以参考中提供的代码。在代码中,使用了左旋转、右旋转和颜色翻转等操作来重新平衡红黑树。 首先,如果节点的右子树是红色而左子树是黑色,可以通过左旋转操作将其变为左子树为红色,右子树为黑色的情况。 其次,如果节点的左子树和左子树的左子树都是红色,可以通过右旋转操作将其修正为上述情况。 最后,如果节点的左子树和右子树都是红色,可以通过颜色翻转操作将其修正为左子树和右子树都为黑色的情况。 在插入完节点后,需要将根节点的颜色设置为黑色,以确保红黑树的性质满足。 这样,通过以上的步骤,就能够实现红黑树的插入操作。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Java数据结构红黑树的真正理解](https://download.csdn.net/download/weixin_38622475/12770272)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [Java高阶数据结构红黑树](https://blog.csdn.net/qq15035899256/article/details/126678970)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [Java数据结构——红黑树](https://blog.csdn.net/weixin_30699463/article/details/95256212)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值