CD80.【C++ Dev】模拟实现红黑树的插入

目录

1.知识回顾

2.对比AVL树和红黑树

3.插入的模拟实现

结构

插入函数

插入结点的父结点是黑色

插入结点的父结点是红色

1.如果uncle存在且为红色

2.如果uncle不存在

3.如果uncle存在且为黑色

总结

分析抽象图

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

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

情况3: cur为红,p为红,g为黑,u不存在

代码

测试代码

4.附: 红黑树的插入动图

5.其他说明

6.附:红黑树可视化网站


1.知识回顾

在C++ Contest专栏提到过红黑树,可回顾:

CC41.【C++ Cont】红黑树的简单了解

CC42.【C++ Cont】STL库的红黑树set和map的使用

在C++ Develop专栏提到过AVL树,可回顾:

CD76.【C++ Dev】AVL的模拟实现(1) 以左单旋为切口,分析旋转规律

CD77.【C++ Dev】AVL的模拟实现(2) 继续编写插入函数

CD78.【C++ Dev】以AVL项目的bug讲讲调试技巧

红黑树的性质:

1.前提是二叉搜索树
2.根结点以及叶子结点是黑色的

        ★注意:这里的叶子结点不是常规意义上的叶子结点,而是指空结点

3.红色结点的左右孩子必须是黑色的,换句话说,任意一条路径中,不会存在连续的红色结点
4.任一结点到叶子结点的路径上,黑色结点的数量相同

满足了所有的性质,就满足了最长路径不超过最短路径的2倍

2.对比AVL树和红黑树

AVL树红黑树
特点左右子树的高度差的绝对值不超过1  最长路径不超过最短路径的2倍
插入N个结点后大致的高度logN(最多高度次)2*logN(按照最短路径和最长路径的关系来估计)
效率上几乎没有差别

可以发现红黑树和AVL树在插入N个结点大致次数上的性能是同一个量级的,增删改查的时间复杂度都是O(logN)

红黑树相对于AVL树来说,牺牲了部分平衡性以换取插入/删除操作时少量的旋转操作,整体来说在经常进行增删的结构中,红黑树的性能要优于AVL树

3.插入的模拟实现

重点关注红黑树的染色方法和旋转过程即可

结构

先写红黑树结点的结构,内部包含三叉链(左指针、右指针、父指针),存储的pair数据,结点的颜色

而结点的颜色可以使用枚举语法

enum color
{
	RED,
	BLACK
};
template<class K,class V>
class rbtree_node
{
public:
	rbtree_node<K, V>* _left;
	rbtree_node<K, V>* _right;
	rbtree_node<K, V>* _parent;
	pair<K, V> _pack;
	color _col;
};

注: 枚举语法参见67.【C语言】枚举类型文章

红黑树的结构(未写成员函数):

template<class K, class V>
class rbtree
{
	typedef rbtree_node<K, V> node;
private:
	node* _root;
};

插入函数

因为红黑树符合二叉搜索树的性质,因此先借用CD77.【C++ Dev】AVL的模拟实现(2) 继续编写插入函数文章的插入函数,后续再修改

对于空的红黑树,一开始插入的结点必定为根结点

问题1: 根结点是什么颜色?

由红黑树的性质可知,根结点是黑色,因此:

bool insert(const pair<K, V>& kv)
{
	if (_root == nullptr)
	{
		_root = new  node(kv);
		_root->_col = BLACK;
		return true;
	}
    //......
}

问题2: 插入的结点优先考虑什么颜色?

红黑树中重要的性质是:

        3.红色结点的左右孩子必须是黑色的,换句话说,任意一条路径中,不会存在连续的红色结点
        4.任一结点到叶子结点的路径上,黑色结点的数量相同

会发现性质4非常苛刻,要求从任意结点出发,每条路径的黑色结点的数量相同,因此插入黑色结点可能要对树做较大的改动,比较麻烦,影响大

而插入红色结点的影响小,因为"红色结点的左右孩子必须是黑色的",只影响当前路径,不影响其他路径

插入结点的父结点是黑色

借用Sun_TTTT博主的图,如果父结点是黑色的,那么能顺利插入红结点,不用进行变色或者旋转的

综上所述,优先插入红色结点

那么有:先找到插入的位置,默认是红色结点

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);
cur->_col = RED;
//......

