数据结构 红黑树(RBTree)的原理与实现

Posted WhiteShirtI

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构 红黑树(RBTree)的原理与实现相关的知识,希望对你有一定的参考价值。

学习红黑树之前你应该保证你学过AVL树,也就是平衡二叉搜索树
数据结构 AVL树

AVL树是一棵高度平衡的二叉搜索树,其要求每个结点的高度差不能大于1,这样子就保证了其查询的时间复杂度为log2(N),不会出现单支树而导致时间复杂度退化到线性时间。但是AVL树的插入和删除性能非常低下,只要稍微不平衡,都需要进行旋转操作,实现起来也相对困难。所以如果当你需要查询效率高且有序时你可以有优先选择AVL树,但是如果你所需的数据结构是频繁的插入与删除,且需要高效查询且有序时,你可以优先考虑红黑树(RBTree)

红黑树与AVL树的比较:AVL树的查询效率虽然优于红黑树,但是这种差距不大,在cpu的快速调度下,这种差距可以忽略不计。而红黑树的插入与删除的性能要高于AVL树,AVL树插入一个节点或者删除一个节点都有可能需要旋转,且有可能追溯至根节点;红黑树是只有特定情况下才旋转,且很少会追溯至根节点。所以说红黑树是AVL树的一种更优的迭代版

红黑树

红黑树是一棵二叉搜索树,每个结点都增加了存储颜色的标志,这个颜色只有红色RED或者黑色BLACK。通过任意一条从跟结点到叶子结点的路径上颜色的限制,从而确保红黑树的最长路径不会超过最短路径的两倍,因此就可以达到近似平衡的效果

在这里插入图片描述

红黑树的性质

  1. 每个结点的颜色只有红色和黑色,且根节点必须为黑色
  2. 不能存在连续的红色结点,也就说如果一个结点为红色,那么它的左右孩子就必须为黑色
  3. 对于每个结点来说,从该结点到其所有后代结点的简单路径上,经过的黑色结点的数目应该是相同的

由这三条性质就能知道为什么红黑树可以保证最长路径不会超过最短路径的两倍了。如下图
在这里插入图片描述
最短路径的路径上的结点全为黑色结点,最长路径则是黑红相间,而要保证黑色结点数目相同且不能出现连续的两个红结点。从而确定了最长路径不会超过最短路径的两倍

红黑树的实现

结点信息

结点信息和AVL树类似,有5个属性,分别是父节点_parent,左孩子_left,右孩子_right,结点的值_val,颜色信息_color。其中节点都是该结点的指针,结点的值我们可以使用泛型T来实现,颜色我们可以通过bool值或者枚举来实现,这里为了提高可读性,我们使用枚举来实现。其构造函数参数就为pair,设置为缺省参数。

enum COLOR
{
	BLACK,
	RED
};

template <class T>
struct RBTreeNode
{
	RBTreeNode<T>* _parent; //父节点
	RBTreeNode<T>* _left; //左孩子
	RBTreeNode<T>* _right; //右孩子
	T _val; //键值对
	COLOR _color; //颜色

	RBTreeNode(const T& val = T())
		:_parent(nullptr)
		, _left(nullptr)
		, _right(nullptr)
		, _val(val)
		, _color(RED)
	{}
};

这里颜色为什么默认为红色呢?如果默认为黑色,那么就可以插入无论父亲是什么颜色,由于红黑树的性质,需要确保每条路径上的黑色结点个数相同,所以你此时插入了一个黑色结点,其他的路径上的黑色结点肯定会比插入新结点的这条路径少1个,调整起来就会非常繁琐。而当插入的结点为红色时,如果父节点的颜色为黑色,那么就直接插入;如果父节点为红色,那么就在该路径上进行颜色调整或者旋转操作。所以插入的结点默认为红色,调整的成本就远低于黑色


树的结构信息

只有一个成员根节点_root

template <class T>
class RBTree
{
public:
	typedef RBTreeNode<T> Node;

	RBTree()
		:_root(nullptr)
	{}
private:
	Node* _root;
};

插入

红黑树的结点插入是红黑树的重点也是难点,插入结点和AVL的插入类似,只是最后颜色修改和调整会不一样

