数据结构之红黑树

一、红黑树的概念

红黑树是一棵二叉搜索树,他的每个结点增加一个存储位来表示结点的颜色,可以是红色,也可以是黑色。通过对任何一条从根到叶子的路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出两倍,因而接近平衡

1.红黑树的规则

(1)每个结点不是红色就是黑色

(2)根节点是黑色的

(3)如果一个结点是红色的,那么它的两个孩子结点必须是黑色的,也就是说,任意一条路径不会有连续的红色结点。

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

说明:《算法导论》等书上补充了每个叶子结点(NIL)都是黑色的规则。他这里所指的叶子结点不是传统意义上的叶子结点,而是我们说的空结点,有些书籍上也把NIL叫做外部结点。NIL是为了方便准确的表示出所有路径,《算法导论》在后续讲解实现的细节中也忽略了NIL结点,所以我们了解即可

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

由规则(4)可知,从根到NULL结点的每条路径都有相同数量的黑色结点,所以在极端场景下,最短路径就是全是黑色的结点的路径,假设最短路径长度为bh(black height)

由规则(2)和规则(3)可知,任意一条路径不会有连续的红色结点,所以极端场景下,,最长的路径就是由黑红间隔交替组成,那么最长路径的长度为2*bh。

综合红黑树的4点规则而言,理论上的全黑最短路径和一黑一红的最长路径并不是在每颗红黑树都存在的。假设任意一条从根到NULL结点路径的长度为x,那么bh<=h<=2*bh。

3.红黑树的效率

假设N是红黑树中结点数量,h最短路径的长度,那么2^h-1<=N<=2^2*h-1,由此推出h约等于logN,也就是意味着红黑树增删改查最坏也就是走最长路径2*logN,那么时间复杂度还是O(logN)。

红黑树的表达相对于AVL树要抽象一些,AVL树通过高度差直观的控制了平衡。红黑树通过4条规则的颜色约束,间接的实现了近似平衡,它们的效率都是同一档次,但是相对而言,插入相同数量的结点,红黑树的旋转次数的更少的,因为他对平衡的控制没有那么严格。

二、红黑树的实现

1.红黑树的结构

cpp 复制代码
//枚举值表示颜色
enum Color {
	RED,
	BLACK
};
//按Key/Value结构实现
template<class K,class V>
struct BRTreeNode {
	pair<K, V> _kv;
	int _col;
	BRTreeNode<K,V>* _parent;
	BRTreeNode<K, V>* _left;
	BRTreeNode<K, V>* _right;
	BRTreeNode(const pair<K,V>& kv)
		:_col(RED)
		,_parent(nullptr)
		,_left(nullptr)
		,_right(nullptr)
		,_kv(kv){ }
};

如上图,为红黑树结点的实现。

cpp 复制代码
template<class K,class V>
class BRTree {
	using BRNode = BRTreeNode<K, V>;
public:
	BRTree():_root(nullptr){}
private:
//红黑树的根节点
	BRNode* _root;
};

如上图,为红黑树的定义

2.红黑树的插入

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

(1)插入一个值按二叉搜索树规则进行插入,插入后我们只需观察是否符合红黑树的4条规则。

(2)如果是空树插入,新增结点就是黑色结点。如果是非空树,新增结点必须为红色结点,因为非空树插入,新增黑色结点就破坏了规则4,规则4是很难维护的。

(3)非空树插入后,新增结点必须为红色结点,如果父亲结点是黑色的,则没有违反任何规则,插入结束。

(4)非空树插入后,新增结点必须为红色结点,如果父亲结点是红色的,则违反规则3。进一步开始分析,c为红色,p为红色,g必为黑色,这三个颜色都固定了,关键看u的情况,需要根据u分为以下几种情况处理

**注意:我们把新增结点称为c(cur)c的父亲结点为p(parent),p的父亲结点为g(grandparent) ,p的兄弟结点(uncle)

2.2 情况1:变色

c为红,p为红,g为黑,u存在且为红,则将p和u变黑,g变红,将g当做新的c,继续向上更新。

分析:因为p和u都为红,g为黑,从g往下走,不是过p就是过u,将g变红,p和u变黑,即保证了g往下面走的黑色结点个数保持不变,同时将红结点向上更新,进一步处理。如果一直下去,更新到根节点,直接将其置为黑,结束。如果发现p为黑色,满足红色不与红色相连,也可结束。

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

**跟AVL树类似,我们展示了一个具体情况,但实际情况会有很多。

**如图,将上述进行抽象表示,d/e/代表每条路径都拥有hb个黑色结点的子树,,a/b代表有hb-1个黑色结点,根为红的子树。hb>=0.

2.3 情况2:单旋+变色

c为红,p为红,g为黑,u不存在或者u存在且为黑,u如果不存在,则c一定为新增结点