插入结点的父结点是红色

需要看叔叔结点uncle,一共就三种情况

parent、uncle和grandparent之间的关系:

1.如果uncle存在且为红色

在22的左侧插入新结点,如何修改结点的颜色?

上图违反了"不会存在连续的红色结点",那么parent和cur必须有一个要变黑

1.如果cur变黑

违反了"任一结点到叶子结点的路径上,黑色结点的数量相同"

13→17→25→27→NIL,一共3个黑色结点

13→17→15→插入的节点→NIL,一共4个黑色结点

2.如果parent变黑

此时必须把uncle变黑且把grandparent变红才能不违反"任一结点到叶子结点的路径上,黑色结点的数量相同",保持黑色结点的平衡,即局部的子树里面红黑结点的数量不变,且连续的红节点问题暂时解决了

解决方法: parent和uncle都变黑,grandparent变红

但仍然违反"不会存在连续的红色结点",需要继续向上调整

1. grandparent没有父结点,就是根,grandparent变黑即可,这样满足"根结点以及叶子结点是黑色的"
2. grandparent有父结点,父亲是黑色,相当于"插入结点的父结点是黑色"的情况,此时结束调整
3. grandparent有父结点,父亲是红色,和刚才是类似问题处理→更新4个指针的位置,继续调整

先移动指针:

cur先移动到grandparent的位置,那么就可以定位出parent、grandparent、uncle指向的节点

解决方法: parent和uncle都变黑,grandparent变红(即"反色":parent、uncle、grandparent变成与自身颜色相反的颜色)

发现grandparent没有父结点,就是根,那么grandparent变黑即可:

调整完的结果:每条路径都增加了一个黑色结点

2.如果uncle不存在

在22的右侧插入新结点

违反"不会存在连续的红色结点",而且单纯变色无法解决问题:

假设将parent变黑,grandparent变红:

违反"不会存在连续的红色结点",继续向上处理:

违反"任一结点到叶子结点的路径上,黑色结点的数量相同",所以单纯变色无法解决问题,需要旋转

25的左子树高于右子树,需要右旋来降低左子树高度,提升右子树高度

cur指向结点的值介于17~22之间,那么有:

违反"不会存在连续的红色结点",将22染成黑色,25染成红色后,满足红黑树的所有性质

3.如果uncle存在且为黑色

下图画框的4个位置任意插入结点,向上调整时,都为使uncle存在且为黑

例如以下这种情况:

第一次调整:如果uncle存在且为红色,更改颜色+调整4个指针后:

此时违反"不会存在连续的红色结点",单纯染色无法解决问题,需要旋转

以grandparent为旋转点进行左旋:

再变色:

会发现两次调整时parent指向的结点的颜色都要变黑,如果uncle无法和父亲一起变黑,就要考虑旋转

总结

1. uncle结点存在且为红,变色 + 继续往上更新(不需要旋转)

2. uncle结点不存在或uncle存在且为黑,先旋转再变色,然后结束调整

分析抽象图

先约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点

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

注:1. 上图看到的以g为根节点的树可能是一棵完整的树,也可能是一棵子树 2.不讨论a、b、c、d、e子树的高度,没有意义

出现连续的红节点(p和cur),需要变色(p和u都变黑,g变红) + 继续往上更新(不需要旋转)

        特别提醒:如果g是根节点,将g变黑; 如果g不是根节点,继续调整,保持g为红不变

如果g是根节点,将g变黑

如果g不是根节点,继续向上调整,将g改成cur,剩下的g、p、u依次对应即可

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

同理,1. 上图看到的以g为根节点的树可能是一棵完整的树,也可能是一棵子树 2.不讨论a、b、c、d、e子树的高度,没有意义

出现连续的红节点(p和cur),需要变色(p变黑,g变红) + 旋转(四种方式),其实反过来也可以,但不需要继续往上更新

注: 1. u本身就为黑,不需要变色

      2. 旋转的四种方式参见CC40.【C++ Cont】二叉搜索树和平衡二叉树文章

情况3: cur为红,p为红,g为黑,u不存在

操作方法和情况2一样,不再赘述

代码

出现连续的红节点(cur和parent都为红,前提是parent存在),需要调整

框架:

