【数据结构取经之路】图解红黑树
目录
前言
红黑树的概念
红黑树的性质
红黑树结点的定义
左右旋动图
红黑树的插入分析
红黑树的插入代码
红黑树与AVL-tree比较
红黑树的应用场景
前言
AVL-tree之外,另一个颇具历史且被广泛使用的平衡二叉搜索树是红黑树(RB-tree),这名字听起来除了很抽象外,还颇具大哥味哈,会不会很难?客观地说,难度是有的,下面我们细细道来。
红黑树的概念
红黑树(Red Black Tree)本质上就是一棵平衡二叉搜索树。这种数据结构通过增加节点颜色的维度(红色或黑色),并在插入、删除等操作中执行特定的旋转和重新着色操作,以保持树的平衡,从而确保高效的查找、插入和删除性能。红黑树通过对任何一条从根到叶子结点的路径上各个节点着色方式的限制,确保没有一条路径会比其他路径长出两倍,因而接近平衡。
看到这你是否有这样的疑问:通过着色也可以控制树的平衡?!太不可思议了吧!
当然,仅仅通过着色它还真无法做到平衡,它还在其他方面做了一些限制,下面我们来看看红黑树的性质,就可以揭开这个谜团。
红黑树的性质
只顾着说红黑树,不见识见识总感觉不太好,下面我们正式把红黑树给请上来。
红黑树有以下的几个性质,可以对照着上图验证。
● 每个节点不是红色就是黑色。(嘿嘿,我没在开玩笑)
● 根结点是黑色的。
● 任意一条路径中,没有连续的红色节点。
● 每条路径上的黑色结点数量是相同的。
● 每个叶子结点都是黑色的(这里特指的是空节点NIL)。
以上就是红黑树的性质了,那么我的问题来了:为什么满足以上的性质,红黑树就能保证其最长路径中结点个数不会超过最短路径结点个数的两倍呢?这里我就不卖关子了,直接分析。
全由黑色节点组成的路径就是最短路径。最长路径就是在各个黑色结点间插入红色结点。根据上面的性质,不能有连续的红色结点,所以每两个黑色结点间只能插入一个红色结点,故最长路径不会超过最短路径的两倍。有图有真相,请看:
根据上面的性质,根节点是黑色的,所以不管是最长路径还是最短路径的开端都是黑色结点。同时,不管是最长路径还是最短路径,每条路径上的黑色结点数量相等。关于上面的问题,就解释到这。
红黑树结点的定义
//结点的颜色
enum Color{RED, BLACK};
//红黑树结点的定义
template <class T>
struct RBTreeNode
{
RBTreeNode<T>* _left; //结点的左孩子
RBTreeNode<T>* _right; //结点的右孩子
RBTreeNode<T>* _parent;//结点的父节点
Color _color; //结点的颜色
T _data; //结点的数据域
RBTreeNode(const T& data = T(), const Color color = RED)
:_left(nullptr), _right(nullptr), _parent(nullptr)
,_color(color), _data(data)
{}
};
以上就是红黑树节点定义,但有两个问题:为什么需要指向父节点的指针?为什么要将结点的默认颜色设置成黑色?
先来回答第一个问题,在旋转时需要访问父节点,所以引入了指向父节点的指针。
第二个问题,之所以结点的默认颜色设置成黑色,是因为要确保黑色结点平衡(上面的性质4)。我们试想一下,如果插入的新节点默认颜色是黑色的,那么会导致黑色结点失衡,影响到所有路径。
左右旋动图
为了帮助大家理解左旋和右旋,这里给出两张动图,希望对你有帮助。
左旋:
右旋:
红黑树的插入分析
相对于AVL-tree来说,红黑树比较抽象。因为AVL-tree是通过直接控制高度来维持平衡的,这一点我们都很好理解。而红黑树呢,它是通过颜色控制来间接维持平衡的,要是不了解,我们就很有可能有这样的疑问:颜色和高度有什么关系呢?!疯了?!
虽然红黑树抽象,但不至于抽象到理解不了的地步,下面我在讲解的时候也会插入一些图片来供大家参考学习。所以,我们开始吧!
红黑树,本质上也属于二叉搜索树,所以它在插入时也要先找到自己的落脚点才能插入。所以我们可以高度的概括为:
● 按照二叉搜索树的方式插入新节点。
● 检测新节点插入后是否破坏了红黑树的规则,若满足则大功告成,否则就对红黑树进行调整。
在开始下面内容之前,我们先在命名上做个约定:cur——当前结点,p——父节点,g——祖父结点,u——叔叔节点,gg——祖父结点的父节点(g的父节点)。
这张图是为了让大家理解上面声明的内容,理解它们之间的关系,请忽略颜色。
下面我们进入本文最头疼的部分,插入的情形分类,这里既要细心又要耐心。
①cur为红,p为红,g为黑,u存在且为红(如下图)。
首先说明一下gg这一节点,它可能存在,也可能不存在。如果gg存在,颜色可能是黑色也可能是红色。因为我们这里所看到的树,可能是一棵完整的树,即g为整棵树的根节点,也可能是一棵子树,即g为gg结点的左子树或者右子树。 这里要好好理解。对于这种情况,很显然已经违背了红黑树的性质——任何路径中都不能有连续的红节点。那么我们如何调整呢?
解决方案:把p和u的颜色设为黑,g的颜色置为红(如下图)。②②
这就好啦?!未必!注意啦,这里将要分出3中情况。
a. 如果gg的颜色为黑(如下图),那么万事大吉。
b. 如果gg的颜色为红(如下图),那么就出现了连续的红节点,需要继续向上调整。
此处的变化是把g当成cur,然后继续向上调整。继续向上调整的情形就是我们后面所要讲到的,在此处我们只需要知道在这种情况下还需要向上调整即可,切记不可恋战,否则容易走火入魔。这种情况会转化为②的a分支所对应的两种情况(u存在且为黑或u不存在,u不可能是存在且为红,因为这会导致原来的树不满足红黑树的规则)。
c. 如果gg结点不存在,也就是说g就是整棵树的根节点(如下图),我们只需要将根节点设为黑色即可——根节点是黑色的。
② cur为红,p为红,g为黑,u不存在或者存在且为黑(如下图)。
这是我们的第二大类,老铁们可不能被上面列举的一堆情形给整懵了呀。
当u不存在时,cur一定是新增节点。如果cur不是新增节点,那么原有的红黑树就存在连续的红节点,违反了规则。然而我们要确保在插入新节点前为红黑树,所以cur只能是新增节点。
当u存在且为黑时(u存在且为红的情况我们前面已经讨论过了),cur原来一定是黑色的。也就是说cur是被它的子树在向上继续调整时改为红色的。请看到①的b分支,就是①的b分支这种情况转化而来的。
在u不存在或者存在且为黑的情况下,就需要旋转了。旋转又涉及到单旋和双旋,所以注意啦,我又要从这里开始进行分岔了。
a. p为g的左孩子,cur为p的左孩子(如下图),则进行右旋,旋转后,p变为黑色,g变为红色,over。
可以看到,u结点存在且为黑和u结点不存在的处理方案是一样的,所以把它们归为了一类。
b. p为g的左孩子,cur为p的右孩子(如下图)。
不论u不存在还是u存在且为黑,在这种情况下都需要双旋。
● 对以p为根的子树进行左旋。
● 重新调整父子关系。
● 对以g为根结点的树进行右旋。
● 重新调整父子关系。
做完了这些操作,还没结束哟。你看第二张图,是不是还有连续的红节点。但先别慌,其实这种情况我们已经有处理方案了或者说已经处理过了,请看②的a分支,第二张图旋转后的结果转变成了②的a分支中的情况。到这,我们也算处理完成了,因为在循环处理的过程中,这种情况会匹配到②的a分支的处理方案。如果对这句话感到云里雾里的,没关系,后面看代码就明白了。
有人也许会有这样的疑问:为什么所有情况都假设p为红,cur为红?cur为新插入节点或者是被调整的结点,所以颜色是红色。如果p为黑,就不需要我们在绞尽脑汁的去调整了。只有cur为红,p也为红才要调整,所以所有情况下都假设cur为红,p为红。
所有情况(只需要改变颜色、不需要旋转,单旋,双旋)都已经讲完了,我知道,到这我们该总结了,各种情况之间的相互转换,确实让我们摸不着头脑,下面我画张思维导图帮助大家理解我的分类情况。
红黑树的插入代码
#pragma once
#include <iostream>
using namespace std;
enum Color{RED, BLACK};
template <class T>
struct RBTreeNode
{
RBTreeNode<T>* _left;
RBTreeNode<T>* _right;
RBTreeNode<T>* _parent;
Color _color;
T _data;
RBTreeNode(const T& data = T(), const Color color = RED)
:_left(nullptr), _right(nullptr), _parent(nullptr)
,_color(color), _data(data)
{}
};
template <class T>
class RBTree
{
typedef RBTreeNode<T> Node;
public:
bool insert(const T& data)
{
//第一次插入
if (_root == nullptr)
{
_root = new Node(data);
_root->_color = BLACK;
return true;
}
Node* cur = _root;
Node* parent = nullptr;
//找到插入的落脚点
while (cur)
{
if (cur->_data < data)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_data > data)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
//插入
cur = new Node(data);
cur->_parent = parent;
if (parent->_data > data)
parent->_left = cur;
else
parent->_right = cur;
//检查是否破坏红黑树的规则
while (parent && parent->_color == RED)
{
//parent的父节点一定存在,因为根节点是黑色的
Node* grandfather = parent->_parent;
//先讨论左边
if (grandfather->_left == parent)
{
Node* unclue = grandfather->_right;
if (unclue && unclue->_color == RED)
{
//u存在且为红
parent->_color = BLACK;
unclue->_color = BLACK;
grandfather->_color = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
//u存在且为黑或u不存在——旋转
if (cur == parent->_left)
{
//右单旋
RotateRight(grandfather);
}
else
{
//左旋
RotateLeft(parent);
//右旋
RotateRight(grandfather);
//注意上面左旋和右旋是处理以谁为根的子树
}
//调整颜色
parent->_color = BLACK;
grandfather->_color = RED;
//旋转结束后绝对符合红黑树的规则了,就该结束了
//除非旋转错误
break;
}
}
else
{
Node* unclue = grandfather->_left;
if (unclue && unclue->_color == RED)
{
//u存在且为红
parent->_color = BLACK;
unclue->_color = BLACK;
grandfather->_color = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
//u存在且为黑或u不存在——旋转
if (parent->_right == cur)
{
//左旋
RotateLeft(grandfather);
}
else
{
//右旋
RotateRight(parent);
//左旋
RotateLeft(grandfather);
//注意上面左旋和右旋是处理以谁为根的子树
}
parent->_color = BLACK;
grandfather->_color = RED;
break;
}
}
}
//确保根节点是黑色的
_root->_color = BLACK;
return true;
}
void inorder() { _inorder(_root); }
private:
void RotateLeft(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
subR->_left = parent;
Node* parentParent = parent->_parent;
parent->_parent = subR;
subR->_parent = parentParent;
if (parentParent)
{
if (parentParent->_left == parent)
parentParent->_left = subR;
else
parentParent->_right = subR;
}
else
{
_root = subR;
subR->_color = BLACK;
subR->_parent = nullptr;
}
}
void RotateRight(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
subL->_right = parent;
Node* parentParent = parent->_parent;
parent->_parent = subL;
subL->_parent = parentParent;
if (parentParent)
{
if (parentParent->_left == parent)
parentParent->_left = subL;
else
parentParent->_right = subL;
}
else
{
_root = subL;
subL->_color = BLACK;
subL->_parent = nullptr;
}
}
void _inorder(Node* root)
{
if (root == nullptr)
return;
_inorder(root->_left);
cout << root->_data << " ";
_inorder(root->_right);
}
private:
Node* _root = nullptr;
};
#include "RBTree.h"
int main()
{
int arr[] = { 1, 6, 12, 17, 3, 2, 18, 9, 10 };
RBTree<int> rb;
for (auto val : arr)
{
rb.insert(val);
}
rb.inorder();
return 0;
}
红黑树与AVL-tree比较
由于AVL树追求严格的平衡,所以在插入和删除数据时需要经过多次旋转来恢复平衡。而红黑树的平衡条件比较宽松,只需保证最长路径不超过最短路径的两倍即可,所以相对而言,红黑树在插入和删除操作中旋转次数少一些。因而在经常需要进行增删的场景下,红黑树的性能更优一些。
红黑树平衡条件较宽松,AVL树追求严格的平衡,相同数据量情况下,AVL树的高度相对比较的低,搜索的效率理论上要比红黑树的高,但实践表明,在大多数情况下这种优势并不明显。
如果搜索的需求远高于插入和删除,那么选择AVL树可能会更好一些。
红黑树的应用场景
C++STL库中就用到了红黑树,除此之外,数据库索引、文件系统的目录结构以及操作系统的任务调度等领域中都可以看到红黑树的身影。
本文到这就结束啦~感谢支持!