数据结构——红黑树

AVL树可以将查找的效率提升到logN,但是AVL树的插入节点和删除节点,为了维持高度的平衡
需要大量的旋转,而这大量的的旋转,导致非常浪费资源,于是就有人提出了新的一种树------
红黑树,红黑树在牺牲了logN查找效率的的情况下,换做不需要那么多的旋转,转而用"颜色"
来管理二叉树,,也得到了不错的效果,它的查找效率是log2N,也是logN级别的。但是少了
很多旋转
接下来也是只介绍红黑树的插入。

文章目录

1.红黑树

1). 初步认识红黑树

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或

Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路
径会比其他路径长出两倍,因而是接近平衡的。让我们先来看看红黑树:

上面这两颗树都是红黑树。而红黑树主要是以下几种性质,也算规则:

性质1. 结点是红色或黑色。
性质2. 根结点是黑色。
性质3. 所有叶子都是黑色。(叶子是NIL结点)
性质4. 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
性质5. 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。

这其中的NIL节点也可以理解为空指针。

以上五个性质,主要是性质4和性质5导致了 "红黑树确保没有一条路径会比其他路径长出两倍"。为什么呢?其实也很好验证。但在此之前我们需要再次认识"路径"这个问题,现在问你一个问题上图中的第一个红黑树的路径有多少个?不乏有人脱口而出三种,那这种认识就是错误的,正确答案是7种。这里的路径应该是根节点到每个空指针的总和,所以我会在上图画出空指针。为了方便数清路径。

那我们再次回到原来那个问题,为什么红黑树确保没有一条路径会比其他路径长出两倍呢?

性质四说明了父子的颜色关系中只有:黑红、黑黑、红黑、而没有红红

性质五保证了每条路径上的黑色节点数量一样,那么在一种极端情况下,相同黑色节点的路径下最短的路径一定是只有黑色,而最长路径一定是黑红黑红黑红...:

可以看到最左边路径无法再增加节点,为最长路径,最右路径为最短路径,也无法再减少节点。

2). 红黑树的节点

红黑树的节点中有着"颜色"来控制,并且也有指向父亲节点的指针:

cpp 复制代码
enum COLOR{
	RED,
	BLACK
};

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

	COLOR _col;

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

};

3). 红黑树节点的插入

依旧和AVL树一样开始按照二叉搜索树一样的插入:

cpp 复制代码
bool Insert(const pair<K, V>& pa)
{
	//插入节点
	if (_root == nullptr)
	{
		_root = new Node(pa);
		_root->_col = BLACK;
		return true;
	}
	else
	{
		Node* cur = _root;
		Node* parent = _root;
		while (cur)
		{
			if (cur->_kv.first > pa.first) parent = cur, cur = cur->_left;
			else if (cur->_kv.first < pa.first) parent = cur, cur = cur->_right;
			else return false;
		}

		cur = new Node(pa);
		if (parent->_kv.first > cur->_kv.first) parent->_left = cur;
		else if (parent->_kv.first < cur->_kv.first) parent->_right = cur;

		cur->_parent = parent;
		cur->_col = RED;

		//调整颜色...
		return true;
	}
}

在上面的代码中我们需要设置新增节点的初始颜色,初始颜色是红色最好,黑色会麻烦很多:

假如我们要在这里新增一个节点,如果我们增加一个红色节点,它没有破坏每条路径的黑色节点数相同,对旁支影响不大。如果是黑色节点,导致其他路径的黑色节点数都要变成三个,影响不小,需要调整整棵树。不止这个节点,任意一个空指针处插入都是如此。因此我们在设置新增节点的需要将新增节点设置为红色。

4). 红黑树的变色与旋转

我们在插入一个新增红色节点的时候,父亲节点只有两种情况,非黑即红,如果父亲节点是黑色的情况,意味着这条路径肯定没有达到最长,而且黑红的父子颜色关系也符合,没有破坏任何规则所以:

新增节红色节点的父亲节点是黑色的情况下不用处理。

那如果父亲节点是红色的话,因为在插入前这棵树一定是一个红黑树,所以此时父亲节点肯定不是根节点,并且父亲节点的父亲也就是新增节点的爷爷节点一定是黑色。而在这种情况下,前人也已经帮我们总结出规律来了,我们只需要跟着演算一下就可以,而这种情况也只需要关注父亲节点的兄弟节点(我们叫它叔叔节点)。

a. 叔叔节点存在且为红

处理方式就是把parent和uncle节点变黑,grandfather变红,cur变成grandfather继续向上调整颜色
因为当把grandfather变红之后你不知道grandfather是不是根节点,也不能确保grandfather的父亲节
点是黑色所以需要迭代调整,知道cur的父亲节点不是红色,而在最后迭代到根节点把根节点变红的
时候,需要将根节点再次变黑。

b. 叔叔节点不存在或者存在为黑

I.叔叔节点不存在

我们先来看叔叔不存在:

一目了然,需要旋转:

然后将parent变黑,grandfather变红,而且也可以看到旋转变色之后不需要继续向上调整,因为新增节点前,跟旋转变色后没有本质的区别。

那么这种呢:

需要一个双旋 + 变色:

同样,这样调整之后也不需要继续向上调整了。

II.叔叔节点存在且为黑

我们再来看叔叔节点存在且为黑的情况:

我们发现如果cur它一定是从下面调整上来的,不是新增节点,如果是新增节点的话,parent这颗子树开始是没有新增节点的,也就说明grandfather原来就不是一颗红黑树,这是不合理的。所以我们需要再把这个图画精细一些:

这其中a、b、c是三个节点的子树是一种粗略的,不是具体的某一种树。

而我们将其中一个最简单的情况举例:

而这也只需要一个右单旋 + 变色:

先对grandfather右单旋:

然后就需要变色了,这时我们将那个粗略一点的图也右单旋了:

我们分析这张图,我们可以看到a、b中至少有一个黑色节点,在有多余的黑色节点,则c中也要增加对应的黑色节点数,在上面讨论新增节点的初始颜色时也说到,尽量不要有多余的黑色节点,所以要变化,尽量在变化的过程中不要让路径上的黑色节点多出来。假设cur这颗子树有每条路径有k个黑色节点,那么b中也是同样的情况,而c中每条路径有k-1个黑色节点:

在这种情况下调整颜色的方案就有两种了

1.把cur变黑导致parent每颗子树的的黑色节点数量变成k+1个,但是parent是红色
无法保证parent的父亲节点是黑色,所以需要向上调整。
2.把parent变成黑色,grandfather变成红色,使parent每颗子树上的黑色节点变成k个。
且由于parent是黑色,所以不需要向上调整了。

那么由此观之第二种方法显然更稳定。所以我们选择第二种方式:
把parent变成黑色,grandfather变成红色:

那如果是这种情况呢?

跟上面叔叔节点不存在一样先左旋parent再右旋grandfather,然后再变色

我们再次假设a1中每条路径有k个黑色节点:

那么根据上面的变色规律我们可以得出这里的变色是cur变黑,grandfather变红。也不需要向上调整。

至此所有叔叔在右边的情况就已经说完了,剩下的就是叔叔在左边,与上述对称,不再赘述,直接上代码:

cpp 复制代码
bool Insert(const pair<K, V>& pa)
{
	if (_root == nullptr)
	{
		_root = new Node(pa);
		_root->_col = BLACK;
		return true;
	}
	else
	{
		Node* cur = _root;
		Node* parent = _root;
		while (cur)
		{
			if (cur->_kv.first > pa.first) parent = cur, cur = cur->_left;
			else if (cur->_kv.first < pa.first) parent = cur, cur = cur->_right;
			else return false;
		}

		cur = new Node(pa);
		if (parent->_kv.first > cur->_kv.first) parent->_left = cur;
		else if (parent->_kv.first < cur->_kv.first) parent->_right = cur;

		cur->_parent = parent;
		cur->_col = RED;

		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			//右树是叔叔
			if (grandfather->_left == parent)
			{
				Node* uncle = parent->_right;
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					cur = grandfather;

					parent = grandfather->_parent;
				}
				else
				{
					if (parent->_left == cur)
					{
						RotateR(grandfather);
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else
					{
						RotateL(parent);
						RotateR(grandfather);
						grandfather->_col = RED;
						cur->_col = BLACK;
					}
					break;
				}
			}
			//左树是叔叔
			else
			{
				Node* uncle = parent->_left;
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					cur = grandfather;
					
					parent = grandfather->_parent;
				}
				else
				{
					if (parent->_right == cur)
					{
						RotateL(grandfather);
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else
					{
						RotateR(parent);
						RotateL(grandfather);
						grandfather->_col = RED;
						cur->_col = BLACK;
					}
					break;
				}
			}
		}
		_root->_col = BLACK;
		return true;
	}
}

5). 红黑树的验证

cpp 复制代码
	bool Check(Node* root, int blacknum, const int refVal)
	{
		if (root == nullptr)
		{
			if (blacknum != refVal)
			{
				return false;
			}

			return true;
		}

		if (root->_col == RED && root->_parent->_col == RED)
		{
			return false;
		}

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

		return Check(root->_left, blacknum, refVal)
			&& Check(root->_right, blacknum, refVal);
	}

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

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

		int refVal = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
			{
				++refVal;
			}

			cur = cur->_left;
		}

		int blacknum = 0;
		return Check(_root, blacknum, refVal);
	}

	int Height()
	{
		return _Height(_root);
	}

	int _Height(Node* root)
	{
		if (root == nullptr)
			return 0;

		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);

		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}

	size_t Size()
	{
		return _Size(_root);
	}

	size_t _Size(Node* root)
	{
		if (root == NULL)
			return 0;

		return _Size(root->_left)
			+ _Size(root->_right) + 1;
	}
相关推荐
卡皮巴拉吖7 分钟前
【堆排】为何使用向下调整法建堆比向上调整法建堆更好呢?
数据结构
大二转专业2 小时前
408算法题leetcode--第24天
考研·算法·leetcode
zaim12 小时前
计算机的错误计算(一百一十四)
java·c++·python·rust·go·c·多项式
学习使我变快乐2 小时前
C++:const成员
开发语言·c++
凭栏落花侧2 小时前
决策树:简单易懂的预测模型
人工智能·算法·决策树·机器学习·信息可视化·数据挖掘·数据分析
Starry_hello world3 小时前
二叉树实现
数据结构·笔记·有问必答
hong_zc3 小时前
算法【Java】—— 二叉树的深搜
java·算法
吱吱鼠叔4 小时前
MATLAB计算与建模常见函数:5.曲线拟合
算法·机器学习·matlab
嵌入式AI的盲5 小时前
数组指针和指针数组
数据结构·算法
一律清风5 小时前
QT-文件创建时间修改器
c++·qt