C++中的红黑树

一、红黑树简介

1.1红黑树的性质

不同于AVL树的严格平衡,红黑树更多的是一种近似平衡,它符合如下性质:

①每个节点不是红色就是黑色

②根节点是黑色的

③如果一个节点是红色,那它的两个孩子是黑色的

这一条限制了一条路径没有连续的红色节点

④对于每个节点,对于该节点到其后代所有叶节点的简单路径上,均包含相同数目的黑色节点

这一条限制了每条路径的黑色节点数目相同

⑤(只用于判断路径条数)每个叶子节点都是黑色的,这里的叶子节点指的是空节点,而不是常规意义上的完整节点

这里的叶子节点又称NIL节点,在判断路径条数时候使用,每一个NIL节点都对应一条路径

综上,通过保持以上性质,红黑树可以保证:最长路径的节点个数不超过最短路径的二倍

即   最短路径长度*2  >=  最长路径长度

1.1补:空树也是红黑树

1.2一种极端情况下的红黑树

对于一颗红黑树而言,在规则限制下的最短路径为全黑的路径,而最长路径为一黑一红的路径,例如:

它拥有规则限制的两种极端情况:

其最短路径就是13->8

最长路径就是13->17->25->27

但是,实际上在一棵红黑树当中,规则限制的两种极端情况可能并不存在,此时最短路径和最长路径以实际为准

1.3红黑树与AVL树效率的比较

①在查找的时候,严格来说AVL树的效率为logn,而红黑树中最长路径的效率为2*logn,此时的AVL树效果要好,但实际在查找操作执行的时候,logn与2*logn并不会在时间上差别明显

②在插入和删除的时候,红黑树往往更具有优势

1.4红黑树节点的结构

红黑树的结构不再需要平衡因子,但是需要一个新的类型“颜色”,我们可以通过枚举来实现这一新的类型

enum Color
{
	RED,
	BLACK
};

此外还需要二叉搜索树中基本的几个属性构成一个三叉链表,因为要控制颜色,所以依旧需要存储父节点:

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

	Color _col;
};

二、红黑树的模拟实现——插入

2.1插入时的颜色设置

我们在插入一个新节点时,不论插入红色还是黑色,都会不可避免地对红黑树的性质造成改变,但假设两种不同情况并观察会发现:

①若插入黑色节点,一定会破坏规则4

②若插入红色节点,可能破坏规则3,但不一定会破坏;此外还需要注意插入根节点需要变色一次

综上,对比两种并选择一种最优的方式,我们规定新插入节点是红色节点

2.2插入时的情况分析

为了便于介绍,我们标记几个符号:

g:grandfather

p:parent

u:uncle

cur:插入的新节点

2.2.1情况一:cur为红,p为红,g为黑,u存在且为红

抽象图:

abcde为抽象内容,我们的处理方式是:p和u改成黑色,g改为红色,然后把g当成cur继续向上调整,如果g是根节点,则还需要把颜色变回黑色

原图:                                                            处理后:

①当abcde都为空的时候

此时情况较为简单,只需要按照标准变换方式对颜色进行变换即可

补:为什么要把g变红?

我们抽象图对应的可能是整棵树,也可能是一棵树的子树,

如果是一棵树的子树,g仍然为黑的话会导致该子树的黑色节点数目多1个,会违反规则4(这样其实和插入一个黑色节点造成的影响类似了)

②当cde是具有一个黑色节点的红黑树

我们应该知道,此时的cur起初不是红色,而是经过下面节点进行一次变换后,cur代替了原来的grandfather而变成红

即起初:

在经过了一次变换后才形成之前的情况,此时新节点插入的位置可能是ab四个孩子的位置

补:分析一下此时的可能数目

此时的cde均为有一个黑节点的红黑树,均有三种可能的情况,加上cur插入的四种可能

综上共有:3*3*3*4=108种可能情况

③当cde是具有两个黑色节点的红黑树

此时的cur是经过两次变换才变红的,而对于一个拥有两个黑色节点的红黑树而言,可能的情况数目会出现爆炸式增长,而更多的黑色节点难以直接计算

2.2.2情况二:cur为红,p为红,g为黑,u不存在/u存在且为黑

抽象图:

abcde为抽象内容,我们的处理方式主要是旋转,因为此时已经难以通过单纯的变换颜色来控制平衡了,这里我们暂时只抽象

p为g的左孩子

cur为p的左孩子

这种情况

原图:                                                                 处理后:

①当u不存在时,abcde一定为空

1> p为g的左孩子,cur为p的左孩子

处理方式:进行以g为parent的右单旋,旋转后p变黑,g变红

原图:                                           处理后:

       

2>p为g的左孩子,cur为p的右孩子

处理方式:进行左右双旋,旋转后cur变黑,g变红

原图:                                           处理后:

     

3>p为g的右孩子,cur为p的右孩子

处理方式:进行以g为parent的左单旋,旋转后p变黑,g变红

原图:                                           处理后:

4>p为g的右孩子,cur为p的左孩子

处理方式:进行右左双旋,旋转后cur变黑,g变红

原图:                                           处理后:

②当u存在且为黑的时候
1>  de为红或空,c为有一个黑色节点的红黑树

cur原来是黑,由情况一变过来

处理:先进行情况一的变色,然后以parent进行右单旋,旋转后p变黑,g变红

先变色:                                                     再右单旋:

补:分析一下此时的可能数目

de:可能性都有两种,红和空

新节点:有四个可能插入的位置

c:可能性有三种

综上,2*2*3*4=48种不同可能

⭐情况汇总:

观察发现,在u存在且为黑的时候进行旋转与变色的处理过程都与u不存在的时候完全一样,我们由此可以推出他们其实属于同种处理情况。(余下处理方式参考u不存在即可)

