C++进阶——红黑树

1.红黑树的概念及其介绍

红黑树是一种近似平衡的二叉搜索树,与AVL树极为相似,红黑树的主要特点在于它通过约束树中节点的颜色和其他规则,确保树的高度始终接近对数时间复杂度,从而使常见操作(如插入、删除、查找)能够在 O(log n) 时间内完成。

2.红黑树的特性

红黑树的每个结点上都有一个存储位,用于表示该结点的颜色,节点的颜色只有两种,分别是RED(红色)或BLACK(黑色),这也是为什么其名为红黑树的原因。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近近似平衡。

红黑树具有以下特性:

  1. 结点不是红色就是黑色。
  2. 根节点是黑色的。
  3. 不能有连续的红色节点。
  4. 对于每个结点,从该结点到其所有后代叶结点的路径上,均包含相同数目的黑色结点。
  5. 每个叶子的左右空结点都算做为黑色。

2.1红黑树的节点着色

红黑树通过节点着色来控制较高一侧树高不会超过另一侧树高的两倍。其原理是由于不能有连续的红色节点,而每个节点到其后代所以叶子节点的黑色节点数都是一样的,所以一个节点的最长路径最多只能是红黑节点交替的,而最短的路径最短也只能是连续的黑色节点,所以对于一个节点的最长路径是必然不可能会大于其最短路径的两倍的,通过这种方式红黑树控制树的高度差,可将二叉搜索树近似看作为一颗平衡二叉树。

3.红黑树的基本结构

在了解红黑树之前,我想介绍一下使用于红黑树中的STL容器:pair ,pair对于红黑树十分重要,需要我们了解并知晓如何使用。

3.1pair容器的使用

pair 是定义在 头文件 <utility> 中的一个模板类,常用于表示一个二元组或元素对,且其提供了按照字典序对元素对进行大小比较的比较运算符模版函数。

pair的使用

pair< 类型1,类型2> 类名 

pair容器中存放两个值,从左往右依次是,K值、V值。

两个值的访问通过:类名.first  和  类名.second 来访问。

在创建时可不赋值通过默认构造来创建,或者带有参数的构造函数创建一个空对象,也可通过相同类的对象拷贝构造的去赋值创建。

pair重载运算符的使用

pair的运算符的使用的逻辑是按照先依次比较first、second的数据来判断使用的,只有当两个pair对象的first、second两个值都相等时,才算相等,而当比较大小时,只要first大的就更大,当first一样大时再通过比较second中的值来比较确定大小。

make_pair( ) 函数

对于pair的使用显得较为麻烦,每次都要指明pair中存储的两个值的类型,而make_pair( )函数通过封装使用pair类的使用可以不用的显示两个存储值的类型。

需要注意的是make_pair()函数的本质是一个函数,通过两个参数值创建对应的pair类型的对象作为函数的返回值

3.2红黑树的创建

红黑树相比AVL树,取消了平衡因子这一概念,引入了树节点着色的方式来限制左右树高,其它基本结构与AVL树基本一致。

enum  Color
{
	RED,
	BLACK,
};

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

	Color _col;
	pair<K, V> _kv;

	RBTreeNode(const pair<K, V> kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _col(RED)
		, _kv(kv)
	{}
};

template<class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
private:
	Node* _root = nullptr;
public:
    //成员函数
}

在红黑树中,我们使用了pair类作为红黑树的成员变量,此外,通过枚举enum来定义每个树节点的数颜色。

3.3红黑树的数据插入

红黑树的数据插入同样是最需要我们重点关注的地方,需要我们理解其控制数高度的方式,以及树节点的着色。

对于节点的着色,除了根节点以外,其他新节点的插入都默认是红色着色,其节点的插入的难点在于维持每条路径上的黑树节点数是相同的,为了维持这个特性,红黑树同样需要用到AVL树中的旋转调整树来完成对节点颜色的控制,从而达到控制树高的目的,但不同于AVL树的平衡因子,红黑树在旋转调整树的操作之后需要更改树节点的颜色,这个对于不同情况的处理不同。

bool insert(const pair<K, V>&  kv)
{
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_col = BLACK;
		return true;
	}
	Node* cur = _root;
	Node* parent = nullptr;

	while (cur)
	{
		if (cur->_kv.first > kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (cur->_kv.first < kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			return false;
		}
	}

	cur = new Node(kv);
	cur->_parent = parent;
	if (cur->_kv.first > parent->_kv.first)//cur为parent右节点
	{
		parent->_right = cur;
	}
	else //cur为parent左节点
	{
		parent->_left = cur;
	}

	//修改颜色
	cur->_col = RED;//新节点着色默认为红色
	while (parent && parent->_col == RED)
		{

		Node* grandfather = parent->_parent;
        //由于parent为红色,一定有上一节点,所以不需要讨论grandparent的存在情况
		if (parent == grandfather->_left)//parent为左子树
		{
			Node* uncle = grandfather->_right;
			if (uncle && uncle->_col == RED)//uncle存在且为红
			{
				uncle->_col = parent->_col = BLACK;
				grandfather->_col = RED;

				cur = grandfather;
				parent = cur->_parent;
			}
			else//uncle不存在,或者存在为黑
			{
				if (cur == parent->_right)
				{
					RotateL(parent);
					std::swap(cur, parent);
				}
				RotateR(grandfather);
				grandfather->_col = RED;
				parent->_col = BLACK;
				break;
			}
		}
		else if(grandfather && parent == grandfather->_right)//parent为右子树
		{
			Node* uncle = grandfather->_left;
			if (uncle && uncle->_col == RED)//uncle存在且为红
			{
				uncle->_col = parent->_col = BLACK;
				grandfather->_col = RED;

				cur = grandfather;
				parent = cur->_parent;
			}
			else//uncle不存在,或者存在为黑
			{
				if (cur == parent->_left)
				{
					RotateR(parent);
					std::swap(cur, parent);
				}
				RotateL(grandfather);
				grandfather->_col = RED;
				parent->_col = BLACK;
				break;
			}
		}
	}
	_root->_col = BLACK;
	return true;
}