大致的步骤分为三步

  • 寻找到要插入的结点的位置
  • 创建新结点并插入新结点
  • 修改结点颜色或者调整树的结构

前两步实现简单,和AVL树类似,这里着重讲第三步颜色修改以及结构调整。当父节点为黑色时,此时没有存在连续的两个红色结点,所以就不需要颜色修改以及结构调整;而当父节点为红色时,就会出现连续的红色结点,此时就有必要修改结点颜色或者调整结构。是修改颜色还是调整结构又分为三种情况

结点约定:cur为当前节点、parent为父节点、gfather为祖父结点、uncle为叔结点

第一种情况:爸叔通红就变色----cur为红色,parent结点为红色,uncle结点存在且为红色
调整方法:需要对该路径上的子树进行调整,parent结点和uncle结点都修改为黑色,祖父结点修改为红色,此时以祖父结点为根的子树就不存在连续的两个红色结点。但是由于祖父结点颜色也被修改了,此次需要向上追溯,判断是否还存在连续的两个红色结点。令cur = gfather结点向上追溯调整
在这里插入图片描述

第二种情况:爸红叔黑就旋转,爸红没叔也要旋,哪边黑往哪边转----cur为红色,parent结点为红色,uncle结点不存在或者为黑色。parent在gfather的左边,cur在parent的左边;或者parent在gfather的右边,cur在parent的右边

parent在gfather的左边,cur在parent的左边:以gfather结点为轴进行右旋,parent结点修改为黑色,gfather结点修改为红色

uncle结点不存在
在这里插入图片描述
uncle结点存在且为黑色
在这里插入图片描述
parent在gfather的右边,cur在parent的右边:以gfather结点为轴进行左旋,parent结点修改为黑色,gfather结点修改为红色

uncle结点不存在
在这里插入图片描述
uncle结点存在且为黑
在这里插入图片描述
第三种情况:爸红叔黑就旋转,爸红没叔也要旋,哪边黑往哪边转----cur为红色,parent结点为红色,uncle结点不存在或者为黑色。parent在gfather的左边,cur在parent的右边;或者parent在gfather的右边,cur在parent的左边

parent在gfather的左边,cur在parent的右边:先以parent结点为轴进行左旋,交换cur和parent结点,此时变成第二种情况,再对gfather结点进行右单旋,并修改parent颜色为黑色gfather颜色为红色

uncle结点不存在
在这里插入图片描述
uncle结点存在且为黑
在这里插入图片描述
parent在gfather的右边,cur在parent的左边:先以parent结点为轴进行右旋,交换cur和parent结点,此时变成第二种情况,再对gfather结点进行左单旋,并修改parent颜色为黑色gfather颜色为红色

uncle结点不存在
在这里插入图片描述
uncle结点存在且为黑
在这里插入图片描述
总结成顺口溜根结点为黑,新结点为红,只能黑连黑,不能红连红;爸叔通红就变色,没叔叔黑就旋转,哪边黑往哪边转
代码

	bool insert(const T& val)
	{
		if (_root == nullptr)
		{
			_root = new Node(val);
			//根节点为黑色
			_root->_color = BLACK;
			return true;
		}

		Node* cur = _root;
		Node* parent = nullptr;

		//1.寻找到要插入的结点的位置
		while (cur)
		{
			parent = cur;
			if (cur->_val == val)//key值相同插入失败
				return false;
			else if (cur->_val > val)
				cur = cur->_left;
			else
				cur = cur->_right;
		}
		//2.创建节点
		cur = new Node(val);
		if (parent->_val > cur->_val)
			parent->_left = cur;
		else
			parent->_right = cur;
		cur->_parent = parent;

		//3.颜色的修改或者结构的调整
		while (cur != _root && cur->_parent->_color == RED)//不为根且存在连续红色,则需要调整
		{
			parent = cur->_parent;
			Node* gfather = parent->_parent;
			
			if (gfather->_left == parent)
			{
				Node* uncle = gfather->_right;
				//情况1.uncle存在且为红
				if (uncle && uncle->_color == RED)
				{
					parent->_color = uncle->_color = BLACK;
					gfather->_color = RED;
					//向上追溯
					cur = gfather;
				}
				else
				{
					if (parent->_right == cur)//情况3
					{
						RotateL(parent);
						swap(cur, parent);
					}
					//情况2.uncle不存在或者uncle为黑
					RotateR(gfather);
					parent->_color = BLACK;
					gfather->_color = RED;
					break;
				}
			}
			else
			{
				Node* uncle = gfather->_left;
				if (uncle && uncle->_color == RED)
				{
					parent->_color = uncle->_color = BLACK;
					gfather->_color = RED;
					//向上追溯
					cur = gfather;
				}
				else
				{
					if (parent->_left == cur)
					{
						RotateR(parent);
						swap(cur, parent);
					}

					RotateL(gfather);
					parent->_color = BLACK;
					gfather->_color = RED;
					break;
				}
			}
		}

		//根节点为黑色
		_root->_color = BLACK;
		return true;
	}

	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;
		if (parent == _root)
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			Node* gfather = parent->_parent;
			if (gfather->_left == parent)
				gfather->_left = subR;
			else
				gfather->_right = subR;
			subR->_parent = gfather;
		}
		subR->_left = parent;
		parent->_parent = subR;
	}

	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;

		if (parent == _root)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			Node* gfather = parent->_parent;
			if (gfather->_left == parent)
				gfather->_left = subL;
			else
				gfather->_right = subL;
			subL->_parent = gfather;
		}
		subL->_right = parent;
		parent->_parent = subL;
	}

