之前我们有介绍高级数据结构中的BST树与AVL树:
《C++高级数据结构算法 | Binary Search Tree(二叉查找树)》
《C++高级数据结构算法 | AVL(自平衡二叉查找树)》
但是,即使AVL树拥有平衡特性,但它是通过其不断的旋转操作来实现的,最坏情况下将每回溯一层都要进行旋转调整。即数据量大了以后,AVL树的旋转操作就拖慢了插入和删除的时间。
为此引入了红黑树,它具有良好的旋转次数,不至于影响结点插入和删除的时间效率。
红黑树的概念与重要性质
红黑树是一种近似平衡的二叉查找树,它能够确保任何一个节点的左右子树的高度差不会超过二者中较低子树的一倍。具体来说,红黑树是满足如下条件的二叉查找树(Binary Search Tree):
- 性质1 : 每一个节点是要么是红色要么是黑色
- 性质2 : 根节点必须是黑色
- 性质3 :叶子节点都是黑色(指叶子节点的地址域null为黑色,一般null节点默认颜色是黑色)
- 性质4 : 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 性质5 : 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。
什么这些特性确保了这个结果?
我们注意到性质4要求任一路径不能有两个连续的红色节点。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质4所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。
在进行红黑树的插入以及删除操作的时候,会涉及节点的旋转和重新着色问题,红黑树的旋转次数要比AVL少很多,红黑树插入最多旋转两次,删除最多旋转三次。
例如C++ STL库中的map,multimap,set,multiset等容器,Linux的虚拟内存管理,epoll 的内核实现都应用到了红黑树这种数据结构,其增删查时间复杂度能达到 。
红黑树的结构定义
红黑树的定义比我们之前讲解的AVL树、BST树要稍微复杂一点,因为涉及到结点着色问题,因此我们需要为每个结点定义颜色,并且由于插入删除时我们需要知道结点的叔父结点(指该节点的父节点的兄弟节点),因此我们需要定义指针指向该结点的父节点。
enum Color
{
BLACK,RED
};
template<typename T>
class RBTree
{
public:
RBTree() :_root(nullptr) {}
private:
struct RBNode
{
RBNode(T data = T(),
Color color = BLACK,
RBNode* parent = nullptr)
:_data(data)
, _left(nullptr)
, _right(nullptr)
, _parent(parent)
, _color(color)
{}
T _data;
RBNode* _left;
RBNode* _right;
RBNode* _parent;
Color _color;
};
RBNode* _root; // 指向红黑树的根节点
// 获取结点颜色
Color color(RBNode* node)
{
return node == nullptr ? BLACK : node->_color;
}
// 设置结点颜色
void setColor(RBNode* node, Color color)
{
node->_color = color;
}
// 获取左孩子结点
RBNode* left(RBNode* node)
{
return node->_left;
}
// 获取右孩子结点
RBNode* right(RBNode* node)
{
return node->_right;
}
// 获取父节点
RBNode* parent(RBNode* node)
{
return node->_parent;
}
};
红黑树的旋转操作
当查找树的结构发生改变时,红黑树的条件可能被破坏,需要通过调整使得查找树重新满足红黑树的条件。调整可以分为两类:一类是颜色调整,即改变某个节点的颜色;另一类是结构调整,即改变树中结点的结构关系。结构调整过程包含两个基本操作:左旋(Rotate Left),右旋(RotateRight)。
上述我们给出了红黑树的结构定义,尽管我们只添加了一个指向其父节点的指针域,但是对于红黑树的旋转操作来说,就较为复杂了,因为我们需要对其父节点进行修改。
左旋转操作
左旋转操作的整体流程是与我们之前介绍的AVL树的旋转操作是相同的,我们唯一需要考虑的就是有关父节点的修改问题。
完整代码如下:
/**
* 左旋转操作
* 拿到当前结点的右孩子结点child,它即将成为新的根节点
* 1、那么我们需要将原结点的父节点写成新的根节点child的父节点
* 当然我们也需要修改祖先结点的孩子结点,修改为child结点
* 我们需要判断祖先结点是否为空nullptr,若为空,这说明原结点node
* 就是树的根节点,因此我们直接将指向根节点的指针root指向child即可
* 若祖先结点不为空,那么我们就判断原结点时祖先结点左孩子或右孩子
* 最后将祖先结点的孩子结点置为child即可。
* 2、将child结点的左孩子(有可能为nullptr)置为原结点node的右孩子
* 然后,我们需要判断child结点是否有左孩子,如果有左孩子的话我们还要
* 修改该左孩子的父节点为node结点
* 3、最后一步就是将node结点置为新根结点child的左孩子
* 当然还要更改一下node结点的父节点,其新的父节点就为新根child。
*/
void leftRotate(RBNode* node)
{
RBNode* child = node->_right;
child->_parent = node->_parent;
if (node->_parent == nullptr)
{
_root = child;
}
else
{
if (node->_parent->_left == node)
{
node->_parent->_left = child;
}
else
{
node->_parent->_right = child;
}
}
node->_right = child->_left;
if (child->_left != nullptr)
{
child->_left->_parent = node;
}
child->_left = node;
node->_parent = child;
}
右旋转操作
/**
* 右旋转操作
* 右旋转操作与左旋转操作是成镜像关系的,还是分3步:
* 拿到当前结点的左孩子结点child,它即将成为新的根节点
* 1、将原结点的父节点写成新的根节点child的父节点
* 当然我们也需要修改祖先结点的孩子结点,修改为child结点
* 我们需要判断祖先结点是否为空nullptr,若为空,这说明原结点node
* 就是树的根节点,因此我们直接将指向根节点的指针root指向child即可
* 若祖先结点不为空,那么我们就判断原结点时祖先结点左孩子或右孩子
* 最后将祖先结点的孩子结点置为child即可。
* 2、将child结点的右孩子(有可能为nullptr)置为原结点node的左孩子
* 然后,我们需要判断child结点是否有右孩子,如果有右孩子的话我们还要
* 修改该右孩子的父节点为node结点
* 3、最后一步就是将node结点置为新根结点child的右孩子
* 当然还要更改一下node结点的父节点,其新的父节点就为新根child。
*/
void rightRotate(RBNode* node)
{
RBNode* child = node->_left;
child->_parent = node->_parent;
if (node->_parent == nullptr)
{
_root = child;
}
else
{
if (node->_parent->_left == node)
{
node->_parent->_left = child;
}
else
{
node->_parent->_right = child;
}
}
node->_left = child->_right;
if (child->_right != nullptr)
{
child->_right->_parent = node;
}
child->_right = node;
node->_parent = child;
}
红黑树的插入
我们要向红黑树中插入结点,若树为空,那么我们将其插入到根节点位置,调整根节点指针,并将其着色成黑色,直接结束。
否则,我们都是将红色结点插入到树中,因为这样不会影响路径中黑色结点的数量改变,那么如果其父节点是黑色的,那么直接插入结束,因为我们该结点的插入并没有导致红黑树性质的改变,包括任一简单的路径的黑色结点数目相同,并且没有两个连续的红色结点等。
但是,如果我们插入结点的父节点是红色的,那么就破坏了红黑树的性质,即不能出现连续的两个红色结点。此时,我们需要对红黑树进行结点结构调整,颜色调整等一系列插入修复操作。
整体来说,红黑树的插入的整体代码架构和BST树是相同的,我们修改的地方是,在插入完成后,我们判断插入结点的父节点的颜色,若是黑色,则插入结束;若是红色,则调用插入修复函数。
插入修复函数是红黑树的插入操作的核心。我们将其分为三种情况:
情况一:如下左图,插入结点(N)的叔叔结点(U)是红色的。
修复操作:将父节点与叔叔结点都改为黑色,将祖父结点改为红色,继续向上检查,直到父结点黑色结点停止。
情况二:叔叔结点为黑色(或为空),祖父结点、父节点、插入结点处在一条直线上。
修复操作:直接进行一个旋转操作(左旋或右旋),把父节点置为黑色,把祖父结点都置为红色。
情况三:叔叔结点为黑色(或为空),祖父结点、父节点、插入结点不在一条直线上。
修复操作:两次旋转操作(左-右旋转 或 右-左旋转),第一次针对父节点,第二次针对祖父结点,第二次旋转与着色即为情况二的情况。
下面是实现代码:
首先是insert的函数,该函数与BST树的非递归插入基本结构相同:
/**
* 红黑树的插入
* 树不为空,则插入的都是红色结点
* 插入完成后,需要判断父节点的颜色,父节点是黑色,则插入结束
* 父节点是红色,则需要进行插入调整
*/
void insert(const T& val)
{
if (_root == nullptr)
{
_root = new RBNode(val, BLACK);
return;
}
RBNode* parent = nullptr;
RBNode* cur = _root;
while (cur != nullptr)
{
parent = cur;
if (cur->_data > val)
{
cur = cur->_left;
}
else if (cur->_data < val)
{
cur = cur->_right;
}
else
{
return;
}
}
// 以红色结点插入到红黑树中
RBNode* node = new RBNode(val, RED, parent);
if (val < parent->_data)
{
parent->_left = node;
}
else
{
parent->_right = node;
}
// 父亲结点为红色,红黑树性质被破坏,需要进行调整
if (color(parent) == RED)
{
fixAfterInsert(node);
}
}
/*
* 插入调整函数
* 注意情况三的一次旋转后就变为了情况二
*/
void fixAfterInsert(RBNode* node)
{
while (color(parent(node)) == RED)
{
// 插在了祖先节点的左子树当中
if (left(parent(parent(node))) == parent(node))
{
RBNode* uncle = right(parent(parent(node)));
// 情况1 : 叔叔结点是红色
if (color(uncle) == RED)
{
setColor(parent(node), BLACK); // 父节点置黑色
setColor(uncle, BLACK); // 叔叔节点置黑色
setColor(parent(parent(node)), RED); // 祖父节点置红色
node = parent(parent(node)); // node指向祖父节点,继续向根回溯
}
else
{
/* 情况3 : 叔叔结点是黑色,且该结点与其父亲结点、
* 祖父结点不在一条直线上
* 这里我们为了和后面情况二的代码兼容,因此让node
* 指向中间节点(父节点),进行旋转操作后node指向
* 三个节点中最后一个结点。
*/
if (right(parent(node)) == node)
{
node = parent(node);
leftRotate(node);// 以父节点为根做左旋转
}
/* 情况2 : 叔叔结点是黑色,且该结点与其父亲结点、
* 祖父结点在一条直线上
*/
setColor(parent(node), BLACK); // 父节点置黑
setColor(parent(parent(node)), RED); // 祖父结点置红
rightRotate(parent(parent(node))); // 对祖父节点做右旋转
break;
}
}
else // 插在了祖先节点的右子树当中,与上述过程是镜像关系
{
RBNode* uncle = left(parent(parent(node)));
// 情况1
if (color(uncle) == RED)
{
setColor(parent(node), BLACK);
setColor(uncle, BLACK);
setColor(parent(parent(node)), RED);
node = parent(parent(node));
}
else
{
// 情况3
if (left(parent(node)) == node)
{
node = parent(node);
rightRotate(parent(node));
}
// 情况2
setColor(parent(node), BLACK);
setColor(parent(parent(node)), RED);
leftRotate(parent(parent(node)));
break;
}
}
}
// 在调整的过程中有可能修改了根节点的颜色为红色,需要修改为黑色
setColor(_root, BLACK);
}
红黑树的删除
红黑树的删除操作是最为复杂的操作,因为涉及到需要调整的场景是比插入多的。
如果删除的是红色节点,直接删除就可以,因为删除一个红色结点不会改变红黑树的任何性质;如果删除的是黑色节点,删除完节点需要进行调整,因为破坏了红黑树的性质,某一个分支路径上的黑色节点少了一个。
删除操作的总体思想是从兄弟结点借调黑色结点使树保持局部的平衡,如果局部的平衡达到了,就看整体的树是否是平衡的,如果不平衡就接着向上回溯调整。
下面我们默认调整点是父节点的左孩子,删除主要分为四种情况:
情况一 : 待删除结点的兄弟节点为红色节点,只能做选择调整成其他的情况。
由于我们无法借调一个黑色节点过来,但是兄弟节点的孩子节点肯定都是黑色的,我们可以进行一次旋转操作,把黑色的兄弟节点提上来,就可以借调黑色节点了。
调整方案:把兄弟节点颜色改为黑色,把父节点改为红色,然后以父节点为根节点进行左旋。
情况二 :待删除结点的兄弟节点是黑色且兄弟节点的左右孩子均为黑色。
调整方案:直接把兄弟节点设置成红色,然后从父节点开始继续回溯调整。
情况三 :待删除结点的兄弟节点是黑色且兄弟节点左孩子为红色右孩子为黑色。
调整方案:将兄弟结点的左孩子与兄弟结点交换颜色,即将兄弟结点设置为红色,其左孩子颜色设置为黑色,然后以兄弟结点为根进行右旋操作。
情况四 :待删除结点的兄弟节点是黑色且兄弟节点左孩子是任意颜色右孩子为红色。
我们可以发现情况四就是情况三调整之后的结果,因此我们代码可以顺序执行。
调整方案:将兄弟结点与其父结点交换颜色,将父节点设置为黑色,将兄弟结点的右孩子设置为黑色,以父节点为根进行左旋转操作。
完整代码如下:
/**
* 红黑树的删除
*/
void remove(const T& val)
{
if (_root == nullptr)
{
return;
}
/* 搜索待删除结点位置 */
RBNode* curNode = _root;
while (curNode != nullptr)
{
if (curNode->_data > val)
{
curNode = curNode->_left;
}
else if (curNode->_data < val)
{
curNode = curNode->_right;
}
else
{
break;
}
}
/* 没有找到直接返回 */
if (curNode == nullptr)
{
return;
}
/* 待删除结点的左右孩子均不为空,将前驱结点值赋给待删除结点,删除前驱 */
if (curNode->_left != nullptr && curNode->_right != nullptr)
{
RBNode* preNode = curNode->_left;
while (preNode->_right != nullptr)
{
preNode = preNode->_right;
}
curNode->_data = preNode->_data;
}
/* 待删除结点至少有一个孩子结点,或没有孩子结点 */
RBNode* childNode = curNode->_left;
if (childNode == nullptr)
{
childNode = curNode->_right;
}
/* 待删除结点至少有一个孩子结点的情况,我们需要根据判断条件修改父节点指针 */
if (childNode != nullptr)
{
/* 更新孩子结点的父节点域 */
childNode->_parent = curNode->_parent;
if (curNode->_parent == nullptr)
{
_root = childNode;
}
else if (curNode->_parent->_left == curNode)
{
curNode->_parent->_left = childNode;
}
else
{
curNode->_parent->_right = childNode;
}
/* 待删除结点为黑色,需要进行结点调整 */
if (curNode->_color == BLACK)
{
fixAfterRemove(childNode);
}
}
else // 待删除结点没有孩子结点
{
/* 待删除结点是否为根节点 */
if (curNode->_parent == nullptr)
{
_root = nullptr;
}
else
{
/* 待删除结点为黑色,需要进行结点调整 */
if (curNode->_color == BLACK)
{
fixAfterRemove(curNode);
}
/* 进行红黑树的删除调整完成后,把curNode节点删除掉 */
if (curNode->_parent->_left == curNode)
{
curNode->_parent->_left = nullptr;
}
else
{
curNode->_parent->_right = nullptr;
}
}
}
}
/**
* 红黑树的删除调整
*/
void fixAfterRemove(RBNode* node)
{
while (node != _root && node->_color == BLACK)
{
/* 调整点是父节点的左孩子 */
if (node->_parent->_left == node)
{
/* 兄弟结点在父节点右边,保存以备用 */
RBNode* broNode = node->_parent->_right;
/* 情况一:兄弟结点是红色 */
if (broNode->_color == RED)
{
broNode->_color = BLACK; // 兄弟结点置为黑色
node->_parent->_color = RED; // 父节点置为红色
leftRotate(node->_parent); //以对父节点为根进行左旋操作
broNode = node->_parent->_right; // 更新兄弟结点
}
/* 情况二:兄弟结点为黑色并且其左右孩子也是黑色 */
if (broNode->_left->_color == BLACK
&& broNode->_right->_color == BLACK)
{
broNode->_color = RED; // 将兄弟结点置为红色
node = node->_parent; // 将node改为其父亲结点,继续向根回溯
}
else
{
/* 情况三:兄弟结点为黑色并且其左孩子为红色右孩子为黑色 */
if (broNode->_right->_color == BLACK)
{
broNode->_left->_color = BLACK; // 兄弟结点左孩子置为黑色
broNode->_color = RED; // 兄弟结点置为红色
rightRotate(broNode); // 以兄弟结点为根进行右旋转
broNode = node->_parent->_right; // 更新兄弟结点
}
/* 情况四:兄弟结点为黑且右孩子为红色,左孩子可为任意颜色*/
broNode->_color = node->_parent->_color; // 兄弟结点与其父节点交换颜色
node->_parent->_color = BLACK; // 兄弟结点的父节点置黑
broNode->_right->_color = BLACK; // 兄弟结点的右孩子置黑
leftRotate(node->_parent); // 以父节点为根进行左旋操作
//在执行完这一步后,一定会调整好,直接设置为根节点下次退出
node = _root;
}
}
else /* 调整点是父节点的右孩子,具体操作时上面的镜像 */
{
/* 兄弟结点在父节点左边 */
RBNode* broNode = node->_parent->_left;
/* 情况一:兄弟结点是红色 */
if (broNode->_color == RED)
{
broNode->_color = BLACK;
node->_parent->_color = RED;
rightRotate(node->_parent);
broNode = node->_parent->_left;
}
/* 情况二:兄弟结点为黑色并且其左右孩子也是黑色 */
if (broNode->_left->_color == BLACK
&& broNode->_right->_color == BLACK)
{
broNode->_color = RED;
node = node->_parent;
}
else
{
/* 情况三:兄弟结点为黑色并且其右孩子为红色左孩子为黑色 */
if (broNode->_left->_color == BLACK)
{
broNode->_right->_color = BLACK;
broNode->_color = RED;
leftRotate(broNode);
broNode = node->_parent->_left;
}
/* 情况四:兄弟结点为黑且左孩子为红色,右孩子可为任意颜色*/
broNode->_color = node->_parent->_color;
node->_parent->_color = BLACK;
broNode->_left->_color = BLACK;
rightRotate(node->_parent);
node = _root;
}
}
}
/**
* 删除黑色节点后,其孩子节点是红色,上面循环无法进入
* 我们直接将孩子结点直接调成黑色,即可保持黑色节点数量不变;
* 删除黑色节点后,其孩子节点是黑色,但是向上回溯的时候,
* 遇到红色节点,直接将其改成黑色节点即可。
*/
node->_color = BLACK;
}
应用场景
红黑树往往出现由于树的深度过大而造成磁盘IO读写过于频繁,进而导致效率低下的情况在数据较小,可以完全放到内存中时,红黑树的时间复杂度比B树低。反之,数据量较大,外存中占主要部分时,B树因其读磁盘次数少,而具有更快的速度。
- 著名的linux进程调度Completely Fair Scheduler,用红黑树管理进程控制块;
- epoll在内核中的实现,内核事件表就是一颗红黑树;
- nginx中,用红黑树管理timer等;
- Java的TreeMap实现;
- 广泛用在C++的STL中。map、multimap、set、multiset 都是用红黑树实现的