为了更加方便的表述各个节点间的关系,我们使用:

cur表示待插入的新节点(或调整起始节点),p节点(parent)为cur的上一节点,g节点(grandparent)为p的上一节点,u节点(uncle)为g节点的另一个孩子节点

根据处理的情况不同,大致分为以下几种情况:

  1. 当根节点为空时,此时cur充当根节点。
  2. p节点为黑色时,此时不需要做额外处理

p节点为红色时(由于不能有连续红色节点,g节点必定黑色)需要额外讨论以下几种情况

  1. 当u节点存在且为红色时
  2. 当u节点不存在或存在且为黑色时(此种情况处理相同)

 注:

对于根节点为空或p节点为黑色时,较为简单,不做讨论,对剩余几种情况特殊分析。需要注意的是,以下分析的情况均是p节点为g节点的左孩子节点的情况,对于p为g的右孩子节点的情况处理逻辑是一样的。

1.p为红色,u为红色(不需要旋转操作)

对于p节点,u节点都为红色的情况,我们需要将p、u两个节点都变为黑树,将g节点变为红色

这样可以确保p、u以下的节点各个路径的黑树节点数量是一致的,但是由于g节点的颜色发生了变化,这可能导致g节点的颜色违反了红黑树的条件约束(不能有连续的红色节点),因为我们并不知道p的上一节点的颜色究竟是红色还是黑色,所以需要对从g节点继续往上调整及节点颜色,只需要将cur改变为指向g节点即可,让其继续循环调整。

 2.p为红色,u为黑色or不存在(需要旋转操作)

 对于p节点为红色,u节点为黑树或不存在时,需要通过旋转的操作来调整树高,而由于cur节点的左右位置对旋转的操作又有着不同的情况,这需要分两种情况,当cur节点的左右位置与p节点相同时(指节点cur、p同时为左节点或右节点),此时只需要1次旋转操作+调色即可当cur节点左右位置与p节点不同时需要使用额外1次旋转操作来使得cur、p节点的左右位置相同,变为只需要旋转1次的情况

 这里红黑树的旋转与AVL树的旋转操作是完全一致的,例如上图图是较为复杂的一种情况,cur、p节点左右位置不同,此时旋转调整节点为p节点,与AVL树中的旋转调整节点相同,下图所对应的是左旋的操作,通过将cur替代p节点的位置,然后将cur的左子树链接到p节点的右节点,p节点再链接到cur的左子树上,完成左旋转的操作。

通过旋转的操作使得cur、p节点的左右位置相同后,我们还需要做一个额外处理:通过交换cur以及p节点处的指针所指向的节点(为了使得此次旋转过后情况与只需要旋转1次的情况完全相同,使得cur、p的位置变为我们熟悉的位置,如下图的cur、p左右位置相同的情况,此时再对g节点旋转一次(右旋),将p节点替代g的位置,p的右子树链接到g的左子树上,再将g连接到p的右子树上,最好通过调色,将p节点颜色变为红色p节点变为黑色,即完成了对红黑树的调整。

有关红黑树数据插入的总结 

由于红黑树旋转的操作与AVL树中的旋转是一模一样的,所以在这里并没有过多讲解,而对于另一种p节点位于右子树的情况处理逻辑是一样的,可以画图捋一下这另一种情况。

红黑树的数据插入的操作是十分复杂的,需要我们仔细理解,通过多画图我们才能够更好去理解,只有动手画图尝试自己去理解才能够更好的了解红黑树对于数据插入的这一过程。

4红黑树与AVL树

提到红黑树,难免会让人想到AVL树,两数十分相似,但是红黑树的出现却基本取代了AVL树,相比于ALV树,虽然红黑树的查找数据的效率可能略低一点(实际差距不大),但是红黑树对于修改数据的效率要更加高效。 

红黑树的底层实现逻辑大致与AVL树是相同的,都是通过子树的旋转来控制两侧树高,但不同于AVL树,AVL树是一颗十分严格的平衡二叉搜索树,其子树的高度差都不能超过2,也正是因为这种严格的平衡要求,使得AVL树的对于数据的修改需要通过大量的旋转操作来完成,从而降低了AVL树的效率,而红黑树对于树高的控制显得更加不那么严格,红黑树中的左树树高只要满足一侧树高不超过另一侧的两倍即可。

总得来说,相比于AVL树,红黑树除了查找的效率略低,对于数据的删除插入等修改操作是更加高效的,而红黑树之所以能够取代AVL树也主要是由于对数据的修改效率高,更加全能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值