红黑树的验证

验证一棵树是否为红黑树,必须要满足以下条件

  1. 该树是一棵二叉搜索树,也就是中序遍历出来的结果必须是有序的
  2. 根节点必须是黑色的
  3. 每一条路径上的黑色结点个数相同
  4. 不能存在连续红色的节点

我们先针对第一个条件写出中序遍历来验证是否有序输出

	void inoder()
	{
		_inoder(_root);
		cout << endl;
	}
	void _inoder(Node* root)
	{
		if (root)
		{
			_inoder(root->_left);
			cout << root->_val << " ";
			_inoder(root->_right);
		}
	}

测试给出100个随机数
满足第一个条件:中序遍历有序
在这里插入图片描述
剩下的三个条件都是红黑树的性质,我们可以都写在一个函数中,并递归的判断子树是否也满足红黑树的性质条件

在判断每条路径上的黑色结点是否相同时,我们可以选择一条路径上的黑色结点作为基准值,然后将该值作为该树的每条路径的黑色结点数,只要有一条路径与其值不同,则表示该树不是红黑树。代码简单,注释很明确

	bool isRBTree()
	{
		if (_root == nullptr) //空树也属于红黑树
			return true;
		if (_root->_color == RED) //不满足条件二:根节点必须为黑色
			return false;

		int bCount = 0; //某条路径上的黑色结点数
		Node* cur = _root;
		while (cur)
		{
			if (cur->_color == BLACK)
				++bCount;
			cur = cur->_left;
		}
		int pathCount = 0;
		return _isRBTree(_root, bCount, pathCount);//遍历判断每条路径上的黑色结点个数是否相同
	}

	bool _isRBTree(Node* root, const int bCount, int pathCount)
	{
		if (root == nullptr)//路径走完,判断黑色结点个数
		{
			if (pathCount == bCount)
				return true;
			else
				return false;//不满足条件三: 每一条路径上的黑色结点个数相同
		}

		if (root->_color == BLACK)//遇到黑色结点就++
			++pathCount;

		Node* parent = root->_parent;
		if (parent && parent->_color == RED && root->_color == RED)
			return false; //不满足条件四:不能存在连续红色的节点

		return _isRBTree(root->_left, bCount, pathCount) //继续遍历左子树和右子树
			&& _isRBTree(root->_right, bCount, pathCount);
	}

运行结果:满足后三种条件,为红黑树
在这里插入图片描述

以上是关于数据结构 红黑树(RBTree)的原理与实现的主要内容,如果未能解决你的问题,请参考以下文章

红黑树底层实现原理分析及JAVA代码实现

二叉树之红黑树(RBTree)

红黑树(RBTREE)之上-------构造红黑树

C++ 泛型编程 实现红黑树RBTree

java——红黑树 RBTree

RBtree插入跟删除图解代码