//条件不能颠倒顺序,因为短路运算
//不用判断cur->_col是否为RED,因为默认插入的结点为红色
while (parent && parent->_col == RED)
{
	//调整
}

写while循环的内部代码:

有了cur和parent,可以得出grandparent和uncle

if (parent == grandparent->_left)
{
	node* uncle = grandparent->_right;
	//......
}
else
{
	node* uncle = grandparent->_left;
	//......
}

写入3种情况:

while (parent && parent->_col == RED)
{
	node* grandparent = parent->_parent;
	//讨论uncle是左节点还是右节点
	if (parent == grandparent->_left)
	{
		node* uncle = grandparent->_right;
		//uncle存在且为红
		if (uncle && uncle->_col == RED)
		{
			//变色(parent和uncle都变黑, grandparent变红)
			parent->_col = uncle->_col = BLACK;
			//由于进入循环的条件满足parent不为空,因此grandparent一定不是根节点
			grandparent->_col = RED;

			//继续向上处理
			cur = grandparent;
			parent = cur->_parent;
		}
		else//uncle不存在或存在且为黑
		{
			if (cur == parent->_left)
			{
				//旋转前示意图:
				//		         ......
				//          grandparent
				//			/			\
				//      parent   uncle(可能不存在)
				//    /
				//  cur

				RotateR(grandparent);
				parent->_col = BLACK;
				grandparent->_col = RED;
			}
			else//cur == parent->_right
			{
				//旋转前示意图:
				//		         ......
				//          grandparent
				//			/			\
				//      parent   uncle(可能不存在)
				//          \
				//         cur
				RotateL(parent);
				RotateR(grandparent);
				cur->_col = BLACK;
				grandparent->_col = RED;
			}
			break;//停止调整
		}
	}
	else
	{
		node* uncle = grandparent->_left;
		//uncle存在且为红
		if (uncle && uncle->_col == RED)
		{
			//变色(parent和uncle都变黑, grandparent变红)
			parent->_col = uncle->_col = BLACK;
			//由于进入循环的条件满足parent不为空,因此grandparent一定不是根节点
			grandparent->_col = RED;

			//继续向上处理
			cur = grandparent;
			parent = cur->_parent;
		}
		else//uncle不存在或存在且为黑
		{
			if (cur == parent->_right)
			{
				//旋转前示意图:
				//		         ......
				//          grandparent
				//			/			\
				//      parent   uncle(可能不存在)
				//    /
				//  cur

				RotateL(grandparent);
				parent->_col = BLACK;
				grandparent->_col = RED;
			}
			else//cur == parent->_right
			{
				//旋转前示意图:
				//		         ......
				//                 grandparent
				//			/			                      \
				//      uncle(可能不存在)    parent
				//                                             \
				//                                           cur
				RotateR(parent);
				RotateL(grandparent);
				cur->_col = BLACK;
				grandparent->_col = RED;
			}
			break;
		}
		//不用判断parent有没有父节点,统一将_root变为黑即可
		_root->_col = BLACK;
	}
}

注: 其中RotateR和RotateL函数借用了CD76.【C++ Dev】AVL的模拟实现(1) 以左单旋为切口,分析旋转规律CD77.【C++ Dev】AVL的模拟实现(2) 继续编写插入函数文章的代码

节点的构造函数:

rbtree_node(const pair<K, V>& kv)
	:_pack(kv)
	,_left(nullptr)
	,_right(nullptr)
	,_parent(nullptr)
	,_col(RED)
{}

测试代码

#include "rbtree.h"
#include <utility>
int main() 
{
	rbtree<int, nullptr_t> rbt;
	for (int i = 0; i < 10; i++)
		rbt.insert(make_pair(i, nullptr));
}

运行结果:退出代码为0

退出代码为0不代表插入没有问题,下篇文章将讲解如何判断二叉搜索树是否是红黑树

4.附: 红黑树的插入动图

以升序插入构建红黑树:

以降序插入构建红黑树:

随机插入构建红黑树:

5.其他说明

注: 红黑树的删除不做说明,可参考《算法导论》或者《STL源码剖析》

6.附:红黑树可视化网站

cs.usfca.edu Red/Black Tree

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

zhangcoder

赠人玫瑰手有余香,感谢支持~

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

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

打赏作者

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

抵扣说明:

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

余额充值