可视化红黑树详解(gif图演示,洛谷P3369 普通平衡树)

写在前面

推荐一个很实用的工具:红黑树可视化

本文参考OI wiki中的红黑树代码,读者也可以参考该篇解析(写得还是很不错的),不过OI Wiki里删除后平衡维护的Case 4和Case 5在代码细节上稍微有些问题(把 c c c, d d d 均为红色算进Case 4了,这样不会出bug,只是相当于绕了个弯)。

大部分红黑树代码都采用 rotateLeft 和 rotateRight 两个函数来进行旋转,而且在找close/distant nephew的时候也是分类讨论,这样比较麻烦。
我们其实可以使用 node *ch[2] 来表示左右孩子,ch[0] 表示左孩子,ch[1] 表示右孩子。在后续使用中,我们#define left ch[0], #define right ch[1],从而兼容用 left, right 找左右孩子的方法。这种存储方式的好处是,我们可以通过异或1来轻松切换左右分支,而不是采用三目运算符来找兄弟节点。
再考虑 rotate,其实 rotate 相当于把某个子节点往上转到其父节点的位置,因此我们可以用同一个 rotate 函数来表示左旋或右旋,使用时旋转左孩子即为 rotateLeft,旋转右孩子即为 rotateRight。

红黑树是什么

如果能把一个又一个节点积累起来,也许就能变成一棵红黑树。 ——高松灯

红黑树是一棵满足特殊性质的二叉搜索树。它的特殊性质有:

  1. 节点均为红色/黑色(顾名思义)
  2. NIL 节点(空叶子节点,有时也叫外部节点)为黑色
  3. 红色节点的子节点均为黑色
  4. 从根节点(不含根节点本身)到 NIL 节点的每条路径上的黑色节点数量相同

(注:有的教材/解析要求根节点一定是黑色,不过这个没有太大影响,根节点是红色也不会影响树的平衡性)

我们把第 4 条性质中,从某节点出发(不含节点本身),到任意 NIL 节点路径上的黑节点数,称为节点的“黑高”(black height)。
这里黑高的定义是比较反直觉的(究竟是谁定义的?),因为既要去掉节点本身,又要加上一个额外的 NIL 节点。
可以用以下递推式来加深对黑高的理解:
b h [ N I L ] = 0 bh[NIL] = 0 bh[NIL]=0
∀ s ∈ s o n ( x ) , \forall s \in son(x), sson(x), b h [ x ] = b h [ s ] + [ c o l o r ( s ) = = B L A C K ] bh[x] = bh[s] + [color(s) == BLACK] bh[x]=bh[s]+[color(s)==BLACK],中括号表示该表达式的bool值,若表达式成立则为1,表达式不成立则为0.

我们把粉色头发的ano酱作为红节点,黑色头发的Rikki作为黑节点,那么下面这棵树是一棵红黑树:
在这里插入图片描述

其中蓝色数字标注的是节点的黑高。
假如给上面这棵树的节点填入权值,就是一棵标准的红黑树:
在这里插入图片描述

红黑树的平衡性

考虑这样一个问题:一棵“黑高”为 h h h的红黑树,至少会有多少节点?(“黑高”是指根到任意NIL节点路径上的黑节点数)
其实可以用归纳法证明结果是 2 h − 1 2^h-1 2h1,这里提供另一种思路:

对于黑高确定的红黑树,我们要想节点数最少,直接全部放黑节点就可以了。因为往里面放红节点不会对黑高产生任何影响,而且由于红节点的子节点必须是黑节点,放红节点还会让总节点数变得更多。
而全为黑节点的红黑树必然是一棵满二叉树,因为根节点到任意NIL节点路径上的黑节点数相同,如果某个部分缺了一块,那这部分的黑高就会减少。
那么,黑高为 h h h的红黑树,节点最少的情况是一棵高为 h h h,全是黑节点的满二叉树(注意高是 h h h而不是 h + 1 h+1 h+1,因为计算黑高时多统计了一次NIL节点,又少统计一次自身,二者抵消了)。这样的树有 2 h − 1 2^h-1 2h1个节点。
因此,一棵“黑高”为 h h h的红黑树,至少有 2 h − 1 2^h-1 2h1个节点。( N = s i z e o f ( x ) ≥ 2 b h ( x ) − 1 N = \rm sizeof(x)\ge 2^{bh(x)}-1 N=sizeof(x)2bh(x)1

我们还知道,红节点的两个孩子一定为黑节点,所以说,根节点到NIL节点的路径上,一个红节点就必定有一个黑节点与之对应,所以红节点数一定不超过黑节点数。
因此, b h ( T r e e ) ≥ h ( T r e e ) / 2 \rm bh(Tree)\ge h(Tree)/2 bh(Tree)h(Tree)/2

根据上面两个不等式,可以推出:

一棵有 N N N个节点的红黑树,树高不超过 2 log ⁡ ( N + 1 ) 2\log (N+1) 2log(N+1)

也就是说,红黑树天生就是平衡的,树高在 O ( log ⁡ N ) O(\log N) O(logN)级别。
假如我们能在插入和删除时维护好红黑树的几条性质,我们就能得到一棵高恒为 O ( log ⁡ N ) O(\log N) O(logN)的二叉搜索树。

红黑树整体框架

红黑树类定义为:

template<typename T>
class RedBlackTree {
   
   
	private:
		struct node;
		node* root;		//根节点
		//...
	public:
		int size();
		void insert(T);
		bool remove(T);
		//...
};

节点定义为:

#define RED 1
#define BLACK 0
#define left ch[0]
#define right ch[1] 
template<typename T>
struct RedBlackTree<T>::node {
   
   
	T val;			//权值
	bool color;		//1 is red, 0 is black 
	node *father, *ch[2];
	int siz;		//子树大小
	int direction() {
   
   
		if(father == NULL)	return 0;
		return father->right == this;
	}
	node* sibling() {
   
   
		if(father == NULL)	return NULL;
		return father->ch[direction() ^ 1];
	}
	node* uncle() {
   
   
		if(father == NULL)	return NULL;
		return father->sibling();
	}
	void pushup() {
   
   
		siz = (left?left->siz:0) + (right?right->siz:0) + 1;
	}
	//......
}

其中val代表节点权值,siz代表子树大小,ch[0] 和 ch[1] 分别代表左右孩子。
direction() 表示当前节点所在分支(0为左孩子,1为右孩子),sibling(), uncle() 是在插入/删除中需要经常用到的亲戚节点。为了方便,我们统一提前写好。

旋转操作

可以参考splay树的旋转操作。这里我们不需要区分左旋和右旋,rotate(x) 表示把节点 x x x 旋转到它父亲的位置。

template<typename T>
void RedBlackTree<T>::connect(node *x, node *fa, int k) {
   
   
	if(x != NULL)	x->father = fa;
	if(fa != NULL) {
   
   
		fa->ch[k] = x;
	} else {
   
   
		root = x;
	}
}

template<typename T>
void RedBlackTree<T>::rotate(node *x) {
   
   
	//rotate x to its parent's position
	node* y = x->father;
	node* z = y->father;
	int yson = x->direction();	//the branch of x, 0 is left, 1 is right
	if(z == NULL) {
   
   
		root = x;
		x->father = NULL;
	} else {
   
   
		int zson = y->direction();
		connect(x, z, zson);
	}
	connect(x->ch[yson^1], y, yson);
	connect(y, x, yson^1);
	y->pushup();
	x->pushup();
}

插入操作

从今天开始,我们就是一起演奏音乐的命运共同体! ——丰川祥子

红黑树的插入与普通的 BST 的插入操作类似。
我们将新节点作为红节点插入到树中对应位置,再根据相关节点状态进行调整,使整棵树满足红黑树的性质。
具体地说,红黑树要求红节点的子节点均为黑节点,而插入一个红节点可能会使父节点和子节点均为红色,所以在插入后,我们需要进行双红修正。
插入操作的代码实现如下:

template<typename T>
void RedBlackTree<T>::insert(T v) {
   
   
	node *x = root, *fa = NULL;
	while(x != NULL) {
   
   
		x->siz++;
		fa = x;
		if(v < x->val) {
   
   
			x = x->left;
		} else {
   
   
			x = x->right;
		}
	}
	x = new node(v, RED, fa);	//create a new node
	if(fa == NULL) {
   
   
		root = x;
	} else if(v < fa->val) {
   
   
		fa->left = x;
	} else {
   
   
		fa->right = x;
	}
	SolveDoubleRed(x);
}

双红修正

不过,为了下次不失败而努力不就好了?就算失败一次,也要有重来的信心。 ——千早爱音

上面的插入过程中,可能会出现父节点和子节点都是红色的连续双红情况,这违反了红黑树的性质。
在 SolveDoubleRed(x) 函数中, x x x 为红节点,我们检查 x x x 的父节点是否为红节点,如果是,则进行修正。(也就是说,这里 x x x 表示连续双红节点的子节点)
修正时,我们要保证红黑树的性质成立,即不出现连续的双红节点,以及保证黑高相同。

在下面的所有注释中,我们用<X>来表示红节点,[X]表示黑节点,{X}表示任意颜色的节点。

Case 1, 2

x x x 为根(父节点为空),或 x x x 的父节点为黑,此时无需修正。下面都是需要修正的情况。

Case 3

x x x 的父节点 p p p 为根节点,此时把 p p p 染黑即可。

if(p == root) {
   
   
	// Case 3: Parent is root and is RED
	//   Paint parent to BLACK.
	//    <P>         [P]
	//     |   ====>   |
	//    <X>         <X>
	p->color = BLACK;
	return;
}

Case 4

x x x (图中的 N)的父节点 p p p,叔节点 u u u 均为红色。由于该树原本是一棵合法的红黑树,所以 x x x 的祖父节点 g g g 一定是黑色。
在这里插入图片描述

此时我们将 p p p u u u 染黑,将 g g g 染红,这样在 g g g 以下就不会有连续的红节点。
由于插入前 g g g

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值