树是一种非线性数据结构,这种数据结构要比线性数据结构复杂的多,因此分为三篇博客进行讲解:
第三篇:红黑树
第三篇:红黑树
开篇说明:对于红黑树的学习,近阶段只需要掌握这种数据结构的思想、特点、适用场景以及它能够解决的问题即可,它的实现过程比较复杂,一般面试中也不会让你手写红黑树代码,而且实际代码开发中,几乎也不会遇到手写红黑树的场景。以后,学有余力的时候,再对红黑树进行深入的了解。
1、红黑树的基本概念
在上一篇文章中,讲述了二叉查找树,它支持快速插入、删除和查找操作,并且各个操作的时间复杂度跟树的高度成正比,理想情况下,时间复杂度是O(logn)。但是,二叉查找树在频繁的动态更新过程中,可能会出现树的高度远远大于的情况,从而导致各个操作的效率降低。在极端情况下,二叉查找树会退化成链表,所以时间复杂度也退化到了O(logn)。那么本文讲的平衡二叉查找树---红黑树就是来解决普通二叉查找树在数据更新的过程中复杂度退化问题。
1.1 红黑树的定义
首先我们需要先了解下“平衡二叉查找树”的概念:二叉树中任意一个结点的左右子树高度相差不能大于1。所以完全二叉树和满二叉树都是平衡二叉查找树,但是非完全二叉树也有可能是平衡二叉查找树。常见的平衡二叉查找树有:AVL树、Sply Tree(伸展树)、Treap(树堆)等。
虽然我们平时提到最多的平衡二叉查找树是红黑树,但是实际上红黑树并不是严格意义上的平衡二叉查找树。比如:它的根节点到各个叶子节点的最长路径可能会比最短路径大一倍。所以红黑树实质上是一种近似的平衡二叉查找树,我们也没必要去死抠定义。对于平衡二叉查找树这个数据结构,核心还是在“平衡”二字。
所以,平衡二叉查找树中“平衡”的意思其实就是让整棵树左右看起来比较“对称”,比较“平衡”,不要出现左子树很高而右子树很矮的情况。这样就能让整棵树的高度相对来说低一些,相应的插入、删除和查找等操作的效率高一些。
因此,如果我们设计一个新的平衡二叉查找树,只要它的高度不比大很多,比如树的高度仍然还是对数级的,它仍然是一棵合格的平衡二叉查找树。
下面我们就正式来讲解下红黑树的概念和它的特性:
红黑树R-B Tree,全称是:Red-Black Tree,它是一种特殊的二叉查找树。红黑树的每个结点要么被标记为红色,要么被标记为黑色。同时一棵红黑树还需要满足以下4点要求:
- 根节点是黑色的;
- 每个叶子结点都是黑色的空结点,即叶子结点不存储数据(这样做是为了便于代码实现);
- 任何相邻的结点都不能同时为红色,即红色结点是被黑色结点隔开的;
- 每个结点,从该结点到达其可达叶子结点的所有路径,都包含相同数目的黑色结点。
- 上面的这些约束条件体现了红黑树的一个关键性质:从根结点到叶子结点最长的可能路径不多于最短的可能路径的两倍长。
1.2 红黑树的时间复杂度
一棵含有n个结点的红黑树的高度至多为2log(n+1)。具体数学证明过程点击:证明过程。
上面链接里的文章使用了数学归纳法对红黑树的时间复杂度进行了证明,本文采纳了《数据结构与算法之美》专栏里的简单证明方法,旨在说明问题,不追求精确的计算结果。
二叉查找树的很多操作的时间复杂度都是和树的高度成正比的,一棵及其平衡的二叉树(满二叉树或者完全二叉树)的高度大约是,所以如果要证明红黑树是近似平衡的,只需要分析它的高度稳定在附近即可。
首先,我们将红色结点从红黑树中去掉,那么有些结点就没有父结点了,它们会直接拿这些结点的祖父结点作为父结点。所以,之前的二叉树就变成了四叉树,再从四叉树中取出某些结点放到叶子结点位置,四叉树就变成了完全二叉树。所以,仅包含黑色结点的四叉树高度比包含相同结点个数的完全二叉树的高度()还要小,所以去掉红色结点的“黑树”的高度也不会超过。
通过上面的讲解,我们知道去掉红色结点的“黑树”的高度也不会超过。然而,在红黑树中,红色结点是被黑色结点隔开而不能相邻的。也就是说,有一个红色结点至少要有一个黑色结点,将它与其他红色结点隔开。红黑树中包含最多黑色结点的路径不会超过(因为去掉红色结点的“黑树”的高度不会超过)。所以加入红色结点后,最长路径也不会超过2,即红黑树的高度近似于2,故其常用操作的时间复杂度都稳定在O(logn)。
说明:虽然上面的推到过程不够严谨,但已经能够说明问题,虽然红黑树的高度比严格意义上的平衡二叉树(AVL)大了一倍,但是性能下降的并不多。红黑树只做到了近似平衡,并不是严格意义上的平衡,所以在维护成本上要比AVL树低。
1.3 红黑树的Java代码实现
public class RB_Tree {
public static final boolean RED = true;
public static final boolean BLACK = false;
public Node root; // 根结点
// 结点内部类
class Node{
public int data; // 值
public Node left, right; // 左子树和右子树
public int N; // 以该结点为根结点的子树中的结点总数
public boolean color; // 结点颜色
public Node(int data, int n, boolean color) {
super();
this.data = data;
N = n;
this.color = color;
}
}
// 获取整个红黑树的大小
public int size(){
return size(root);
}
// 获取以某一结点为根结点的树的大小
public int size(Node node){
if(node == null){
return 0;
}else{
return node.N;
}
}
// 判断当前结点是否为红色结点
public boolean isRed(Node node){
if(node == null){
return false;
}
return node.color == RED;
}
}
2、红黑树的基本操作(一)左旋、右旋、颜色转换
红黑树在插入和删除过程中可能会破坏原本的平衡条件导致不满足红黑树的性质,这时候一般需要通过左旋、右旋和重新着色这三个操作来使红黑树重新满足平衡化条件。
2.1 左旋
通常左旋操作用于将一个向右倾斜的红色链接(这个红色链接连接的两个结点均是红色结点)旋转为左连接。对比前后操作,可以看出,该操作实际上是将红链接的两个结点中较大的结点移动到根节点上。
左旋的动画效果如下: 【动画图来自:https://blog.csdn.net/u010853261/article/details/54312932】
// 左旋转
public Node rotateLeft(Node h){
Node x = h.right;
// 把x的左结点赋值给h的右结点
h.right = x.left;
// 把h赋值给x的左结点
x.left = h;
x.color = h.color;
h.color = RED;
x.N = h.N;
h.N = size(h.left) + size(h.right) + 1;
return x;
}
2.2 右旋
右旋其实是左旋的逆操作,如下所示:
// 右旋转
public Node rotateRight(Node h){
Node x = h.left;
// 把x的右结点赋值给h的左结点
h.left = x.right;
// 将h赋值给x的右结点
x.right = h;
x.color = h.color;
h.color = RED;
x.N = h.N;
h.N = size(h.left) + size(h.right) + 1;
return x;
}
右旋的动态示意图:
2.3 颜色反转
当出现一个临时4-node的时候,即一个结点的两个子结点均为红色,如下图A所示。我们需要将E提升至父结点,即把E对子结点的连线设置为黑色,自己的颜色设置为红色。颜色反转之后如图B所示。
// 颜色反转
public void flipColors(Node h){
h.color = RED; // 父结点的颜色变红
h.left.color = BLACK; // 左子结点颜色变黑
h.right.color = BLACK; // 右子结点颜色变黑
}
3、红黑树的基本操作(二)插入、删除 ---- 了解思想
具体操作分析可以参考:
3.1 插入操作
红黑树规定,插入的结点必须是红色的。而且,二叉查找树新插入的结点都是放在叶子结点上,所以关于插入操作的平衡调整,有下面这两种特殊情况:
1、如果插入结点的父结点是黑色,那我们什么都不用做,它仍满满足红黑树的定义;
2、如果插入的结点是根结点,那我们只需要改变它的颜色,把它变成黑色就行了。
除了上诉的两种特殊情况外,其他情况都会违背红黑树的定义,需要通过左右选择和颜色转换进行调整。
红黑树的平衡调整过程是一个迭代的过程,我们把正在处理的结点叫做关注结点。关注结点会随着不停地迭代处理,而不断地发生变化。最开始的关注结点就是新插入的结点。
新结点插入后,如果红黑树的平衡被打破,一般会有三种情况。我们只需要根据每种情况的特点,不停地调整,就可以让红黑树继续符合定义,继续保持平衡。
现象说明 | 处理策略 | |
---|---|---|
Case 1 | 当前节点的父节点是红色,且当前节点的祖父节点的另一个子节点(叔叔节点)也是红色。 | (01) 将“父节点”设为黑色。 |
Case 2 | 当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的右孩子 | (01) 将“父节点”作为“新的当前节点”。 |
Case 3 | 当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的左孩子 | (01) 将“父节点”设为“黑色”。 |
说明:上面三种情况处理问题的核心思想都是:将红色结点移到根结点,然后,再将根结点设置为黑色。
3.2 删除操作
删除操作的平衡调整分为两步,第一步是针对删除结点的初步调整,初步调整只保证整棵红黑树在一个结点删除之后,仍然满足最后一条要求(每个结点,从该结点到达其可达叶子结点的所有路径,都包含相同数目的黑色结点);第二步是针对关注结点进行二次调整,让它满足红黑树的第三条要求(不存在相邻的两个红色结点)。
删除操作有以下四种情况:
现象说明 | 处理策略 | |
---|---|---|
Case 1 | x是"黑+黑"节点,x的兄弟节点是红色。(此时x的父节点和x的兄弟节点的子节点都是黑节点)。 | (01) 将x的兄弟节点设为“黑色”。 |
Case 2 | x是“黑+黑”节点,x的兄弟节点是黑色,x的兄弟节点的两个孩子都是黑色。 | (01) 将x的兄弟节点设为“红色”。 |
Case 3 | x是“黑+黑”节点,x的兄弟节点是黑色;x的兄弟节点的左孩子是红色,右孩子是黑色的。 | (01) 将x兄弟节点的左孩子设为“黑色”。 |
Case 4 | x是“黑+黑”节点,x的兄弟节点是黑色;x的兄弟节点的右孩子是红色的,x的兄弟节点的左孩子任意颜色。 | (01) 将x父节点颜色 赋值给 x的兄弟节点。 |
4 总结:
红黑树是一种平衡二叉查找树(当然不是严格意义上的平衡二叉查找树),它是为了解决普通二叉查找树在数据更新的过程中复杂度退化问题。红黑树的高度近似于,所以它是近似平衡,其插入、删除和查找操作的时间复杂度都是O(logn)。
也正是因为红黑树是一种性能非常稳定的二叉查找树,所以,在工程中,但凡用到动态插入、删除和查找数据的场景,都可以用到它。但是它的代码实现起来比较困难。所以重点在于学习它的思想和能够解决的问题。
对于红黑树的插入和删除操作,目前基础比较薄弱,选择暂时不深究。
参考及推荐:
1、《数据结构与算法之美》:https://time.geekbang.org/column/article/68638
2、红黑树(一)之 原理和算法详细介绍:http://www.cnblogs.com/skywang12345/p/3245399.html
3、红黑树(五)之 Java的实现:https://www.cnblogs.com/skywang12345/p/3624343.html