动画红黑树,旋转的艺术
红黑树简介
对于程序员来说,红黑树是一个用的很多但是很少去实现的一种数据结构,用的多是因为高效,不管是插入删除操作还是查找操作,复杂度都是 。各个语言都有对应的容器库,很少实现是因为代码量大,平时做题或者工作很少会手写红黑树。
不想看文字的可以直接下滑看红黑树的动画视频。
二叉查找树
要理解红黑树是什么,首先我们需要了解二叉树,然后是二叉搜索树,二叉搜索树是一种特殊结构的二叉树,每一次插入,都是将大于当前节点的数放在节点右边,小于节点的数放在节点左边,每一颗子树同样也是二叉查找树。
刷过LeetCode树题目的童鞋,会知道二叉搜索树有一个常见特性,那就是前序遍历二叉查找树会生成一个有序的数组。有很多题目都是利用这个性质解题的。
二叉搜索树有一个问题,那就是这棵树不会主动的维护树结构,比如我一次插入123456,那么这个二叉树看起来会像一个链表,计算的时间复杂度退化到 ,这样导致搜索树变成了一个不稳定的数据结构。
红黑树
为了解决二叉搜索树这种不稳定性,需要结构自身去调整树的平衡性,红黑树是很多平衡搜索树中比较高效的一种。对于每一次节点添加与删除,红黑树都会去检查当前树结构是否满足红黑树定的五条特性,如果不满足,红黑树最多会使用3次旋转(删除时)解决问题。红黑树的插入操作有3种情况(case),删除操作有4种情况(case),部分情况只需要一次旋转甚至只改变颜色不旋转的方式完成。
7个case
上面提到了插入的3个case和删除的4个case,这是红黑树的核心部分,对于每一个case,这里就不画图细讲了,咳咳,所以到这里就完了?!
啊哈!为了结束这场烧脑的噩梦,我把这7个case写进了动画里。
红黑树动画
本期动画也是花了比较多的时间才完成,动画时间比之前的几个算法动画长很多,因为红黑树的细节很多,要将这些细节展现出来会比较耗时。一起来看动画吧。
横屏体验更佳!
源代码
源代码参考算法导论
#include <algorithm>
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
#include <time.h>
class RBTree {
private:
enum Color {
RED,
BLACK
};
enum Direction {
LEFT,
RIGHT
};
public:
struct RBTreeNode {
int k = 0;
int v = 0;
RBTreeNode* p = nullptr;
RBTreeNode* left = nullptr;
RBTreeNode* right = nullptr;
Color color = RED;
RBTreeNode(int k, int v, Color color)
: k(k)
, v(v)
, color(color)
{
left = NIL;
right = NIL;
p = NIL;
}
bool isLeft()
{
return this == p->left;
}
bool isRight()
{
return this == p->right;
}
RBTreeNode* brother()
{
return isLeft() ? p->right : p->left;
}
void replaceChild(RBTreeNode* u, RBTreeNode* v)
{
if (u == left) {
left = v;
} else {
right = v;
}
v->p = this;
}
void addChild(RBTree* t, RBTreeNode* z)
{
auto y = this;
if (y == NIL) {
t->root = z;
} else if (z->k < y->k) {
y->left = z;
} else {
y->right = z;
}
}
static RBTreeNode* NIL;
};
public:
RBTree()
{
nil = RBTreeNode::NIL;
root = nil;
}
void leftRotate(RBTreeNode* x)
{
// x左移,x的右孩子y成为根节点,y的左孩子成为x的右孩子,其他不动
// 看起来就像是x左移了
auto y = x->right;
// 1 x的右节点变化
x->right = y->left;
// 更新left的父节点
if (y->left != nil) {
y->left->p = x;
}
// 2 根节点变化,更换根节点
y->p = x->p;
// 如果x是根节点,将root设置为y
transplant(x, y);
// 3 y的左孩子
y->left = x;
x->p = y;
}
void rightRotate(RBTreeNode* x)
{
// x右移,x的左孩子y成为根节点,y的右孩子成为x的左孩子,其他不动
// 看起来就像是x右移了
auto y = x->left;
x->left = y->right;
// 更新right的父节点
if (y->right != nil) {
y->right->p = x;
}
y->p = x->p;
transplant(x, y);
y->right = x;
x->p = y;
}
RBTreeNode* getInternal(RBTreeNode* n, int k)
{
if (!n || n == nil) {
return nullptr;
}
if (n->k == k) {
return n;
}
if (k < n->k) {
return getInternal(n->left, k);
}
return getInternal(n->right, k);
}
int get(int key)
{
// 普通的搜索
auto v = getInternal(root, key);
return v ? v->v : -1;
}
void insert(RBTreeNode* z)
{
auto y = nil;
auto x = root;
// 找到父节点
while (x != nil) {
y = x;
if (z->k < x->k) {
x = x->left;
} else {
x = x->right;
}
}
z->p = y;
y->addChild(this, z);
insertFixup(z);
}
/*
1 父子节点之间不能出现两个连续的红节点
2 任何一个节点向下遍历到其子孙的叶子节点,所经过的黑节点个数必须相等
*/
void insertFixup(RBTreeNode* z)
{
// 处理父节点是红色,父子同为红色冲突了
while (z->p->color == RED) {
// 父节点在左边
if (z->p->isLeft()) {
// 找到叔叔
auto y = z->p->brother();
// case 1
if (y->color == RED) {
// 父亲和叔叔都是红色,把他们都变成黑色
z->p->color = BLACK;
y->color = BLACK;
// 把祖父变成红色
z->p->p->color = RED;
z = z->p->p;
} else {
// case 2
// 父亲是红色,叔叔是黑色
if (z->isRight()) {
// z在右边就左旋,z指向父节点
z = z->p;
// 左旋父节点
leftRotate(z);
}
// case 3
// 父亲设置为黑色
z->p->color = BLACK;
// 把祖父变成红色
z->p->p->color = RED;
// 右旋祖父节点
rightRotate(z->p->p);
}
} else {
auto y = z->p->brother();
// case 1
if (y->color == RED) {
z->p->color = BLACK;
y->color = BLACK;
z->p->p->color = RED;
z = z->p->p;
} else {
if (z->isLeft()) {
// case 2
z = z->p;
rightRotate(z);
}
// case 3
z->p->color = BLACK;
z->p->p->color = RED;
leftRotate(z->p->p);
}
}
}
root->color = BLACK;
}
void set(int k, int v)
{
if (root == nil) {
root = new RBTreeNode(k, v, BLACK);
} else {
auto z = new RBTreeNode(k, v, RED);
insert(z);
}
}
void transplant(RBTreeNode* u, RBTreeNode* v)
{
if (u->p == nil) {
root = v;
root->p = nil;
} else {
u->p->replaceChild(u, v);
}
}
void deleteFixUp(RBTreeNode* x)
{
while (x != root && x->color == BLACK) {
if (x->isLeft()) {
auto w = x->brother();
if (w->color == RED) {
// case 1
w->color = BLACK;
x->p->color = RED;
leftRotate(x->p);
w = x->p->right;
}
if (w->left->color == BLACK && w->right->color == BLACK) {
// case 2
w->color = RED;
x = x->p;
} else {
if (w->right->color == BLACK) {
// case 3
w->left->color = BLACK;
w->color = RED;
rightRotate(w);
w = x->p->right;
}
// case 4
w->color = x->p->color;
x->p->color = BLACK;
w->right->color = BLACK;
leftRotate(x->p);
x = root;
}
} else {
auto w = x->p->left;
if (w->color == RED) {
// case 1
w->color = BLACK;
x->p->color = RED;
rightRotate(x->p);
w = x->p->left;
}
if (w->right->color == BLACK && w->left->color == BLACK) {
// case 2
w->color = RED;
x = x->p;
} else {
if (w->left->color == BLACK) {
// case 3
w->right->color = BLACK;
w->color = RED;
leftRotate(w);
w = x->p->left;
}
// case 4
w->color = x->p->color;
x->p->color = BLACK;
w->left->color = BLACK;
rightRotate(x->p);
x = root;
}
}
}
x->color = BLACK;
}
RBTreeNode* treeMaxmum(RBTreeNode* x)
{
auto p = x;
while (p->right != nil) {
p = p->right;
}
return p;
}
RBTreeNode* treeMinimum(RBTreeNode* x)
{
auto p = x;
while (p->left != nil) {
p = p->left;
}
return p;
}
void deleteInternal(RBTreeNode* z)
{
auto y = z;
auto origin_color = y->color;
RBTreeNode* x = nullptr;
if (z->left == nil) {
x = z->right;
transplant(z, z->right);
} else if (z->right == nil) {
x = z->left;
transplant(z, z->left);
} else {
y = treeMinimum(z->right);
origin_color = y->color;
x = y->right;
if (y->p == z) {
x->p = y;
} else {
transplant(y, y->right);
y->right = z->right;
y->right->p = y;
}
transplant(z, y);
y->left = z->left;
y->left->p = y;
y->color = z->color;
}
if (origin_color == BLACK) {
deleteFixUp(x);
}
}
void remove(int k)
{
auto z = getInternal(root, k);
if (z) {
deleteInternal(z);
}
}
private:
RBTreeNode* root = nullptr;
RBTreeNode* nil = nullptr;
};
RBTree::RBTreeNode* RBTree::RBTreeNode::NIL = new RBTree::RBTreeNode(0, 0, RBTree::BLACK);
int main()
{
RBTree t;
int i, count = 5;
int key;
srand(time(NULL));
int arr[] = {2, 8, 4, 9, 1};
for (i = 0; i < count; ++i) {
t.set(arr[i], i);
}
for (i = 0; i < count; ++i) {
t.remove(arr[i]);
}
return 0;
}
总结
红黑树作为目前业界使用最广泛的树形结构之一,最大的优点应该是高效和稳定,简单的旋转操作就能保持树平衡,可以说是一种非常优雅的设计。
补充:红黑树面试八股文
1 STL中的set底层用的什么数据结构?
红黑树(Map也是)
2 红黑树有哪些性质?
-
具有二叉查找树的特点 -
根节点是黑色的; -
每个叶子节点都是黑色的空节点(NIL) -
父子节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的 -
每个节点,从该节点到达其可达的叶子节点是所有路径,都包含相同数目的黑色节点
3 红黑树的各种操作的时间复杂度是多少?
能保证在最坏情况下,基本的动态几何操作的时间均为
4 红黑树相比于BST和AVL树有什么优点?
相比于BST,因为红黑树可以能确保树的最长路径不大于两倍的最短路径的长度,所以可以看出它的查找效果是有最低保证的。在最坏的情况下也可以保证 的,这是要好于二叉查找树的。因为二叉查找树最坏情况可以让查找达到 。
红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高,所以在插入和删除中所做的后期维护操作肯定会比红黑树要耗时好多。
avl树是严格的平衡树,而红黑树没那么严格,因此avl树在搜索上略胜红黑树。也因为太严了,删除操作avl树性能比红黑树差。
5 红黑树相对于哈希表,在选择使用的时候有什么依据?
红黑树是有序的,Hash是无序的,根据需求来选择。
红黑树占用的内存更小(仅需要为其存在的节点分配内存),而Hash事先就应该分配足够的内存存储散列表(即使有些槽可能遭弃用)。
红黑树查找和删除的时间复杂度都是 ,Hash查找和删除的时间复杂度都是 。