红黑树的由来及其底层原理
一、树
1.1 树的定义
- 树是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
- 树结构是一种非线性存储结构,存储的是具有“一对多”关系的数据元素的集合。
1.2 树的特点
- 每个结点有零个或多个子结点;没有父结点的结点称为根结点;每一个非根结点有且只有一个父结点;除了根结点外,每个子结点可以分为多个不相交的子树;
- 树是一种特殊的图
1.3 树与图的区别
- 树是没有环的图(在图里面,环的路线是开始和结束都是一样的点)一个子节点只有一个父节点,所以树不是一个递归的数据结构。
Trees | Graphs |
---|---|
1. A tree is a special kind of graph that there are never multiple paths exist. There is always one way to get from A to B. | 1. A graph is a system that has multiple ways to get from any point A to any other point B. |
2. Tree must be connected. | 2. Graph may not be connected. |
3. Since it connected we can reach from one particular node to all other nodes. This kind of searching is called traversal. | 3. Traversal always not applicable on graphs. Because graphs may not be connected. |
4. Tree contains no loops, no circuits. | 4. Graph may contain self-loops, loops. |
5. There must be a root node in tree. | 5. There is no such kind of root node in graphs |
6. We do traversal on trees. That mean from a point we go to each and every node of tree. | 6. We do searching on graphs. That means starting from any node try to find a particular node which we need. |
7. pre-order, in-order, post-order are some kind of traversals in trees. | 7. Breath first search, Depth first search, are some kind of searching algorithms in graphs. |
8. Trees are directed acyclic graphs. | 8. Graphs are cyclic or acyclic. |
9. Tree is a hierarchical model structure. | 9. Graph is network model. |
10. All trees are graphs. | 10. But all graphs are not trees. |
11. Based on different properties trees can be classified as Binary tree, Binary search tree, AVL trees, Heaps. | 11. We differ the graphs like directed graphs and undirected graphs. |
12. If tree have “n” vertices then it must have exactly “n-1” edges only. | 12. In graphs number of edges doesn’t depend on the number of vertices. |
13. Main use of trees is for sorting and traversing. | 13. Main use of graphs is coloring and job scheduling. |
14. Less in complexity compared to graphs. | 14. High complexity than trees due to loops. |
二、二叉树
2.1 什么是二叉树
2.1.1 定义
- 树的任意节点至多包含两棵子树。
- 是n(n>=0)个结点的有限集合,它或者是空树(n=0),或者是由一个根结点及两颗互不相交的、分别称为左子树和右子树的二叉树所组成
2.1.2 特点
- 每个结点最多有两颗子树,所以二叉树中不存在度(拥有的子树数目称为结点的度)大于2的结点
- 左子树和右子树是有顺序的,次序不能任意颠倒
- 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树
2.2 二叉树的分类
2.2.1 满二叉树
-
除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树
-
满二叉树特点:
-
- 叶子只能出现在最下一层。出现在其它层就不可能达成平衡。
- 非叶子结点的度(结点拥有的子树数目称为结点的度)一定是2
- 在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多
2.2.2 完全二叉树
-
除最后一层外,每一层上的结点数均达到最大值;在最后一层上只缺少右边的若干结点
-
完全二叉树特点:
-
- 叶子结点只能出现在最下层和次下层。
- 最下层的叶子结点集中在树的左部。
- 倒数第二层若存在叶子结点,一定在右部连续位置。
- 如果结点度为1,则该结点只有左孩子,即没有右子树。
- 同样结点数目的二叉树,完全二叉树深度最小
-
堆的实现一般基于完全二叉树
2.2.3 二叉搜索树
- 可以为空树,或者是具备如下性质:若它的左子树不空,则左子树上的所有结点的值均小于根节点的值;若它的右子树不空,则右子树上的所有结点的值均大于根节点的值,左右子树分别为二叉排序树。
- 二叉查找树是一颗二叉树,它每个结点的值都大于其左子树的任意结点而小于右子树的任意结点,它结合了链表插入的灵活性和有序数组查找的高效性(二分查找)。
- 对于使用二叉查找树的算法,它的运行时间取决于树的形状,而树的形状又取决于结点插入的先后顺序。如上图所示,最好情况下,N 个结点的树是完全平衡的,每条空链接到根结点的距离都为 ~lgN;而在最坏的情况下,搜索路径上可能有 N 个结点,退化成了链表。
- 所以,为了保证运行时间始终在对数级别,在动态构建二叉查找树时,希望保持其平衡性,也就是降低树的高度,使其尽可能为 ~lgN,这样就能保证所有的查找都能在 ~lgN 次比较内结束,就像二分查找那样,这样的树被称为平衡二叉查找树。
2.2.4 平衡二叉查找树(AVL树)
什么是平衡二叉查找树
-
由前苏联的数学家 Adelse-Velskil 和 Landis 在 1962 年提出的高度平衡的二叉树,根据科学家的英文名也称为 AVL 树。它具有如下几个性质:
-
可以是空树。
- 假如不是空树,任何一个结点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过 1。
-
平衡之意,如天平,即两边的分量大约相同。
-
最早的自平衡二叉搜索树结构
第一个自平衡二叉查找树就是AVL 树,它规定,每个结点的左右子树的高度之差不超过 1。在插入或删除结点,打破平衡后,就会通过一次或多次树旋转来重新平衡。
为什么有平衡二叉查找树
- 二叉搜索树已经在一定程度上提高了搜索效率,但是由于二叉搜索树自身的特性,会存在一种极端情况:
平衡二叉查找树的缺点
AVL 树是严格平衡的,适用于查找密集型应用程序,因为在频繁插入或删除结点的场景下,它花费在树旋转的代价太高。
而红黑树就是一种折中方案,它不追求完美平衡,只求部分达到平衡,从而降低在调整时树旋转次数。
2.3 二叉树的性质
-
在二叉树的第i层上最多有2 i-1 个节点 。(i>=1)
-
二叉树中如果深度为k,那么最多有2k-1个节点。(k>=1)
-
n0=n2+1 n0表示度数为0的节点 n2表示度数为2的节点
-
在完全二叉树中,具有n个节点的完全二叉树的深度为[log2n]+1,其中[log2n]+1是向下取整。
-
若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点:
(1) 若 i=1,则该结点是二叉树的根,无双亲, 否则,编号为 [i/2] 的结点为其双亲结点;
(2) 若 2i>n,则该结点无左孩子, 否则,编号为 2i 的结点为其左孩子结点;
(3) 若 2i+1>n,则该结点无右孩子结点, 否则,编号为2i+1 的结点为其右孩子结点
三、红黑树
3.1 什么是红黑树
红黑树的英文是“Red-Black Tree",简称R-B Tree。
红黑树是一种二叉查找树,通过设置一些规则来保证二叉搜索树的平衡性,使二叉搜索树不会在极端情况下变成链表。
红黑树也是一种平衡二叉查找树的变体,它的左右子树高差有可能大于1,所以红黑树不是严格意义上的平衡二叉树(AVL),但对之进行平衡的代价较低, 其平均统计性能要强于 AVL。
3.2 红黑树的特性
- 每个节点或者是黑色,或者是红色
- 根节点是黑色
- 每个叶结点(最后的空节点)是黑色
- 如果一个节点是红色的,则它的子节点必须是黑色的,红色节点的孩子和父亲都不能是红色
- 从每个叶子到根的所有路径上不能有两个连续的红色节点,任意一结点到每个叶子结点的路径都包含数量相同的黑结点。确保没有一条路径会比其他路径长出俩倍。
- 因而,红黑树是相对接近平衡的二叉树,并不是一个完美平衡二叉查找树
为了更好地理解什么是红黑树以及这种看起来特殊的数据结构是如何被提出的,我们首先需要了解一下2-3树,这种数据结构与红黑树是等价的。
说到红黑树,就不得不提 2-3 树,因为,红黑树可以说就是它的一种特殊实现,对它有所了解,非常有助于理解红黑树。
3.3 左倾红黑树与2-3树
3.3.1 什么是2-3树
- 保持平衡,无非是为了降低树的高度,如果把二叉查找树一般化,允许一个结点保存多个值,变成多叉树,也可认为是降低了高度。
- 结点可以存放一个元素或者两个元素。相应地,一个结点可能有两个子树或者三个子树。这种结点可以被称为2结点或者3结点。
- 满足二分搜索树的基本性质,但是它不是一种二叉树。
- 2-3树是一颗绝对平衡的树:从根节点到任意一个叶子结点所经过的结点数量一定是相同的。
3.3.2 2-3树的绝对平衡性
- 插入新元素时,并不会创建一个新结点,而是和叶子结点做融合
- 结点的向下分裂和向上融合来保证绝对平衡性
- 插入元素的过程:
3.3.3 2-3树与红黑树的等价性
- 任意的一颗红黑树能够转换为一颗2-3树
- 红黑树的根节点一定是黑色的:因为不管2-3树的根节点是2结点还是3结点,红黑树的根节点一定是黑色结点。
- 每个叶子结点(最后的空节点)一定是黑色的:与上一个性质是吻合的。空树:空节点既是根节点也是叶子结点。
- 如果一个结点是红色的,它的两个子节点一定是黑色的。
- 核心性质:从任意一个结点出发,到叶子结点经过的黑色结点数目一定是相同的:红黑树中转换为2-3树后,不管转换为2-3树的2结点还是3结点,一定会有一个结点时黑色的。红黑树中每经过一个黑色的结点,意味着对应着经过了原来的2-3树中的一个结点。
- 红黑树是保持“黑平衡”的二叉树。严格意义上来讲不是平衡二叉树。最大高度:2logn,时间复杂度O(logn)
3.4 左倾红黑树添加新元素
与2-3树添加元素是等价的
2-3 树插入的都是叶子结点,红黑树插入的结点都是红色的,因为在 2-3 树中,待插入结点都认为可以插入到一个多值结点中。
3.4.1 树旋转
在分析插入和删除之前,先了解下什么是树旋转。树旋转是二叉树中调整子树的一种操作,常用于调整树的局部平衡性,它包含两种方式,左旋转和右旋转。
其实旋转操作很容易理解:左旋转就是将用两个结点中的较小者作为根结点变为将较大者作为根结点,右旋转刚好于此相反,如上图所示:
- 右旋转,就是将较小者 L 作为根结点,然后调整 L 和 P 的子树
- 左旋转,就是将较大者 P 作为根结点,然后调整 P 和 L 的子树
红黑树的旋转其实就是为了确保和其结构相同的 2-3树的一一对应关系,同时保证红黑树的有序性和平衡性。
3.4.2 保持根节点为黑色和左旋转
增加一个元素右倾:进行一次左旋转
3.4.3 颜色翻转和右旋转
颜色翻转:
右旋转:
3.4.4 添加新元素完整过程
3.4.5 维护红黑树平衡的时机
- 左旋转的时机:当前node的右节点为红色,且当前结点的左节点不为红色
- 右旋转的时机:当前node的左节点为红色,且左节点的左节点也是红色
- 颜色翻转的时机:当前node的左右结点都是红色
3.5 左倾红黑树的实现
左倾红黑树(Left-leaning Red-Black Tree)是红黑树的一种变种,它的主要思想是将红色节点向左倾斜,从而简化插入和删除操作的实现。在左倾红黑树中,所有节点都被标记为红色或黑色,而且满足以下性质:
- 根节点是黑色的。
- 所有叶子节点都是黑色的。
- 如果一个节点是红色的,则它的两个子节点都是黑色的。
- 任意一条从根节点到叶子节点的路径上,不能出现连续的两个红色节点。
- 从任意一个节点出发到其每个叶子节点的路径中,黑色节点的个数相同。
下面是左倾红黑树的Java代码实现:
public class LeftLeaningRedBlackTree<Key extends Comparable<Key>, Value> {
private static final boolean RED = true;
private static final boolean BLACK = false;
private Node root;
private class Node {
private Key key;
private Value val;
private Node left, right;
private boolean color;
public Node(Key key, Value val, boolean color) {
this.key = key;
this.val = val;
this.color = color;
}
}
private boolean isRed(Node x) {
if (x == null) return false;
return x.color == RED;
}
// 坐旋转,坐旋转的同时变色
private Node rotateLeft(Node h) {
Node x = h.right;
h.right = x.left;
x.left = h;
x.color = h.color;
h.color = RED;
return x;
}
// 右旋转,右旋转的同时变色
private Node rotateRight(Node h) {
Node x = h.left;
h.left = x.right;
x.right = h;
x.color = h.color;
h.color = RED;
return x;
}
private void flipColors(Node h) {
h.color = RED;
h.left.color = BLACK;
h.right.color = BLACK;
}
public void put(Key key, Value val) {
root = put(root, key, val);
root.color = BLACK;
}
private Node put(Node h, Key key, Value val) {
if (h == null) return new Node(key, val, RED);
int cmp = key.compareTo(h.key);
if (cmp < 0) h.left = put(h.left, key, val);
else if (cmp > 0) h.right = put(h.right, key, val);
else h.val = val;
// 左旋
if (isRed(h.right) && !isRed(h.left)) h = rotateLeft(h);
// 右旋
if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h);
// 颜色翻转
if (isRed(h.left) && isRed(h.right)) flipColors(h);
return h;
}
}
在这个实现中,我们使用了三种基本操作来维护左倾红黑树的性质:
- 左旋(rotateLeft):将一个节点的右子节点向左旋转,使得它成为新的根节点
- 右旋(rotateRight):将一个节点的左子节点向右旋转,使得它成为新的根节点
- 颜色翻转(flipColors):将一个节点的颜色和它的两个子节点的颜色互换,从而保证每个红色节点都有一个黑色节点作为父节点
在插入节点时,我们首先按照二叉搜索树的方式找到要插入的位置,并将节点标记为红色。然后,我们检查当前节点的父节点、父节点的兄弟节点和祖父节点的颜色,根据需要进行旋转和颜色翻转,以保证左倾红黑树的性质不被破坏。
在这个实现中,我们将所有空节点都视为黑色节点,并将根节点标记为黑色。这些约定使得插入操作更加简单,同时保证了左倾红黑树的性质。
总的来说,左倾红黑树是一种比较优秀的数据结构,它能够在保持红黑树性质的前提下,简化插入和删除操作的实现,同时也能够保持良好的平衡性能。
3.6 红黑树和AVL树的区别
RB-Tree和AVL树作为BBST,其实现的算法时间复杂度相同,AVL作为最先提出的BBST,貌似RB-tree实现的功能都可以用AVL树是代替,那么为什么还需要引入RB-Tree呢?
- 红黑树不追求"完全平衡",即不像AVL那样要求节点的 |balFact| <= 1,它只要求部分达到平衡,但是提出了为节点增加颜色,红黑是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,而AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。
- 就插入节点导致树失衡的情况,AVL和RB-Tree都是最多两次树旋转来实现复衡rebalance,旋转的量级是O(1)。删除节点导致失衡,AVL需要维护从被删除节点到根节点root这条路径上所有节点的平衡,旋转的量级为O(logN),而RB-Tree最多只需要旋转3次实现复衡,只需O(1),所以说RB-Tree删除节点的rebalance的效率更高,开销更小!
- AVL的结构相较于RB-Tree更为平衡,插入和删除引起失衡,如2所述,RB-Tree复衡效率更高;当然,由于AVL高度平衡,因此AVL的Search效率更高啦。
- 针对插入和删除节点导致失衡后的rebalance操作,红黑树能够提供一个比较"便宜"的解决方案,降低开销,是对search,insert ,以及delete效率的折衷,总体来说,RB-Tree的统计性能高于AVL.
- 故引入RB-Tree是功能、性能、空间开销的折中结果。
AVL更平衡,结构上更加直观,时间效能针对读取而言更高;维护稍慢,空间开销较大。
红黑树,读取略逊于AVL,维护强于AVL,空间开销与AVL类似,内容极多时略优于AVL,维护优于AVL。
基本上主要的几种平衡树看来,红黑树有着良好的稳定性和完整的功能,性能表现也很不错,综合实力强,在诸如STL的场景中需要稳定表现。
红黑树的查询性能略微逊色于AVL树,因为其比AVL树会稍微不平衡最多一层,也就是说红黑树的查询性能只比相同内容的AVL树最多多一次比较,但是,红黑树在插入和删除上优于AVL树,AVL树每次插入删除会进行大量的平衡度计算,而红黑树为了维持红黑性质所做的红黑变换和旋转的开销,相较于AVL树为了维持平衡的开销要小得多
- 总结:实际应用中,若搜索的次数远远大于插入和删除,那么选择AVL,如果搜索,插入删除次数几乎差不多,应该选择RB。
3.7 红黑树的性能
- 对于完全随机的数据,普通的二叉搜索树就很好用。
- 对于查询较多的情况,AVL树很好用。
- 红黑树牺牲了平衡性(2logn的高度),但是红黑树的统计性能更优(综合增删改查所有操作)。
3.8 红黑树的应用
3.8.1 红黑树在hashmap中的应用
- 在jdk1.8版本后,Java对HashMap做了改进,在链表长度大于8的时候,将后面的数据存在红黑树中,以加快检索速度。
- 红黑树是”近似平衡“的。相比avl树,在检索的时候效率其实差不多,都是通过平衡来二分查找。但对于插入删除等操作效率提高很多。红黑树不像avl树一样追求绝对的平衡,他允许局部很少的不完全平衡,这样对于效率影响不大,但省去了很多没有必要的调平衡操作,avl树调平衡有时候代价较大,所以效率不如红黑树,在现在很多地方都是底层都是红黑树。
- 红黑树的高度只比高度平衡的AVL树的高度(log2n)仅仅大了一倍,在性能上却好很多。
- HashMap在里面就是链表加上红黑树的一种结构,这样利用了链表对内存的使用率以及红黑树的高效检索,是一种很happy的数据结构。
- AVL树是一种高度平衡的二叉树,所以查找的非常高,但是,有利就有弊,AVL树为了维持这种高度的平衡,就要付出更多代价。每次插入、删除都要做调整,就比较复杂、耗时。所以,对于有频繁的插入、删除操作的数据集合,使用AVL树的代价就有点高了。
- 红黑树只是做到了近似平衡,并不严格的平衡,所以在维护的成本上,要比AVL树要低。
- 所以,红黑树的插入、删除、查找各种操作性能都比较稳定。对于工程应用来说,要面对各种异常情况,为了支撑这种工业级的应用,我们更倾向于这种性能稳定的平衡二叉查找树。
- java8不是用红黑树来管理hashmap,而是在hash值相同的情况下(且重复数量大于8),用红黑树来管理数据。 红黑树相当于排序数据,可以自动的使用二分法进行定位,性能较高。一般情况下,hash值做的比较好的话基本上用不到红黑树。
- 红黑树牺牲了一些查找性能 但其本身并不是完全平衡的二叉树。因此插入删除操作效率略高于AVL树。
- AVL树用于自平衡的计算牺牲了插入删除性能,但是因为最多只有一层的高度差,查询效率会高一些。
3.8.2 红黑树在TreeMap和TreeSet的应用
红黑树是一种自平衡的二叉搜索树,它可以在保持二叉搜索树的性质下,保持树的高度平衡,从而保证了树操作的时间复杂度在最坏情况下也是O(log n)。由于其优秀的性能表现,红黑树被广泛应用于Java的TreeMap和TreeSet中。
TreeMap和TreeSet都是基于红黑树实现的数据结构,它们分别用于实现基于键的有序映射和基于元素的有序集合。它们都支持一系列的操作,如插入、删除、查找最大值、查找最小值、查找某个键或元素的前驱或后继等操作。
在TreeMap中,每个键值对被表示为一个节点,节点按照键的大小进行排序,并存储在一棵红黑树中。在TreeSet中,每个元素被表示为一个节点,节点按照元素的大小进行排序,并存储在一棵红黑树中。
由于红黑树的自平衡性质,TreeMap和TreeSet能够在插入、删除、查找等操作中保持较为稳定的性能表现,具有很高的效率和可靠性。在Java中,TreeMap和TreeSet被广泛应用于需要高效实现有序映射和有序集合的场景,如排序、搜索、去重等场景,是非常常用的数据结构之一。
3.9 红黑树的其他实现
算法导论中的红黑树的实现
package rbTree;
import java.util.HashMap;
/**
* @Author WaleGarrett
* @Date 2021/7/11 18:48
*/
public class RBTree<Key extends Comparable<Key>, Value> {
private class Node<Key, value>{
private Key key;
private Value value;
private Node<Key, Value> left;
private Node<Key, Value> right;
private int color;
public Node(Key key, Value value, int color){
this.key = key;
this.value = value;
this.color = color;
}
}
private static final int RED = 0;
private static final int BLACK = 1;
/**
* 判断一个结点的颜色是否是红色
* @param node
* @return
*/
private boolean isRed(Node<Key, Value> node){
if(node == null)
return false;
return node.color == RED;
}
/** node x
* 左旋转: / \ / \
* @param node T1 x ----> node T3
* @return / \ / \
* T2 T3 T1 T2
*/
private Node<Key, Value> leftRotate(Node<Key, Value> node){
Node x = node.right;
node.right = x.left;
x.left = node;
x.color = node.color;
node.color = RED;
return x;
}
/** node x
* 右旋转: / \ / \
* @param node x T2 ----> T3 node
* @return / \ / \
* T3 T1 T1 T2
*/
private Node<Key, Value> rightRotate(Node<Key, Value> node){
Node x = node.left;
node.left = x.right;
x.right = node;
x.color = node.color;
node.color = RED;
return x;
}
/**
* 颜色翻转:当左右指针均为红色时进行颜色翻转
* @param node
*/
private void flipColor(Node<Key, Value> node){
node.color = RED;
node.left.color = BLACK;
node.right.color = BLACK;
}
/**
* 根节点
*/
private Node<Key, Value> root;
/**
* 添加元素
* @param key
* @param value
*/
public void add(Key key, Value value){
root = add(root, key, value);
// 保证根节点的颜色始终是黑色
root.color = BLACK;
}
public Node add(Node<Key, Value> node, Key key, Value value){
/**
* 新加入的结点始终保持是红色的
*/
if(node == null)
return new Node<Key, Value>(key, value, RED);
int compare = key.compareTo(node.key);
if(compare < 0){
// key小于当前node结点,新结点必然插入到node的左子树
node.left = add(node.left, key, value);
}else if(compare > 0){
// key大于当前node结点,新结点必然插入到node的右子树
node.right = add(node.right, key, value);
}else{
node.value = value;
}
/**
* 使红黑树树保持平衡
*/
// 当node结点的右结点为红色,而左结点为黑色时,左旋
if(isRed(node.right) && !isRed(node.left))
node = leftRotate(node);
// 当node左侧有两个连续的红色结点时,右旋
if(isRed(node.right) && isRed(node.right.right))
node = rightRotate(node);
// 当node的左右结点均为红色时,颜色翻转
if(isRed(node.left) && isRed(node.right))
flipColor(node);
return node;
}
}