红黑树是一种自平衡的二叉搜索树,与AVL树类似,在其上进行的插入、删除、查找操作的平均时间复杂度均为 O ( l o g   n ) O(log\,n) O(logn)。
但与AVL树不同的是,红黑树的平衡不是非常严格的平衡(即左右子树高度差不超过1),它牺牲了部分平衡性来换取了插入、删除时的少量旋转操作(最多3次)。
与普通二叉搜索树的区别
节点
-
颜色:相比于普通的二叉搜索树,红黑树的节点增加了一个颜色属性:黑色或红色,这也是红黑树名字的由来。
-
叶子节点:红黑树特别区分了中间节点与叶子节点。在二叉搜索树中,叶子节点与中间节点一般不做区分,两者的区别仅在于有没有子节点(叶节点子节点为NULL,视为没有子节点)。虽然在红黑树中也可以这样,但是为了方便解释与实现,一般对叶子节点进行了区分:叶子节点不存储数据,只是充当树在此结束的标志。
-
父指针:因为红黑树的特殊性,在进行平衡调整时,一般都会涉及到父节点与兄弟节点(具体请参见后文),所以在每个节点中都存储了指向其父节点的指针(叶子节点可能例外,参见后文)。
所以红黑树的节点定义如下:
// 枚举节点颜色:只有黑色与红色
enum COLOR {
BLACK, RED };
class Node {
public:
Node* parent; // 父节点
Node* left; // 左子节点
Node* right; // 右子节点
COLOR color; // 节点颜色
int val; // 节点的值
};
5条性质
在普通二叉搜索树的基础上,红黑树的定义增加了以下5条性质:
- 节点是黑色或者红色。(即树中任意节点非黑即红,没有其它颜色)
- 根节点是黑色。
- 所有的叶子节点是黑色。
- 每个红色节点一定有两个黑色的节点。(即有父子关系的两个节点不能均为红色)
- 从任一节点出发,到其每个叶子节点的所有简单路径上有相同数量的黑节点。(这个黑节点数量也被称为黑高)
基于这5个性质,红黑树能够保证从根节点到叶子节点最长深度不会超过最短深度的2倍,从而保证了红黑树的平衡。
因为最长路径即为红黑交替出现,而最短路径即为全部为黑节点。又由于性质5,所以这两条路径上的黑色节点数目一定相同,根节点与叶节点均为黑色,最长路径上多出的红色节点数目一定不会多于黑色节点的数目。
也正因为红黑树平衡性是通过黑高来进行维护的,所以在其节点上不用存储树的高度。
树的结构
一般情况下,红黑树有以下三种结构(参考《算法导论》):
-
所有叶子节点为NULL,在其它节点上存储节点的黑高。如图:
-
叶子节点由一个节点实例来代表,并且根节点的父节点也指向此节点,如图:
-
每个分支都有一个单独的叶节点实例,如图:
本文选取第二种结构。
操作
对于二叉搜索树的操作基本上也就是三种:增加、删除、查找。
红黑树上也是一样,其中查找操作与普通的二叉搜索树上相同,此处便不再赘述。下文主要以最麻烦的插入与删除来进行说明。
在此之前,先说明一下文中代码所用的红黑树结构(这里只说明了类中的数据,函数声明与定义请参见文末源码):
class RedBlackTree {
private:
// 枚举节点颜色:只有黑色与红色
enum COLOR {
BLACK, RED };
class Node {
public:
Node* parent; // 父节点
Node* left; // 左子节点
Node* right; // 右子节点
COLOR color; // 节点颜色
int val; // 节点的值
};
// 叶节点,只有一个实例,其中的值没有意义(默认为0)、其父、左子、右子节点均为nullptr
Node* leaf;
// 树的根节点,通过此指针可以得到整颗树
Node* root;
// 双黑节点的父节点,只有在删除操作后进行双黑修正时值才有意义,其余情况下均为nullptr
Node* dBlackParent;
}
插入
先从较为简单的插入操作说起。
总的来说,分为三步:
- 找到应该插入到树中的哪个位置。
- 创建新的红色节点并将其插入到树中。
- 对树进行调整(包括染色与旋转),使其满足红黑树的定义(5条性质)。
红黑树本质上也是一颗二叉搜索树,所以它的插入操作与二叉搜索树一样:先找到应该插入的位置,然后插入。这一部分不必多说。
不同的是,因为红黑树增加了5条性质,在插入后有可能会违反其中一条或几条性质,所以在红黑树上的插入操作多了一步调整。
首先,如果一颗红黑树不为空时,那么性质1、2、3则会自然满足,不用去多加考虑。所以我们只需要保证性质4与性质5便可。
为什么要默认插入红色节点呢?
因为性质5更为复杂(因为只要有某一节点不满足,那么其父节点也不满足,从此节点向上到根节点,路径上的所有节点都会不满足),如果破坏了性质5,再进行恢复所付出的代价也会更大,所以我们在插入时尽量先保证性质5不会被破坏。因此,在进行插入时,我们将新的节点默认为红色。这样,不论此节点被插入到何处,都不会破坏性质5(性质5只与黑色节点有关)。
5种情况
默认为红色的新节点插入到树中后,会出现下面5种情况(为方便讨论,在此假设当前讨论节点为N(在插入后第一次讨论时,N代表新插入的节点)、N的父节点为P、P的兄弟节点为U、N的祖父节点为G(即P的父节点)):
情形1:N就是根节点。
这时我们只需要将此节点重绘为黑色即可。这样,树上的所有节点的黑高都增加了1,性质5得到满足。
void RedBlackTree::InsertCase1(Node* root) {
// 树为空,直接返回
if (!root) return;
// 当前节点为根节点,染黑当前节点并返回
if (root->parent == leaf) {
root->color = BLACK;
return;
}
// 否则进入情形2判断
InsertCase2(root);
}
情形2:P为黑色。
此时性质4没有被破坏,性质5因为插入的是红色节点,也未被破坏。所以此种情况不用调整。
void RedBlackTree::InsertCase2(Node* root) {
if (root->parent->color == BLACK) return;
// 否则进入情形3判断
else
InsertCase3(root)