一:背景
红黑树(英语:Red–Black Tree,简称 RB-Tree)是一种平衡的二叉查找树,用途广泛。例如:
- Java 中的:java.util.TreeMap,java.util.TreeSet;
- C++ STL 中的:map,multimap,multiset。
它是在 1972 年由 Rudolf Bayer 发明的,他称之为 “对称二叉 B 树”,它现代的名字(即 “红黑树”)是 Leo J. Guibas 和 Robert Sedgewick 于 1978 年写的一篇论文中获得的。
红黑树的实现比较复杂,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的,它可以在 O(logn) 时间内做查找,插入和删除操作。
红黑树有四个性质(也有书籍和博客上说是五个性质,其实四个性质足矣):
- 每个结点要么是红的,要么是黑的;
- 根结点是黑色的;
- 如果一个结点是红色的,则它的两个孩子都是黑色的;
- 对于任意一个结点,其到叶子结点的每条路径上都包含相同数目的黑色结点。
红黑树的实现和理解还是很复杂的,所以建议读者在阅读本文前,最好已经理解和掌握了二叉查找树和 AVL 树。
二:具体实现与代码分析
和 AVL 树通过约束左右子树高度不同,红黑树是通过它的四条性质来实现 “平衡状态”,在插入结点或者删除结点时,可能会造成某个结点违反了上述的某条性质,那么红黑树的做法就是通过 “重新着色” 和 “旋转” 两种方式使之重新符合性质。
“重新着色” 这很简单,现在来看下 “旋转” 是怎么旋转的。一共有两种旋转方式:左旋和右旋。
左旋
void RBTree::ratate_left(Node * x)
{
Node * y = x->right;
x->right = y->left;
if (y->left)
y->left->parent = x;
y->parent = x->parent;
if (x == root())
root() = y;
else if (x == x->parent->left)
x->parent->left = y;
else
x->parent->right = y;
y->left = x;
x->parent = y;
}
右旋
void RBTree::rotate_right(Node * x)
{
Node * y = x->left;
x->left = y->right;
if (y->right)
y->right->parent = x;
y->parent = x->parent;
if (x == root())
root() = y;
else if (x == x->parent->right)
x->parent->right = y;
else
x->parent->left = y;
y->right = x;
x->parent = y;
}
很容易看出,左旋和右旋其实就是两个镜像操作而已。
好了,真是千呼万唤始出来,重点终于来了!
2.1 插入操作
将红黑树当作一颗二叉查找树,将结点插入,插入后,该树仍然是一棵二叉查找树,但是它可能已经不是红黑树了,所以接下来就要通过 “旋转” 和 “重新着色” 来使它重新成为红黑树。
首先,我们把新插入的结点着色为红色。为什么偏偏是红色呢?先回顾下红黑树的四条性质:
- 每个结点要么是红的,要么是黑的;
- 根结点是黑色的;
- 如果一个结点是红色的,则它的两个孩子都是黑色的;
- 对于任意一个结点,其到叶子结点的每条路径上都包含相同数目的黑色结点。
将插入的结点着色为红色,不会违背 “性质 4″。而少违背一条性质,就意味着我们需要处理的情况越少。
其次,我们再来看看插入结点会遇到哪几种情况,分析发现,一共有三种:
- 被插入结点是根结点,那我们把此结点涂为黑色就行了;
- 被插入结点的父亲结点是黑色的,那么什么也不需要做,结点被插入后,仍然是红黑树。
- 被插入结点的父亲结点是红色的,那么此时是违背 “性质 3” 的。
最后,”被插入结点的父亲结点是红色的” 这种情况下,被插入结点是一定存在非空祖父(即父亲的父亲)结点的。那么此时这种情况可以进一步再划分为 6 种情况,因为涉及到镜像操作,所以我们只需理解其中一边镜像的 3 种情况即可(为方便叙述,我们把 “被插入结点” 称为 “当前结点”,那么 “当前结点” 的父亲的父亲就叫做 “祖父结点”,而 “祖父结点” 如果还有一个儿子的话,我们就称其为 “叔叔结点”。):
Case 1 :当前结点的父亲为红色,叔叔存在且也是红色
图中,”结点 1″ 为 “当前结点”。那么我们的处理策略就是:
- 将 “父亲结点” 改为黑色;
- 将 “叔叔结点” 改为黑色;
- 将 “祖父结点” 改为红色;
- 将 “祖父结点” 设为 “当前结点”,继续进行操作。
处理完后,图中显示的两条路径上,黑色结点数相同且和原图数目一致。
Case 2:当前结点的父亲为红色,叔叔不存在或为黑色,且当前结点是其父亲的右孩子
图中,”结点 2″ 为 “当前结点”。那么我们的处理策略就是:
- 将 “父亲结点” 设为 “当前结点”;
- 以 “新的当前结点” 为支点进行左旋。
处理完后,我们发现依旧不满足红黑树的性质,别急,这就是 “Case 3″。
Case 3:当前结点的父亲为红色,叔叔不存在或为黑色,且当前结点是其父亲的左孩子
图中,”结点 1″ 为 “当前结点”。那么我们的处理策略就是:
- 将 “父亲结点” 改为黑色;
- 将 “祖父结点” 改为红色;
- 以 “祖父结点” 为支点进行右旋。
处理完后,图中显示的两条路径上,黑色结点数相同且和原图数目一致。
2.2 删除操作
和删除一棵普通二叉查找树的结点相同,我们会遇到三种情况:
- “被删结点” 没有孩子,那么直接删除即可;
- “被删结点” 只有一个孩子,那么直接删除该结点,并用该结点的唯一孩子替换它;
- “被删结点” 有两个孩子,那么先找出它的 “后继结点”,然后删除 “被删结点”,再用 “后继结点” 去替换 “被删结点”。
为方便叙述,我们把 “被删结点” 称为 “原先结点”,用来替换 “被删结点” 的结点称为 “当前结点”。
首先,我们来看看删除一个结点会遇到哪几种情况,分析发现,一共有四种:
- “原先结点” 为黑色,”当前结点” 为红色,那么我们把 “原先结点” 删掉后,拿 “当前结点” 去替换它并修改颜色为黑色即可;
- “原先结点” 为黑色,”当前结点” 为黑色,这种情况比较复杂,待会再说;
- “原先结点” 为红色,”当前结点” 为红色,那么我们把 “原先结点” 删掉后,直接拿 “当前结点” 去替换它即可;
- “原先结点” 为红色,”当前结点” 为黑色,那么我们把 “原先结点” 删掉后,再拿 “当前结点” 去替换它。我们发现,此时 “原先结点” 位置是满足红黑树性质的,但是由于 “当前结点” 被拿走,”当前结点” 位置可能就会违背红黑树性质。分析发现,此时的 “当前结点” 不就是上面 “情况 1” 和 “情况 2” 中所讲的 “原先结点”!那么当前的这种情况直接就变成了 “情况 1” 或 “情况 2″。
最后,我们看下上述的 “情况 2″。这种情况可以进一步再划分为 8 种情况,因为涉及到镜像操作,所以我们只需理解其中一边镜像的 4 种情况即可(注意,下面的图片上,”原先结点” 已被删除,故未画出,我们只画出了 “当前结点”):
Case 1:当前结点是黑色,兄弟结点是红色
图中,”结点 1″ 为 “当前结点”。观察上图,因 “原先结点” 已被删除,故原来每条路径上应该是 3 个黑色结点(右侧路径未画完全),此时左侧少了一个,那么我们的处理策略就是:
- 将 “兄弟结点” 改为黑色;
- 将 “父亲结点” 改为红色;
- 以 “父亲结点” 为支点进行左旋;
- 左旋后,重新设置 “兄弟结点”。
处理完后,我们发现最左侧路径依旧是 2 个黑色结点,说明当前状态并不满足红黑树性质。其实,这是进入了下面的 Case 2,Case 3,和 Case 4 阶段了,请继续往下看。
Case 2:当前结点是黑色,兄弟结点是黑色,两个孩子为空或是黑色
图中,”结点 1″ 为 “当前结点”。观察上图,因 “原先结点” 已被删除,故原来每条路径上应该是 2 个黑色结点,此时左侧少了一个,那么我们的处理策略就是:
- 将 “兄弟结点” 改为红色;
- 将 “父亲结点” 设置为新的 “当前结点”,继续进行操作。
处理完后,我们发现图中路径都只有 1 个黑色结点,说明当前状态不满足红黑树性质,但是我们发现只要把 “结点 2” 着色为黑色不就行了么,这也就是
erase_rebalance
代码最后出现if(x) x->color = black;
的缘由之一(x
指向的是 “当前结点”)。Case 3:当前结点是黑色,兄弟结点是黑色,兄弟结点的左孩子是红色,右孩子是为空或是黑色
图中,”结点 1″ 为 “当前结点”。观察上图,因 “原先结点” 已被删除,故原来每条路径上应该是 2 个黑色结点,此时左侧少了一个,那么我们的处理策略就是:
- 将 “兄弟结点” 的左孩子改为黑色;
- 将 “兄弟结点” 改为红色;
- 以 “兄弟结点” 为支点进行右旋;
- 右旋后,重新设置 “当前结点” 的 “兄弟结点”。
处理完后,我们发现图中最左路径只有 1 个黑色结点,说明当前状态不满足红黑树性质。其实这是进入了 Case 4。
Case 4:当前结点是黑色,兄弟结点是黑色,兄弟结点的右孩子是红色,左孩子为空或红黑皆可
图中,”结点 1″ 为 “当前结点”。观察上图,因 “原先结点” 已被删除,故原来每条路径上应该是 2 个黑色结点,此时左侧少了一个,那么我们的处理策略就是:
- 将 “父亲结点” 的颜色赋给 “兄弟结点”;
- 将 “父亲结点” 改为黑色;
- 将 “兄弟结点” 的右孩子改为黑色;
- 以 “父亲结点” 为支点进行左旋;
处理完后,一切 OK。
三:完整代码
/**
*
* author 刘毅(Limer)
* date 2017-08-20
* mode C++
*/
#include <iostream>
#include <algorithm>
using namespace std;
enum { red = 0, black = 1 };
struct Node
{
int key;
bool color;
Node * parent;
Node * left;
Node * right;
Node(int key = 0)
{
this->key = key;
this->color = red;
this->parent = this->left = this->right = nullptr;
}
};
class RBTree
{
private:
Node * header;
private:
void ratate_left(Node * x);
void rotate_right(Node * x);
void destroy(Node * node);
Node *& root();
void insert_rebalance(Node * x);
void erase_rebalance(Node * z);
void in_order(Node * node);
public:
RBTree();
~RBTree();
Node * insert(int key);
Node * find(int key);
void erase(int key);
void print();
};
void RBTree::ratate_left(Node * x)
{
Node * y = x->right;
x->right = y->left;
if (y->left)
y->left->parent = x;
y->parent = x->parent;
if (x == root())
root() = y;
else if (x == x->parent->left)
x->parent->left = y;
else
x->parent->right = y;
y->left = x;
x->parent = y;
}
void RBTree::rotate_right(Node * x)
{
Node * y = x->left;
x->left = y->right;
if (y->right)
y->right->parent = x;
y->parent = x->parent;
if (x == root())
root() = y;
else if (x == x->parent->right)
x->parent->right = y;
else
x->parent->left = y;
y->right = x;
x->parent = y;
}
void RBTree::destroy(Node * node)
{
if (node == nullptr)
return;
destroy(node->left);
destroy(node->right);
delete node;
}
Node *& RBTree::root()
{
return header->left;
}
void RBTree::insert_rebalance(Node * x)
{
x->color = red;
while (x != root() && x->parent->color == red)
{
if (x->parent == x->parent->parent->left)
{
Node * y = x->parent->parent->right;
if (y && y->color == red) // Case 1
{
x->parent->color = black;
y->color = black;
x->parent->parent->color = red;
x = x->parent->parent;
}
else
{
if (x == x->parent->right) // Case 2
{
x = x->parent;
ratate_left(x);
}
x->parent->color = black; // Case 3
x->parent->parent->color = red;
rotate_right(x->parent->parent);
}
}
else // same as above, just left <-> right
{
Node * y = x->parent->parent->left;
if (y && y->color == red)
{
x->parent->color = black;
y->color = black;
x->parent->parent->color = red;
x = x->parent->parent;
}
else
{
if (x == x->parent->left)
{
x = x->parent;
rotate_right(x);
}
x->parent->color = black;
x->parent->parent->color = red;
ratate_left(x->parent->parent);
}
}
}
root()->color = black; // Do not forget!
}
void RBTree::erase_rebalance(Node * z)
{
Node * y = z;
Node * x = nullptr;
Node * x_parent = nullptr;
if (y->left == nullptr)
x = y->right;
else if (y->right == nullptr)
x = y->left;
else
{
y = y->right;
while (y->left)
y = y->left;
x = y->right;
}
if (y != z) // if y is z's successor
{
z->left->parent = y;
y->left = z->left;
if (y != z->right)
{
x_parent = y->parent;
if (x)
x->parent = y->parent;
y->parent->left = x;
y->right = z->right;
z->right->parent = y;
}
else
x_parent = y;
if (root() == z)
root() = y;
else if (z->parent->left == z)
z->parent->left = y;
else
z->parent->right = y;
y->parent = z->parent;
swap(y->color, z->color);
y = z;
}
else
{
x_parent = y->parent;
if (x)
x->parent = y->parent;
if (root() == z)
root() = x;
else if (z->parent->left == z)
z->parent->left = x;
else
z->parent->right = x;
}
// now, y is pointing to what you will erase!
// x is the child of y, and note that x might be nullptr.
// Now, the actual reblance is coming!
// .....
if (y->color == black)
{
while (x != root() && (x == nullptr || x->color == black))
{
if (x == x_parent->left)
{
Node * w = x_parent->right;
if (w->color == red) // Case 1
{
w->color = black;
x_parent->color = red;
ratate_left(x_parent);
w = x_parent->right;
}
if ((w->left == nullptr || w->left->color == black) && // Case 2
(w->right == nullptr || w->right->color == black))
{
w->color = red;
x = x_parent;
x_parent = x_parent->parent;
}
else
{
if (w->right == nullptr || w->right->color == black) //Case 3
{
if (w->left)
w->left->color = black;
w->color = red;
rotate_right(w);
w = x_parent->right;
}
w->color = x_parent->color; // Case 4
x_parent->color = black;
if (w->right)
w->right->color = black;
ratate_left(x_parent);
break;
}
}
else // same as above, just left <-> right
{
Node * w = x_parent->left;
if (w->color == red)
{
w->color = black;
x_parent->color = red;
rotate_right(x_parent);
w = x_parent->left;
}
if ((w->right == nullptr || w->right->color == black) &&
(w->left == nullptr || w->left->color == black))
{
w->color = red;
x = x_parent;
x_parent = x_parent->parent;
}
else
{
if (w->left == nullptr || w->left->color == black)
{
if (w->right)
w->right->color = black;
w->color = red;
ratate_left(w);
w = x_parent->left;
}
w->color = x_parent->color;
x_parent->color = black;
if (w->left)
w->left->color = black;
rotate_right(x_parent);
break;
}
}
}
if (x)
x->color = black;
}
}
void RBTree::in_order(Node * node)
{
if (node == nullptr)
return;
in_order(node->left);
cout << "( " << node->key << ", " << node->color << " )" << endl;
in_order(node->right);
}
RBTree::RBTree()
{
header = new Node(0);
}
RBTree::~RBTree()
{
destroy(root());
delete header;
header = nullptr;
}
Node * RBTree::insert(int key)
{
Node * cur = root();
Node * pre = header;
while (cur)
{
pre = cur;
if (key < cur->key)
cur = cur->left;
else if (key > cur->key)
cur = cur->right;
else
return nullptr;
}
cur = new Node(key);
cur->parent = pre;
if (pre == header || key < pre->key)
pre->left = cur;
else
pre->right = cur;
insert_rebalance(cur);
return cur;
}
Node * RBTree::find(int key)
{
Node * z = root();
while (z)
{
if (key < z->key)
z = z->left;
else if (key > z->key)
z = z->right;
else
return z;
}
return z;
}
void RBTree::erase(int key)
{
Node * z = find(key);
if (z)
{
erase_rebalance(z);
delete z;
}
}
void RBTree::print()
{
in_order(root());
cout << endl;
}
int main()
{
RBTree rb_tree;
// test "insert"
rb_tree.insert(7);
rb_tree.insert(2);
rb_tree.insert(1); rb_tree.insert(1);
rb_tree.insert(5);
rb_tree.insert(3);
rb_tree.insert(6);
rb_tree.insert(4);
rb_tree.insert(9);
rb_tree.insert(8);
rb_tree.insert(11); rb_tree.insert(11);
rb_tree.insert(10);
rb_tree.insert(12);
rb_tree.print();
// test "find"
Node * p = nullptr;
cout << ((p = rb_tree.find(2)) ? p->key : -1) << endl;
cout << ((p = rb_tree.find(100)) ? p->key : -1) << endl << endl;
// test "erase"
rb_tree.erase(1);
rb_tree.print();
rb_tree.erase(9);
rb_tree.print();
rb_tree.erase(11);
rb_tree.print();
return 0;
}
数据测试如下图:
红黑树还是很复杂的,所以建议读者结合算法可视化工具来理解红黑树。注意,该平台的 “删除操作” 采用的是 “前驱替换原则”,这和本文的“后继替换原则” 不同。
四:时间复杂度
最坏情况,输入的序列为升序或降序,此时红黑相间的路径长度是全黑路径长度的 2 倍,时间复杂度为 Oworst(2logn)。
平均情况,时间复杂度为 Oavg(logn)。
最后,附三段红黑树插入操作的视频:
(1)以升序序列插入