(原因:如果c为下面更新上来的,则下面必有黑色结点,但g->u这条路径无黑色结点了,因此必然c为新增)

u存在且为黑,则c一定不是新增结点

(原因:如果c是新增结点由g->p->c这条路径上只有g是黑色结点,但由g->u这条路径有g u两个黑色结点)

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

g

p u

c

如果p是g的左,c是p的左,那么以g为旋转带你进行右单旋,再把p变黑,g变红即可。

g

u p

c

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

2.4 情况3:双旋+变色

这种情况不是上面那种"左边红的左边红"和"右边红的右边红",需要进行双旋+变色

g

p u

c

如果p是g的左,c是p的右,那么先以p为旋转点进行左单选,让其变成"左边红的左边红",就转化为情况1了,再以g为旋转点进行右旋。

g

u p

c

如果p是g的右,c是p的左,那么先以p为旋转点进行右单旋,让其变成"右边红的右边红",就转化为情况1了,再以g为旋转点进行左旋。

3.红黑色的插入代码实现

3.1 红黑树结点的定义

cpp 复制代码
enum Color {
	RED,
	BLACK
};
//红黑树结点
template<class K,class V>
struct BRTreeNode {
	pair<K, V> _kv;//数据用pair来实现
	int _col;//颜色
	BRTreeNode<K,V>* _parent;
	BRTreeNode<K, V>* _left;
	BRTreeNode<K, V>* _right;
//初始化构造
	BRTreeNode(const pair<K,V>& kv)
		:_col(RED)
		,_parent(nullptr)
		,_left(nullptr)
		,_right(nullptr)
		,_kv(kv){ }
};

3.2 红黑树结构定义

cpp 复制代码
template<class K,class V>
class BRTree {
	using BRNode = BRTreeNode<K, V>;
public:
private:
	BRNode* _root;
};

3.3 红黑树插入方法

cpp 复制代码
	bool insert(const pair<K, V>& kv) {
        //定义待会要插入的新结点
		BRNode* Node = new BRNode(kv);
		if (_root == nullptr) {//如果根为空,直接插入,退出
			_root = Node;
			Node->_col = BLACK;
			return true;
		}
        //记录g,p,u
		BRNode* parent = _root;
		BRNode* uncle = nullptr;
		BRNode* g_parent = nullptr;
		while (parent) {//寻找要插入的地方
			//大于结点 右边走
			if (Node->_kv > parent->_kv) {
				if(parent->_right)
				parent = parent->_right;//大于,继续向右查找
				else {
                    //找到插入位置,插入,退出循环
					parent->_right = Node;
					Node->_parent = parent;
					break;
				}
			}
			else if(Node->_kv<parent->_kv){
				if(parent->_left)
				parent = parent->_left;//小于,继续向左查找
				else {
                    //找到插入位置,插入,退出循环
					parent->_left = Node;
					Node->_parent = parent;
					break;
					
				}
			}
			else {
				return  false;
			}
		}
		while (parent) {
			//防止爷爷结点为空
			if (parent->_col == BLACK || parent == _root) {
            //如果更新到父节点为黑,或者父节点为根节点,退出
				break;
			}

			else {
                //找到爷爷结点,进而找出uncle结点来进行判断
				g_parent = parent->_parent;
				if (parent == g_parent->_left) {
					uncle = g_parent->_right;
				}
				else {
					uncle = g_parent->_left;
				}
				//父亲为红
				if (uncle == nullptr|| uncle->_col == BLACK) {
					//叔叔不存在||为黑
					//要旋转加变色
					// 1.左边红的左边红->右旋
					if (parent == g_parent->_left && parent->_left == Node) {
						RotateR(g_parent);
					}
					// 2.右边红的右边红->左旋
					else if (parent == g_parent->_right && parent->_right == Node) {
						RotateL(g_parent);
					}
					//3.左边红的右边红->左右双旋
					else if (parent == g_parent->_left && parent->_right == Node) {
						RotateLR(g_parent);
					}

					//4.右边红的左边红->右左双旋
					else if (parent == g_parent->_right && parent->_left == Node) {
						RotateRL(g_parent);
					}
					else {
						assert("error");
					}
					break;
				}
				else {
					//叔叔存在且为红
                    //变色
					g_parent->_col = RED;
					uncle->_col = BLACK;
					parent->_col = BLACK;
					//继续向上更新
					Node = g_parent;
					parent = g_parent->_parent;
				}

			}
		}
		_root->_col = BLACK;
		return true;
	}

3.4 旋转方法

