简介
红黑树又名Red Black Tree(RBT),是一种自平衡二叉查找树,RBT中的每个节点都有颜色,要么是红色要么是黑色。有以下性质:
- 根节点是黑色
- 叶子节点都是不存储数据的黑色空节点
- 红色节点的儿子节点都是黑色
- 任何一个节点到其所有叶子节点路径上的黑色节点数都相同
注意:
- 性质2中的叶子节点是只为空(NIL或null)的黑色节点,不存储任何数据。
- 性质3和4可以保证没有一条路径会比其他路径长出一倍,因为最坏情况是某个节点的一个子树的每个黑色节点中间都有一个红色节点,而另一个子树中全都是黑色节点。
- RBT中的红和黑并没有特别的含义,只是为了区分两类节点,说成黑白树,红绿树等等都是一个意思。
- 从以上性质可以看出,RBT中的黑色节点要多于红色节点,红色节点是严格控制的。
- RBT也是二叉查找树,满足二叉查找树的基本性质。
下图是一颗标准的红黑树:
时间复杂度
一棵含有n个节点的红黑树的高度至多为 , 因此任何操作的时间复杂度为O()。
操作
红黑树的操作包含查询,增加,删除。查询操作和一般的二叉查找树一样。增加和删除操作和一般的二叉查找树也一样,只是在增加或删除节点后需要通过一系列的旋转,重新着色等操作来保证新的树满足红黑树的要求。这里的旋转和AVL的旋转原理一样,包含左旋,右旋,左右旋,右左旋。重新着色就是红色节点变黑色节点或者黑色节点变红色节点。RBT的增加和删除操作有太多的场景需要考虑,需要应用很多规则,因为自己没有验证过,所以这里暂不讨论,可以参考文章:https://www.cnblogs.com/yyxt/p/4983967.html。
红黑树和AVL树的比较
文章数据结构 - AVL树的Java实现介绍了AVL树的基本性质,我们知道AVL树也是自平衡二叉树,其任何节点的左右子树的高度差小于等于1。AVL树对平衡性的要求非常高,其通过一系列的旋转操作来保证平衡性。红黑树对平衡性的要求是任何一个节点的左右子树高度差小于等于一倍。例如,若其中一个子树的高度为L,那么另一个子树的高度在这个范围 。由于RBT减少了平衡性的要求,所以其再平衡的旋转和着色操作要少于AVL树,所以RBT的插入,删除性能要略好于AVL树。由于AVL树更加平衡,因此,AVL树的查询性能要略微好于红黑树。所以,在增加或删除操作比较频繁的场景下,我们可以选择红黑树,在查询操作比较频繁的场景下,我们可以选择AVL树。
在Java中的应用
我们知道,Java中的TreeMap,TreeSet的底层实现是红黑树,在Java8及以后的HashMap也可能会用到红黑树。这里简答分析下HashMap。
在Java7及以前版本中,HashMap解决冲突的办法是链地址法(拉链法),当某个存储桶中出现了冲突,则在该位置处开辟一个链表,依次存放冲突的元素。而在Java8及以后,如果链表的长度大于8,该链表就会转变成红黑树以提高查询性能。为什么是8?一种说法是根据泊松分布,正常情况出现8个冲突元素的概率非常低,8能满足绝大部分场景,除非是重写了hashCode方法并返回了不合理的hash值,导致hash值不够散列。
HashMap的默认容量是16,最大容量是2^30,如果当前容量大于扩容阈值,则进行2倍扩容。扩容阈值=当前容量 * 负载因子,负载因子默认为0.75。扩容后,新的扩容阈值=2 * 旧的扩容阈值。
扩容在一定程度上也可以解决冲突,例如:
那么,HashMap在什么时候使用红黑树呢?
如果某一个冲突的链表长度大于8且map容量大于等于64,则将链表转换成红黑树,如果map容量小于64,则只扩容。
HashMap并没有强制要求Key实现Comparable接口,如果实现了该接口,在转换成红黑树的过程中会通过compareTo方法来判断key的大小,否则使用System.identityHashCode()方法来判断。注意,此时不能使用hashCode方法,因为链表中key的hashCode肯定都相同,不然就不会有冲突了。System.identityHashCode()基于对象的物理内存地址来获取hash值,其可以在最大程度上保证不同对象有不同的hash值。即便重写了hashCode方法,也不影响System.identityHashCode()的结果。如果对象没有重写hashCode方法,那么hashCode方法和System.identityHashCode()方法的返回值是一样的。
TreeSet和TreeMap需要对Key实现Comparable接口,否则在操作时会报运行时异常。