【数据结构】红黑树

红黑树的概念

AVL树是一个近似平衡的搜索二叉树,它的查找效率可以接近O(logN),AVL树的每个节点左右子树最大高度差不会超过一,也就是说它十分接近满二叉树,而要构造这样的树结构,需要在插入/删除时进行多次调整,会有较多的性能损耗

所以就有了红黑树这样一个平衡条件没有AVL树那么严格的结构红黑树的插入/删除并不会十分频繁的破坏红黑树的条件,不需要频繁进行调整

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

  • 红黑树并没有AVL树那么的追求平衡,所以红黑树的查找效率并没有AVL树高
  • 红黑树在经常进行增删的结构中性能比AVL树更优

红黑树的性质

  • 根节点必须是黑色
  • 如果一个节点是红色的,那么这个节点的左右孩子必须是黑色(也就是说不能有两个连续的红色节点)
  • 每条路径上的黑色节点个数是相等的
  • 叶子节点(这里的叶子节点是指NIL空节点)都是黑色的

这四个性质保证了:红黑树最长路径中节点个数不会超过最短路径节点个数的两倍

在这里插入图片描述


红黑树的实现

红黑树的实现采用三叉链颜色标记来实现


红黑树节点

采用KV结构存储键值对数据,节点中还包括节点颜色parent指针左子树指针右子树指针,还有节点的构造函数

//枚举结构用来定义节点颜色
enum Colour {
	RED,
	BLACK
};

template <class K, class V>
struct RBTreeNode {
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	
    //节点颜色
	Colour _Col;
	//KV键值对
	pair<K, V> _kv;

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

构造节点时,把节点的默认颜色设置为红色,因为插入的节点如果是黑色,就一定会破坏红黑树的性质,而插入红色节点则有可能破坏性质,也有可能不破坏性质插入红色节点破坏的是不能有两个连续的红色节点的性质,在进行调整时也要简单


红黑树的插入(重点)

红黑树是带有颜色平衡性质的二叉搜索树,红黑树的插入分为两个部分

  • 按照二叉搜索树的插入方式插入新节点
  • 插入节点的父节点进行检测,如果插入节点的父节点是红色,则对红黑树进行调整

插入新节点

按照二叉搜索树的插入方式

  • 树为空,创建一个结点,直接链接到根结点
  • 树不为空,按照二叉搜索树的性质找到插入的位置,再创建结点插入链接

检测父节点的颜色并调整

插入新节点后,新节点是红色的需要对插入节点的parent节点进行检测

  • 如果插入节点的parent节点是黑色的,那么插入一个红色节点,并没有对红黑树的性质造成破坏,那么插入成功
  • 如果插入节点的parent节点是红色的,那么就违反了不能有两个连续的红色节点性质,需要对红黑树进行调整

具体的调整还得看parent的兄弟节点,也就是uncle节点,具体的调整方法分为两种情况

cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点


以下情况是cur的p节点为g节点的左节点

情况一:插入结点的叔叔存在,且叔叔的颜色是红色

在这里插入图片描述

此时出现了两个红色节点连续的情况,可以把p节点变成黑色,为了让每个路径上的黑色节点数量一样,还需要把u节点变成黑色,g节点变为红色

  • 这时如果g节点是根节点,那么只需要把g再次变为黑色,就完成了插入

  • 要是g节点不是根节点,那么需要让g节点作为cur节点再次向上判断,如果g节点的父亲结点依然是红色,那么需要把g节点看做新增节点cur,再次对上面的节点进行变色处理

不管cur是p节点的左节点还是右节点,只要u节点是红色,都是这种处理方式


情况二:插入节点的叔叔节点存在且为黑,或者叔叔节点不存在

情况二还分为两种小情况

  • cur是parent的左子树,也就是说,cur,p,g三个节点处于一条直线

在这里插入图片描述

上图是叔叔节点存在且为黑

在这里插入图片描述

上图是叔叔节点不存在

这两种情况的操作是一样的,所以可以合并为一种情况

