红黑树到底是何方神圣?
本文的主角是红黑树,像Java8中的HashMap、TreeMap、TreeSet都使用了这种数据结构,下面来带大家认识一下。
红黑树是许多平衡搜索树(比如,还有前面学到的AVL树)中的一种,可以保证在最坏情况下时间复杂度为O(logn)。
红黑树是一颗二叉查找树,并且每个结点多了一个存储位来表示结点颜色。每个结点都有5个属性:
color:颜色
key:关键字
left:左孩子
right:右孩子
p:指针
-
每个结点要么是红色的,要么是黑色的。
-
根结点一定是黑色的。
-
叶子结点(NIL)一定是黑色的。 -
如果一个结点是红色的,那么它的两个子结点都是黑色的。 -
对每个结点x,从该结点到其所有后代叶子结点的简单路径,均包含相同个数的黑色结点, 黑色结点个数称为黑高 bh(x)。
当然是为了平衡啦,但是红黑树与AVL树相比,它是弱平衡树,而AVL树是高度平衡树。
如果一个结点没有子节点(即叶子结点)或没有父结点(即根结点),则该结点的指针属性为NIL,NIL是指针,且指向外部结点(称为NIL结点)。非NIL结点称为树的内部结点。
外部结点与内部结点如下图所示(内部结点旁的数字表示黑高,NIL结点黑高为0)。
为什么需要NIL结点呢,是因为红黑树的代码处理需要设置一个边界条件。
然而,如果有大量的NIL结点,肯定会浪费空间,NIL结点虽然也是一个普通结点,但仅仅只是color属性为黑色,其它属性没有任何意义,也不存储数据,因此可以用一个哨兵结点T.nil替换。
哨兵结点与内部结点如下图所示。
一颗有n个内部结点的红黑树高度至多为2log(n+1),这也是为什么红黑树能将时间复杂度控制在O(logn)以内。至于为什么是2log(n+1)大家可以查阅相关资料,比如算法导论一书中就有相关证明。
红黑树的结点插入和删除操作会破坏红黑树的性质,为了维护这些性质,就必须要改变树中某些结点的颜色以及指针结构,指针结构是通过旋转来完成的。
红黑树的插入和删除颇有些复杂,这里就不详细介绍了,下面主要介绍红黑树与AVL树的对比,这个比较重要。
RB-Tree和AVL树作为BBST(平衡二叉查找树),其算法时间复杂度都为O(logn),AVL作为最先提出的BBST,貌似RB-tree实现的功能都可以用AVL树是代替,那么为什么还需要引入RB-Tree呢?
AVL是严格平衡树,因此在增加或者删除节点的时候,旋转的次数比红黑树要多。
由于AVL高度平衡,因此AVL的Search效率更高。
AVL更平衡,结构上更加直观,但维护稍慢,空间开销较大。
红黑树不追求完全平衡,即不像AVL那样要求结点的左右子树高度差<=1。它只要求部分达到平衡,提出了为结点增加颜色,红黑是用弱平衡来换取增删结点时旋转次数的降低。
针对增删结点导致失衡后的rebalance操作,红黑树能够提供一个比较便宜的解决方案,降低开销,是对search、insert 、delete效率的折衷,总体来说,RB-Tree的统计性能高于AVL。
引入RB-Tree是功能、性能、空间开销的折中结果。
红黑树,读取略逊于AVL(比AVL会稍微不平衡最多一层,即红黑树只比相同内容的AVL树最多多一次比较),维护强于AVL,空间开销与AVL类似,内容极多时略优于AVL,维护优于AVL。
基本上主要的几种平衡树看来,红黑树有着良好的稳定性和完整的功能,性能表现也很不错,综合实力强。
实际应用中,若搜索的次数远远大于插入和删除,那么选择AVL,如果搜索,插入删除次数几乎差不多,应该选择RB。
看完本文后,能对红黑树有一个初步的了解,至少知道它是干嘛的,优点是什么,什么时候用它。但是本文没有介绍红黑树的原理,即为什么它能将时间复杂度控制在O(logn)以内,以及增删结点后违背红黑树性质又该如何调整。