【C++】红黑树:比AVL树更实用的平衡二叉搜索树

红黑树

一、红黑树的概念

二、红黑树的规则

[1. 思考:红黑树如何确保最长路径不超过最短路径的2倍的?](#1. 思考:红黑树如何确保最长路径不超过最短路径的2倍的?)

[2. 红黑树的效率](#2. 红黑树的效率)

三、红黑树的实现

[1. 整体结构](#1. 整体结构)

[2. 红黑树的插入](#2. 红黑树的插入)

[2.1 插入流程](#2.1 插入流程)

情况一:变色

情况二:单旋+变色

情况三:双旋+变色

代码实现

3.红黑树的查找

[4. 红黑树的验证](#4. 红黑树的验证)

[5. 红黑树的删除](#5. 红黑树的删除)


一、红黑树的概念

红黑树是一颗平衡二叉搜索树 ,它的每个节点增加一个存储位来存储该节点的颜色,可以是红色或黑色。通过对任意从根到叶子节点路径上节点颜色的限制,确保没有一条路径会比其它路径长2倍,达到平衡

二、红黑树的规则

  1. 每个节点不是红色就是黑色
  2. 根节点是黑色的
  3. 如果一个节点是红色的,那么它的两个孩子是黑色
  4. 对于任意节点,该节点到所有NULL节点的简单路径上,均包含相同数量的黑色节点

注意: 在《算法导论》等书中,补充了每个叶子结点(NIL)都是黑色的规则,此叶子结点指我们常说的空节点,有些书也把它们叫做外部节点

1. 思考:红黑树如何确保最长路径不超过最短路径的2倍的?

由规则4可知,一棵树不可能存在连续的红色节点,那我们可以知道:最短的路径就是全黑,长度为bh,最长的路径就是一黑一红交替出现,长度为2bh,因此,最极端的情况,最长的路径是最短的路径的两倍

2. 红黑树的效率

假设N为红黑树中节点的数量,h为最短路径长度,那么2^h-1<=N<=2^(2h)-1 ,因此h是在logN量级的 ,因此其增删查改的效率是O(logN) ,红黑树相比于AVL树要抽象一些,但是红黑树对平衡的控制没有AVL树那么严格,旋转次数也更少

三、红黑树的实现

1. 整体结构

红黑树与AVL树相同,也是由三叉链构成,它还有一个颜色的成员变量,我们可以用一个枚举来实现

cpp 复制代码
enum Colour
{
	RED, BLACK
};

template<typename K, typename V>
struct RBTreeNode
{
	pair<K, V> _kv;
	RBTreeNode<K, V>* _parent;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	Colour _col;
	RBTreeNode(const pair<K,V>& _kv)
		:_kv(_kv), _parent(nullptr), _left(nullptr), _right(nullptr)
	{}
};

template<typename K, typename V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
private:
	Node* _root;
};

2. 红黑树的插入

2.1 插入流程

  1. 首先我们也是要按照二叉搜索树的规则来进行插入,之后我们再看是否满足4条规则
  2. 如果树为空树,那插入的节点必然为 黑色的根节点 ;如果树不是空树,那么插入节点 必然为红色节点因为如果新增黑色节点,那必然会导致一路径黑色节点数量加一,而其他路径黑色节点数量不变,难以维护
  3. 非空树插入后,新增节点必须是红色节点,如果父亲节点是黑色节点,则没有违反规则,插入结束
  4. 非空树插入后,新增节点必须是红色节点,如果父亲节点是红色节点,则违反规则,要进行处理
    进一步分析,下面将当前节点称为c(cur),c的父亲为p(parent),p的父亲为g(grandfather),p的兄弟为u(uncle)。如果c,p为红,那么g必然为黑,接下来就要看u的情况了。

情况一:变色

c红,p红,g黑,u存在且为红,则将p,u变黑,g变红,再把g当做新的c继续向上更新。直到遇到父亲为黑为止

p,u均为红,g为黑,把p,u变黑,则左右两边都增加一个黑色节点,依旧符合条件,但是从根经过g的路径多了一个黑色节点,因此g要变红,随后继续向上更新。如果g的父亲为红,还需要继续处理;如果g的父亲为黑,则处理结束

上图只是其中的一种情况,实际中是有大量的情况的

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

我们给hb具体的数值,来看看这个过程的具体情况。

场景1 :hb=0,这种情况比较简单:

场景2 :hb=1,这种情况的数量一下子就激增了很多

场景3 :hb=2,这种情况的数量激增到了一个恐怖的数字

因此,我们在实际中并不需要去考虑其具体情况,只需要一个抽象的情况即可完成分析。

情况二:单旋+变色

c红,p红,g黑,u不存在或存在且为黑;若u不存在,则c一定是新增节点;若u存在且为黑,则c一定不是新增节点,是从下面更新上来的。

  • 若u不存在,假设c不是新增节点,则c原本是黑,从下面更新上来变成红,那么c所在路径黑节点数量必然会比u所在路径多,假设不成立。
  • 若u存在且为黑,假设c是新增节点,那么c祖先p在c侧的子树一定是空,因为这样c才能插入进来,而p另一侧至少有u这样一个黑,则u所在路径必然比c所在路径黑色节点多一个,假设不成立。

分析:

首先,无论是那种情况,p都必须得变黑,u不存在或存在且为黑都不能通过单纯的变色解决问题,必须进行旋转+变色

1.p为g左,c为p左

复制代码
	g
  p   u
c

处理方式:先以g为旋转点右单旋,在把p变黑,g变红即可,且不需要继续向上更新,因为旋转后新根p已经为黑且符合规则,更新结束。

2.p为g右,c为p右

复制代码
  g
u   p
      c

处理方式:先以g为旋转点进行左单旋,再把p变黑,g变红即可。

以上两种情况使红黑树依旧保持规则,p为黑,成为了子树的新根,不会产生"红红",原本根为黑,修改后根依旧为黑,且黑色节点数量不变。

下面以1和u不存在为示例:

以hb=1为具体情况:

情况三:双旋+变色

c红,p红,g黑,u不存在或存在且为黑,若u不存在,c一定是新增节点;若u存在且为黑,c一定不是新增节点,此推理与情况二一致。解决想这种连续红色的问题,都必须得是p变黑才行。

1.p为g左,c为p右

复制代码
   g
p     c
  u

处理方式:先以p为旋转点进行左单旋,再以g为旋转点进行右单旋(以g左右双旋),再把c变黑,g变红即可

2.p为g右,c为p左

复制代码
   g
u     p
    c

处理方式:先以p为旋转点进行右单旋,再以g为旋转点进行左单旋(以g右左双旋),再把c变黑,g变红即可

这两种情况与上面单旋的类似,也是保持了红黑树的结构。

下面以1和u不存在为示例:

以hb=1为具体情况:

代码实现

这里旋转的逻辑与AVL树完全一样,可参考AVL树

cpp 复制代码
bool insert(const pair<K,V>& kv)
{
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_col = BLACK;
		return true;
	}
	else
	{
		//找插入位置
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (kv.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (kv.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else return false;
		}
		//找到了
		cur = new Node(kv);
		cur->_parent = parent;
		cur->_col = RED;
		if (kv.first > parent->_kv.first)
		{
			parent->_right = cur;
		}
		else if (kv.first < parent->_kv.first)
		{
			parent->_left = cur;
		}
		else assert(false);
		//调整
		while (parent && parent->_col == RED)
		{
			//parent为红,parent必然不为根,grandfather为黑
			Node* grandfather = parent->_parent;
			Node* uncle;
			if (grandfather->_left == parent) uncle = grandfather->_right;
			else uncle = grandfather->_left;
			//判断uncle情况
			if (uncle && uncle->_col == RED)
			{
				parent->_col = BLACK;
				uncle->_col = BLACK;
				grandfather->_col = RED;
			}
			else
			{
				if (grandfather->_left == parent)
				{
					if (parent->_left == cur)
					{
						//	   g
						//	 p
						// c
						//右单旋+变色
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
						cur->_col = RED;
					}
					else
					{
						//		g
						//	 p
						//	   c
						//左右双旋+变色
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
						parent->_col = RED;
					}
				}
				else
				{
					if (parent->_right == cur)
					{
						// g
						//	 p
						//	   c
						//左单旋+变色
						RotateL(grandfather);
						parent->_col = BLACK;
						cur->_col = RED;
						grandfather->_col = RED;
					}
					else
					{
						//  g
						//     p
						//   c
						//右左双旋+变色
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						parent->_col = RED;
						grandfather->_col = RED;
					}
				}
				break;
			}
			cur = grandfather;
			parent = cur->_parent;
		}
		_root->_col = BLACK;
		return true;
	}
}

3.红黑树的查找

红黑树的查找与二叉搜索树一样,效率为O(logN)

cpp 复制代码
Node* find(const K& k)
{
	Node* cur = _root;
	while (cur)
	{
		if (k > cur->_kv.first)
			cur = cur->_right;
		else if (k < cur->_kv.first)
			cur = cur->_left;
		else return cur;
	}
	return nullptr;
}

4. 红黑树的验证

想要验证一棵树是否为红黑树,只需要检查它是否满足红黑树四条规则即可

  1. 颜色非红即黑,非黑即红,我们的实现正好保证了这一点。
  2. 根节点为黑,直接检查即可
  3. 红的孩子为黑,不存在相邻红节点,这个遇到红节点检查左右孩子非常不方便,我们反过来检查父亲节点就方便多了。
  4. 任意路径有相同数量黑节点,我们可以先走任意路径,记录黑节点数量为refnum,然后再进行前序遍历,用一个形参num记录当前遇到黑色节点的数量,形参num出了函数作用域就会被销毁,前序遍历,遇到黑就++,走到空就是一条路径的黑色节点数量,这样我们就能得到每一条路径黑色节点的数量。
cpp 复制代码
private:
	bool _IsBalanceTree(Node* root,int num,const int refnum)
	{
		if (root == nullptr)
		{
			if (num != refnum) return false;
			return true;
		}
		if (root->_col == RED && root->_parent->_col == RED) return false;
		if (root->_col == BLACK) num++;
		return _IsBalanceTree(root->_left,num,refnum) && _IsBalanceTree(root->_right,num,refnum);
	}
public:
	bool IsBalanceTree()
	{
		if (_root->_col == RED) return false;
		Node* cur = _root;
		int refnum = 0;
		//以最左路线为参考
		while (cur)
		{
			if (cur->_col == BLACK) refnum++;
			cur = cur->_left;
		}
		return _IsBalanceTree(_root,0,refnum);
	}

5. 红黑树的删除

红黑树的删除步骤及其复杂,可参考《算法导论》或《STL源码剖析》进行学习。

相关推荐
牛油果子哥q1 小时前
【C++内存对齐与结构体填充】C++内存对齐与结构体填充深度精讲:对齐规则、结构体内存大小计算、填充冗余、笔试真题与工程优化方案
开发语言·c++
ch.ju1 小时前
Java程序设计(第3版)第四章——set-get方法
java·开发语言
Lazionr1 小时前
基础算法 | 模拟算法练习
c++·算法
智能制造产品经理代码提升1 小时前
快速搭建PayPal标准API测试框架
开发语言·lua
智能制造产品经理代码提升1 小时前
Postman批量CaptureID全自动查询
开发语言·lua
爱喝水的鱼丶1 小时前
SAP-ABAP:SAP 内存管理详解:从架构到优化
开发语言·学习·架构·sap·abap·内存管理
_日拱一卒1 小时前
LeetCode:17电话号码的字母组合
java·数据结构·算法·leetcode·职场和发展
我是一颗柠檬1 小时前
【Java项目技术亮点】Outbox事件驱动模式:解决分布式事务的终极方案
java·开发语言·分布式·后端·中间件·kafka
醉颜凉1 小时前
Scala自定义Monad实战:从理论到应用的完整指南
大数据·算法·scala