  • 对g节点进行右单旋,降低这个树的高度
  • 然后对p和g节点进行变色处理

此时p节点变为黑色,不管现在p节点是不是树的根,只要旋转完成,就会插入完成

因为现在p节点的父亲节点不管是什么颜色,都符合红黑树的性质


  • cur是parent的右子树,也就是说,cur,p,g三个节点不处于一条直线

在这里插入图片描述

上图是叔叔节点存在且为黑

在这里插入图片描述

上图是叔叔节点不存在

这两种情况的操作是一样的,所以可以合并为一种情况

  • p节点进行左单旋,把树调整成上一种情况(祖孙三代在一条直线上)
  • 按照上一种情况的处理方法,右单旋降低这个树的高度
  • 然后对cur和g节点进行变色处理

当cur的p节点为g节点的右节点时,处理方式和上面情况都是镜像的


实现代码
pair<Node*, bool> Insert(const pair<K, V>& kv) {
	//先按照二叉搜索树的插入方式把节点插入到红黑树

	//根节点为空
	if (_root == nullptr) {
		_root = new Node(kv);
		//根节点必须是黑色
		_root->_Col = BLACK;
		return make_pair(_root, true);
	}

	//根节点不为空 

	Node* parent = nullptr;
	Node* cur = _root;

	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 make_pair(cur, false);
		}
	}

	//找到要插入的节点位置
	cur = new Node(kv);
	//记录插入节点的位置
	Node* newNode = cur;

	//新节点的链接关系
	if (parent->_kv.first > kv.first) {
		parent->_left = cur;
		cur->_parent = parent;
	}
	else {
		parent->_right = cur;
		cur->_parent = parent;
	}
		

	//插入新节点成功

	//检测红黑树的性质是否被破坏

	//如果cur的parent存在且为红,说明树的性质已经被破坏
	while (parent && parent->_Col == RED) {
		//这时就要看cur的uncle
		Node* grandParent = parent->_parent;
		if (parent == grandParent->_left) {
			Node* uncle = grandParent->_right;

			//情况1 cur的uncle存在且为红色
			//变色处理
			if (uncle && uncle->_Col == RED) {
				parent->_Col = BLACK;
				uncle->_Col = BLACK;
				grandParent->_Col = RED;

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

			//情况2 cur的uncle存在且为黑 
			//说明cur不是新增节点,cur是情况1迭代上来的
			//情况3 cur的uncle不存在 可以和情况2合并
			//进行右单旋 变色
			else{
				//祖孙三代在一条直线
				if (parent->_left == cur) {
					//右单旋
					_RotateR(grandParent);
					//变色
					parent->_Col = BLACK;
					grandParent->_Col = RED;
				}
				else { //parent->_right == cur
					//左单旋
					_RotateL(parent);

					//右单旋
					_RotateR(grandParent);
					//变色
					grandParent->_Col = RED;
					cur->_Col = BLACK;
				}

				break;
				//旋转完后,调整完成,不用再向上调整
			}
		}
		//parent == grandParent->_right
		else {
			Node* uncle = grandParent->_left;
			//uncle为红 变色
			if (uncle && uncle->_Col == RED) {
				uncle->_Col = BLACK;
				parent->_Col = BLACK;
				grandParent->_Col = RED;

				//继续向上检测
				cur = grandParent;
				parent = cur->_parent;
			}
			//uncle为黑或者uncle不存在
			else {
				//祖孙三代在一条直线
				if (parent->_right == cur) {
					//左单旋
					_RotateL(grandParent);
					//变色
					grandParent->_Col = RED;
					parent->_Col = BLACK;
				}
				//祖孙三代不在一条直线
				else {
					_RotateR(parent);
					_RotateL(grandParent);

					//变色
					grandParent->_Col = RED;
					cur->_Col = BLACK;
				}

				//旋转完结束
				break;
			}
		}
	}

	//插入成功
	//把红黑树的根节点变成黑色
	_root->_Col = BLACK;
	return make_pair(newNode, true);
}