3.4.1 左单旋
cpp 复制代码
	//左单旋
	void RotateL(BRNode* parent) {
		BRNode* SubR = parent->_right;
		BRNode* NodeL = SubR->_left;
		//爷爷辈和左子树根节点连
		if (parent == _root) {
			_root = SubR;
			SubR->_parent = nullptr;
		}
		else {
			BRNode* pparent = parent->_parent;
			if (pparent->_left == parent) {
				pparent->_left = SubR;
				SubR->_parent = pparent;
			}
			else {
				pparent->_right = SubR;
				SubR->_parent = pparent;
			}
			
		}
		//左子树和parent左链接
		parent->_right = SubR->_left;
		if (NodeL != nullptr)
			NodeL->_parent = parent;
		//parent和左子树的根节点连
		parent->_parent = SubR;
		SubR->_left = parent;
		//SubR要变成局部子树的根节点,要变黑,
        //p要变成红,从而保证从SubR下来的黑色结点数量相同
		SubR->_col = BLACK;
		parent->_col = RED;
		
	}
3.4.2 右单旋
cpp 复制代码
	//右单旋
	void RotateR(BRNode* parent) {
		BRNode* SubL = parent->_left;
		BRNode* NodeR = SubL->_right;
		//爷爷辈和左子树根节点连
		if (parent == _root) {
			_root = SubL;
			SubL->_parent = nullptr;
		}
		else {
			BRNode* pparent = parent->_parent;
			if (pparent->_right == parent) {
				pparent->_right = SubL;
				SubL->_parent = pparent;
			}
			else {
				pparent->_left = SubL;
				SubL->_parent = pparent;
			}
			
		}
		//左子树和parent左链接
		parent->_left = SubL->_right;
		if (NodeR != nullptr)
			NodeR->_parent = parent;
		//parent和左子树的根节点连
		parent->_parent = SubL;
		SubL->_right = parent;
		
		SubL->_col = BLACK;
		parent->_col = RED;
	}
3.4.3 左右双旋&&右左双旋
cpp 复制代码
//左右双旋
void RotateLR(BRNode* pParent) {
	BRNode* parent = pParent->_left;
	BRNode* SubR = parent->_right;
	
	RotateL(parent);
	RotateR(pParent);
	pParent->_col = RED;
	SubR->_col = BLACK;
	parent->_col = RED;
}
//右左双旋
void RotateRL(BRNode* pParent) {
	BRNode* parent = pParent->_right;
	BRNode* SubL = parent->_left;
	
	RotateR(parent);
	RotateL(pParent);
	pParent->_col = RED;
	SubL->_col = BLACK;
	parent->_col = RED;
}
4.红黑树的查找

按照二叉搜索树逻辑实现即可,搜索效率为O(logN)

cpp 复制代码
	bool find(const K& k) {
		BRNode* parent = _root;
		while (parent) {
			if (parent->_kv.first > k) {
				parent = parent->_left;
			}
			else if(parent->_kv.first<k){
				parent = parent->_right;
			}
			else {
				return true;
			}
		}
		return false;
	}
5.红黑树的验证

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

1.规则(1)枚举颜色类型,天然保证了不是红就是黑

2.规则(2)直接检查根

3.规则(3)前序遍历检查(因为4会统计每条路径的黑色个数,因此用前序遍历),遇见红色结点查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲的颜色就方便多了

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

cpp 复制代码
bool Check(BRNode* root,int BN,int RefNum) {//BN为黑色的数量
	if (root == nullptr) {
		if (BN != RefNum) {
			cout << "各个路径的黑色结点不相等" << endl;
			return false;
		}
		else {
			return true;
		}
	}
	if (root&& root->_parent) {
		if (root->_parent->_col == RED && root->_col == RED) {
			cout << "存在连续两个红色节点" << endl;
			return false;
		}
	}
	if (root->_col == BLACK) {
		BN++;
	}
	
	return Check(root->_left, BN, RefNum) && Check(root->_right, BN, RefNum);
}
相关推荐
hnjzsyjyj2 小时前
洛谷 B4241:[海淀区小学组 2025] 统计数对 ← STL map
数据结构·stl map
泡沫冰@3 小时前
数据结构(18)
数据结构
苏纪云5 小时前
数据结构期中复习
数据结构·算法
初听于你5 小时前
Java五大排序算法详解与实现
数据结构·算法·排序算法
多多*5 小时前
牛客周赛 Round 117 ABCDE 题解
java·开发语言·数据结构·算法·log4j·maven
熬夜敲代码的小N6 小时前
仓颉ArrayList动态数组源码分析:从底层实现到性能优化
数据结构·python·算法·ai·性能优化
大白的编程日记.6 小时前
【高阶数据结构学习笔记】高阶数据结构之B树B+树B*树
数据结构·笔记·学习
ゞ 正在缓冲99%…7 小时前
leetcode1547.切棍子的最小成本
数据结构·算法·leetcode·动态规划
2401_841495647 小时前
【LeetCode刷题】移动零
数据结构·python·算法·leetcode·数组·双指针法·移动零