感性认识,看书笔记——红黑树与二叉平衡树

二者都是针对于二叉排序树的缺点——最差可能形成单枝树,失衡,此时查找(新增,删除都是以查找为基础)退化为线性查找——而提出的解决方案:维持一颗相对平衡的二叉排序树。

AVL是一个平衡树的理想方案:任意节点的左右子树高度差不超过1

但是对于实际应用中,这个条件过于严苛,这样虽然可以保证树非常平衡——查找的复杂度接近logN即二分查找,但是代价就是维护(新增,修改,删除)的开销太大:新增修改或者删除节点时,查找到插入或删除位置以后(代价是O(logN)),插入修改或删除该节点代价是(O(l)),然后进行调整,但这可能需要从本节点一直调整(复衡)到根节点,最差代价O(logN)的复杂度。

而红黑树则是条件不是那么严格的平衡树

满足的条件:

  • 每个节点非红即黑(正如其名)

  • 根节点是黑的

  • 每个叶结点(指的是传统意义上的叶节点的孩子,即空节点,NIL)是黑的

  • 如果一个节点是红的,那么它的孩子一定是黑的

  • 对每个节点,从该结点到其叶子节点的所有路径上包含相同数目的黑节点

(最后一条不用区分叶节点是传统意义的叶节点还是NIL,因为第三条已经规定最后一个节点是黑色的NIL了)

  可以证明红黑树有以下两个性质:

① 一棵有n个内结点(不包括NIL)的红黑树高度至多为2lg(n+1)

② 以当前节点为根和起点的树,最长路径不超过最短路径的二倍

具体证明可见:红黑树相关定理及其证明

这两条定理都反应了一个事实:红黑树是相对平衡的树。

相对于AVL——任意节点的左右子树高度差不超过1——的严格平衡条件,从上面两条性质也可以看到红黑树的平衡性:① 从整体上把握了红黑树的高——给定节点总数n,红黑树保证了树高不超过2log(n+1)即对于任意查找,红黑树保证了递归的最大深度。② 从局部保证了红黑色的平衡性——对于每棵子树都满足这样的平衡性。实际上,整体上的性质①是由局部性质②(确切地说是红黑树的局部条件——红色标识的两条)来保证的,这从①的证明过程也可以知道。

BTW,从上面的分析可以看到,通常提及最多的AVL性质——任意节点的左右子树高度差不超过1——其实只是一个局部性质,咋一看上去这一条并不能保证给定节点总数的AVL最大高度——即最大递归深度,但是就如红黑树由局部平衡性推导整体最大树高一样,AVL由它的局部平衡性也很容易推导出最大树高——给定节点总数n,AVL保证树高不超过log(n)

证明思路可借鉴红黑树证明,先证逆否命题:对于树高为h的AVL,其最少节点需要2^h。接下来使用数学归纳法:h为0时,成立,假设h时也成立,即Nh >= 2^h,当h+1时,明显有Nh+1 >= 1+ Nh+Nh-1 >= 1 + 2^h+2^(h-1) >=

事实证明不可行,具体证明再找吧。AVL先到这里。

回到红黑树,从上面可知,红黑树的查找(新增,删除以查找为基础)的复杂度为2log(n+1)——仍然维持在logn级别的复杂度,虽然是AVL的logn的2倍,但起码还在一个数量级上——logn。但是新增或删除节点时,红黑树能够保证在线性时间内(实际上是3次以内)复衡完毕,这比AVL的复衡——最差logn——要快的多。虽然整体去看——修改或删除一个节点,AVL使用了logn(查找)+logn(复衡)的时间, 红黑树使用了2logn(查找)+O(l)的时间——好像二者没多大区别,甚至对于仅仅查找而言,AVL的logn < 红黑树2logn,但是在实际使用过程中,红黑树几乎不会出现2logn的高度,而AVL的复衡时间倒更有可能达到logn,所以考虑到在实际使用过程中的性能,红黑树的更优。

BTW,对于查找远远大于新增删除的情况(如树新建完成后,基本不会更新的情况),还是推荐AVL树。而如果树的高度不是太高,则根本不需要红黑树和AVL,直接用链表线性查找即可,一个明显的例子就是Java 8的HashMap在拉链长度小于6时由红黑树退化为链表。这是因为在长度比较小时,比如6平衡树的最大高度log6约为2.6(AVL)或者2log7的高度约为5.6(红黑树),这与长度为6的链表性能上区别不大,再考虑到平衡树的维护开销,还不如使用链表来得划算,更不要说长度小于6的情况了。当然,这个阈值到底取多少Java的设计者们应该做了很多实验来寻找,达到最佳的平衡点。这一点和DualPivotQuicksort.sort()中也有体现:小于47时使用插入排序,再大使用快排等等。

 综上,红黑树更像是BST和AVL在效率上的折中,更适合于具体应用。

(当然,对于AVL的插入,删除,修改的具体复杂度复杂度这里偷懒了,都概括为最差logN)

 

 

说到红黑树,就不得不谈到Java的TreeSet,它的实现就是红黑树,而TreeSet又是借用TreeMap实现的——将TreeMap的Key——Value的Value指向一个:

private static final Object PRESENT = new Object();

这个和HashSet与HashMap的关系是一样的,即都是用一个常量Value值的Map实现Set。

这里不多说TreeMap,从概念上认识一下Java的Set结构:

Iterable —— Collection —— Set ——  HashSet

                                                     ——  LinkedHashSet

                                                      —— SortedSet —— TreeSet

① HashSet不用多说,HashMap实现——哈希的思想即通过数组下标访问元素。

② LinkedHashSet不同于HashSet的地方即,HashSet遍历出来的顺序不一定是put进去的顺序,而LinkedHashSet在HashSet基本元素(Entry<K, V>)的基础上,在定义桶table[]类型的数据结构中加上了:

Entry<K,V> before, after;

这样每个几点不仅在key对应下标的数组的"坑"里,还通过自身的before和after指针指向了上一个或下一个节点,相当于位于数组上的不同桶之间相互指向,形成了双向链表。再通过LinkedHashSet自己维持的:

transient LinkedHashMap.Entry<K,V> head;和 transient LinkedHashMap.Entry<K,V> tail;

实现遍历时从头到尾或从尾到头的遍历。

BTW,原始HashMap的Entry<K,V>也是有一个next指针的,用于哈希冲突时,拉链法的连接。

超过TREEIFY_THRESHOLD = 8(桶成树)时,拉链要变成TreeMap,这时候before和after指针如何维持呢。。。。

还有小于UNTREEIFY_THRESHOLD = 6(树退化为桶),要怎么恢复?

③ TreeSet利用红黑树——本质是二叉排序树——实现了节点的有序性(当然这个大小是由具体的Comparable或Compator定义的),并由于红黑树优秀性质得到了效率上的折中。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值