//左单旋
void _RotateL(Node* parent) {
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	Node* grandParent = parent->_parent;

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

	subR->_left = parent;
	parent->_parent = subR;

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


//右单旋
void _RotateR(Node* parent) {
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	Node* grandParent = parent->_parent;

	parent->_left = subLR;
	//subLR有可能为空
	if (subLR) {
		subLR->_parent = parent;
	}

	subL->_right = parent;
	parent->_parent = subL;


	//看要旋转的点是子树还是根
	if (parent == _root) {
		_root = subL;
		subL->_parent = nullptr;
	}
	else {
		if (grandParent->_left == parent) {
			grandParent->_left = subL;
		}
		else {
			grandParent->_right = subL;
		}
		subL->_parent = grandParent;
	}

}

红黑树的查找

红黑树的查找按照二叉搜索树的查找方式查找即可

  • 若树为空查找失败,返回nullptr
  • key值小于当前节点,就去当前节点的左子树查找
  • key值大于当前节点,就去当前节点的右子树查找
  • key值等于当前节点查找成功,返回对应节点指针
Node* Find(const K& key) {
	if (_root == nullptr)
		return nullptr;

	Node* cur = _root;

	while (cur) {
		if (cur->_kv.first > key) {
			cur = cur->_left;
		}
		else if (cur->_kv.first < key) {
			cur = cur->_right;
		}
		else {
			//找到了
			return cur;
		}
	}

	//没找到
	return nullptr;
}

红黑树的验证

可以用中序遍历是否有序的方式来验证红黑树的搜索性质

void InOrder() {
	_InOrder(_root);
}

void _InOrder(Node* root) {
	if (root == nullptr)
		return;

	_InOrder(root->_left);

	cout << root->_kv.first << " : " << root->_kv.second << endl;
	_InOrder(root->_right);
}

除了验证红黑树的搜索性质,还需要验证红黑树的三个特性

  • 根是否为黑色
  • 是否有两个连续的红色节点
  • 每条路径上的黑色节点个数是否相等
bool IsRBTree() {
	if (_root == nullptr)
		return true;

	if (_root->_Col == RED) {
		cout << "根节点为红色" << endl;
		return false;
	}
			

	//计算一个路径黑色节点的标准值
	Node* cur = _root;

	int BlackNum = 0;

	while (cur) {
		if (cur->_Col == BLACK)
			BlackNum++;

		cur = cur->_left;
	}

	int count = 0;

	return _IsRBTree(_root, BlackNum, count);
}
//子函数用来递归
bool _IsRBTree(Node* root, int BlackNum, int count) {
		
	//找到了路径结束
	if (root == nullptr) {
		if (count != BlackNum) {
			//有一条路径黑色节点数量和最左路径不相等
			cout << "有一条路径黑色节点数量和最左路径不相等" << endl;
			return false;
		}
		return true;
	}

	//检测红色节点的父节点是否是红色节点
	if (root->_Col == RED && root->_parent->_Col == RED) {
		cout << "有两个连续的红色节点" << endl;
		return false;
	}

	//如果节点是黑色 ++count
	if (root->_Col == BLACK)
		count++;

	return _IsRBTree(root->_left, BlackNum, count)
		&& _IsRBTree(root->_right, BlackNum, count);
}

红黑树的删除

红黑树的删除参考

红黑树

红黑树(C++实现)


红黑树和AVL树的比较

红黑树保证了最大的路径差不超过二倍,所以其在最坏情况下,查找效率是O(2logN)

AVL树保证了高度平衡,其最大的高度差不超过2,所以他的查找效率一直都是保持在O(logN)左右

可以看出AVL树的查找效率几乎是红黑树的2倍,但是由于是O(logN)数量级,其实在计算机看来,他们俩的查找效率几乎可以忽略不计

但是AVL树的高度平衡是因为其通过大量的旋转来完成的,所以对于经常发生删除和插入的结构红黑树的效率会更优,并且红黑树的实现比起AVL更加容易且易于控制,所以实际中使用红黑树更多

在这里插入图片描述

图片转自

高级数据结构与算法 | 红黑树(Red-Black Tree)


完整代码实现

RBTree.h

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xiaomage1213888

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值