2>  de为有一个黑色节点的红黑树,c为有两个黑色节点的红黑树

cur经过两次情况一变换过来,同样会发生情况数的爆炸式增长

2.3插入的逻辑与代码实现

首先按照搜索二叉树的插入逻辑检查插入值的key是否重复,完成后进入通过颜色控制树的平衡这一过程:

大循环利用parent是否存在来控制:

2.3.1parent不存在:

插入的节点cur就是根节点,此时只需要把他的颜色变黑即可

2.3.2parent存在且为黑:

因为插入之前一定是一个标准的红黑树,所以此时parent为黑就说明循环更新已完成

2.3.3parent存在且为红:

需要继续循环处理,且因为parent为红节点而整棵树根节点应该是黑色的,所以grandfather一定存在,此时我们只需要关注uncle的情况即可

2.3.3.1uncle存在且为红:对应情况一

此时只要把parent和uncle变黑,把grandfather变红,

将grandfather置为新的cur,parent置为garandfather的parent,

再次进入循环即可

2.3.3.2uncle不存在:对应情况二第一种

此时可以确定cur就是新插入的节点,因为如果cur为更新上来的话黑色节点数目就不对了;

此时我们的处理可以按照2.2.2-①中的四种情况分别进行

2.3.3.3uncle存在且为黑:对应情况二第二种

此时可以确定cur一定经过情况一的变换,因为uncle为黑的时候,cur只有本来是黑才不会违反规则,此时cur一定会经历一个变色的过程,也就是情况一对应

此时我们的处理也可以按照2.2.2-①中的四种情况分别进行,因为在2.2.2-②中我们对情况进行总结时发现的结论在这个时候正好可以起作用

⭐2.3.3补:情况二会直接结束循环

除了情况一对应变色会导致grandfather变红,其他两种都对应情况二,在旋转之后grandfather都会变黑,即大循环已经无需再进行

2.3.4代码实现参考

bool Insert(const pair<K, V>& kv)
{

	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_col = BLACK;
		return true;
	}
	//先检查一下有没有重复
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (cur->_kv.first < kv.first)
		{
			parent = cur;
			cur = cur->_right;

		}
		else if (cur->_kv.first > kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
			return false;
	}
	cur = new Node(kv);
	//没有重复,parent此时指向要插入节点的父节点,cur直接存新节点
	if (kv.first < parent->_kv.first)
	{
		parent->_left = cur;
	}
	else
	{
		parent->_right = cur;
	}

	cur->_parent = parent;
	while (parent && parent->_col == RED)
	{
		Node* grandfather = parent->_parent;
		//如果parent是grandfather的左孩子
		if (parent == grandfather->_left)
		{
			Node* uncle = grandfather->_right;
			if (uncle && uncle->_col == RED)
			{
				parent->_col = BLACK;
				uncle->_col = BLACK;
				
				grandfather->_col = RED;

				cur = grandfather;
				parent = cur->_parent;
			}
			else
			{
				if (cur == parent->_left)
				{
					//    g
					//  p
					//c
					RotateR(grandfather);
					grandfather->_col = RED;
					parent->_col = BLACK;
				}
				else
				{
					//    g
					//  p
					//   c
					RotateL(parent);
					RotateR(grandfather);

					cur->_col = BLACK;
					grandfather->_col = RED;
				}
			}
		}
		else
		{
			//  g
			//    p
			Node* uncle = grandfather->_left;
			if (uncle && uncle->_col == RED)
			{
				parent->_col = BLACK;
				uncle->_col = BLACK;

				grandfather->_col = RED;

				cur = grandfather;
				parent = cur->_parent;
			}
			else
			{
				if (cur == parent->_right)
				{
					//  g
					//    p
					//      c
					RotateL(grandfather);
					grandfather->_col = RED;
					parent->_col = BLACK;
				}
				else
				{
					//  g
					//    p
					//   c
					RotateR(parent);
					RotateL(grandfather);

					cur->_col = BLACK;
					grandfather->_col = RED;
				}
			}
		}
	}
	_root->_col = BLACK;


	return true;
}

2.4判红黑树是否平衡

2.4.1.逻辑分析

判断的过程还是要回归到红黑树的性质上

第一点不需要刻意实现,第二点用一个判断即可,关键在第三点和第四点上

第三点我们可以通过判断每个红色节点的父节点是否是红色来完成

而第四点我们的实现思路是:要计算每条路径上黑色节点的数目,可以在每个节点位置记录一个值num,这个值代表根节点到当前节点上的黑色节点总数,

要完成整棵树的遍历并且比较所有节点,我们选择使用前序递归遍历的方式,在递归前获取一个num参考值,以便在递归过程中与所有走完路径节点对应的num进行比较

2.4.2代码参考

bool IsBalance()
{
	if (_root == nullptr)
		return true;

	if (_root->_col == RED)
	{
		return false;
	}

	// 获取参考值
	int refNum = 0;
	Node* cur = _root;
	while (cur)
	{
		if (cur->_col == BLACK)
		{
			++refNum;
		}

		cur = cur->_left;
	}

	return Check(_root, 0, refNum);
}

bool Check(Node* root, int blackNum, const int refNum)
{
	
	if (root == nullptr)
	{
		//cout << blackNum << endl;
		if (refNum != blackNum)
		{
			cout << "存在黑色节点的数量不相等的路径" << endl;
			return false;
		}
		return true;
	}
	if (root->_col == RED && root->_parent->_col == RED)
	{
		cout << root->_kv.first << "存在连续的红色节点" << endl;
		return false;
	}

	if (root->_col == BLACK)
	{
		blackNum++;
	}

	return Check(root->_left, blackNum, refNum)
		&& Check(root->_right, blackNum, refNum);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值