虽是读书笔记,但是如转载请注明出处 http://segmentfault.com/blog/exploring/
.. 拒绝伸手复制党
关于二叉树的基本知识,可以参见:Java 实现基本数据结构 2(树)
以下是算法导论第13章的学习笔记
红黑树
BST的各种操作的时间复杂度是依赖于树的高度,通过使得BST成为红黑树,确保每次对BST进行插入和删除之后,树的高度上限依然是logn
.
红黑树,本质上来说就是一棵二叉查找树,而且是平衡的查找二叉树 — 让BST效率更优
定义
红黑树中每个结点包含五个域:color,key,left,right
和p
。通过对一条从根到叶子的路径上各个节点着色方式的限制,红黑树确保没有一条路径会比其他路径长两倍。
如果某结点没有一个子结点或父结点,则该域指向 NIL。
我们把 NIL 视为二叉树的外结点 (叶子),而带关键字的结点视为内结点。
一棵二叉树如果满足下面的红黑性质,则为一棵红黑树:
1) 每个结点或是红的,或是黑的。
2) 根结点是黑的。
3) 每个叶结点 (NIL) 是黑的。
4) 如果一个结点是红的,则它的两个儿子都是黑的。
5) 对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点。
采用哨兵来代表 NIL,它的 color 域为 BLACK,其它域为任意值。
从某个结点 x
出发 (不包括该结点) 到达一个叶结点的任意一条路径上,黑色结点的个数称为该结点x
的黑高度,用bh(x)
表示。
引理:一颗有 n 个内结点的红黑树的高度至多为 2lg(n+1)。
操作
旋转
旋转的目的是让树保持红黑树的特性。
对 x 进行左旋,意味着,将 “x 的右孩子” 设为 “x 的父亲节点”;即,将 x 变成了一个左节点 (x 成了为 z 的左孩子)!。 因此,左旋中的 “左”,意味着 “被旋转的节点将变成一个左节点”。
对 x 进行右旋,意味着,将 “x 的左孩子” 设为 “x 的父亲节点”;即,将 x 变成了一个右节点 (x 成了为 y 的右孩子)! 因此,右旋中的 “右”,意味着 “被旋转的节点将变成一个右节点”
// 左旋x
public void rotate(TreeNode root, TreeNode x){
if(x.right != null){
//处理x的右孩子
TreeNode y = x.right;
x.right = y.left;
if(y.left != null)
y.left.parent = x;
// 处理x的父节点
y.parent = x.parent ;
if(x.parent != null){
// 判断y链接的位置
if(x.parent.left == x){
x.parent.left = y;
}
if(x.parent.right == x){
x.parent.right = y;
}
}else{
root = y;
}
// 链接新的父节点
x.parent = y;
y.left = x;
}
}
Note: 右旋转的时候可以把代码中的left换成right就好了。
插入
关于插入和删除整理自July大神的blog和youtube的短视频
youtube
重温下RedBlack tree的五条性质:
1 节点 r or b
2 根 b
3 叶子 b
4 红色节点孩子必为黑
5 任意节点其叶子节点的路径包含相同个数黑节点
红黑树插入过程的思想是:利用BST的插入方法,寻找待插入元素的位置并插入[所以这一部分可以把BST的直接挪过来]。然后(sth different:) 将待插入元素涂红色。为了保证红黑树的五条性质,需要调用辅助程序rbInsertFixup
来调整,对节点重新着色并旋转。
插入情况(插入的节点p设置为红
)有三:
1. 原tree为空树,所以p
设置为根节点 — 解决方案:Just 设置p
为黑就可以
2. 插入节点的父节点为黑
— 无需解决方法,插入后无影响。
3. ** 插入节点的父节点为红
— 需要rbInsertFixup
case 1: p
的父
节点和叔
叔节点都为红
— 解决方案:父+叔 都涂黑
;祖父涂红
,p = 祖父
从新的当前结点重新开始算法
case 2: p
的父
节点为红
,叔
为黑
,且p
是父节点的右
子 — 解决方案:p = 父
, 左旋p
(case2 实际上有两种,看youtube 视频时候才发现)
(这两种情况可以想象成一个菱形的两半。只要右子就左旋,左子就右旋)
case 3: p
的父
节点为红
,叔
为黑
,且p
是父节点的左
子 — 解决方案:父+叔 都涂黑
, 父节点涂黑
,祖父
涂红,祖父右旋。
(case3 实际上也有两种,这两种情况可以想象成两条直线,三角形除了底的两条边)
上面三种情况 (Case) 处理问题的核心思路都是:将红色的节点移到根节点;然后,将根节点设为黑色。
case2 和 case3 的区分是1. 二者二叉树的结构不同,菱形和三角 2. 解决方案不同,涂黑or not
最后,把根结点涂为黑
色,整棵红黑树便重新恢复了平衡。
//插入
public RBNode insert(RBNode root, RBNode x){
RBNode y = this.Nil; // Nil
RBNode p = root;
// if the node inserted is null
if(x == null){
return root;
}
// seek the place where x to be inserted
while(p!=null){
if(x.val > p.val){
y = p;
p = p.right;
}
if(x.val < p.val){
y = p;
p = p.left;
}
}
// insert
if(y == Nil){
root = x;
}
else
{
x.parent = y;
if(x.val > y.val){
y.right = x;
}
else{
y.left = x;
}
}
// something different from BST insert
x.left = Nil;
x.right = Nil;
x.color = 0; // set it red;
// fixup
rbInsertFixup(root, x);
return root;
}
public void rbInsertFixup(RBNode root, RBNode x){
// the fixup occurs when x.partent is red
while(x.parent.color == 0){
// 又分为父结点是祖父结点的左子还是右子,对于对称性,我们只要解开一个方向就可以了
if(x.parent == x.parent.parent.left){
RBNode uncle = x.parent.parent.right;
// case 1
if(uncle.color == 0){
x.parent.color = 1;
uncle.color = 1;
x.parent.parent.color = 0;
x = x.parent.parent;
}
else
{
// case 2
if(x == x.parent.right){
{
x = x.parent;
this.rotateLeft(root, x);
}
// case 3
{
x.parent.color = 1;
x.parent.parent.color = 0;
this.rotateRight(root, x.parent.parent);
}
}
else
{
// same as the clause with right and left child
}
}
}
root.color = 1;
}
}
删除
摘录整理自blog
将红黑树内的某一个节点删除。需要执行的操作依次是:
首先,将红黑树当作一颗二叉查找树,将该节点从二叉查找树中删除;
然后,通过 “旋转和重新着色” 等一系列来修正该树,使之重新成为一棵红黑树。详细描述如下:
第一步:将红黑树当作一颗二叉查找树,将节点删除。
这和 “删除常规二叉查找树中删除节点的方法是一样的”。分 3 种情况:
① 被删除节点没有儿子,即为叶节点。那么,直接将该节点删除就 OK 了。
② 被删除节点只有一个儿子。那么,直接删除该节点,并用该节点的唯一子节点顶替它的位置。
③ 被删除节点有两个儿子。那么,先找出它的后继节点;然后把 “它的后继节点的内容” 复制给 “该节点的内容”;之后,删除 “它的后继节点”。在这里,后继节点相当于替身,在将后继节点的内容复制给 “被删除节点” 之后,再将后继节点删除。这样就巧妙的将问题转换为 “删除后继节点” 的情况了,下面就考虑后继节点。 在 “被删除节点” 有两个非空子节点的情况下,它的后继节点不可能是双子非空。既然 “的后继节点” 不可能双子都非空,就意味着 “该节点的后继节点” 要么没有儿子,要么只有一个儿子。若没有儿子,则按 “情况①” 进行处理;若只有一个儿子,则按 “情况②” 进行处理。
第二步:通过 “旋转和重新着色” 等一系列来修正该树,使之重新成为一棵红黑树。
因为 “第一步” 中删除节点之后,可能会违背红黑树的特性。所以需要通过 “旋转和重新着色” 来修正该树,使之重新成为一棵红黑树。
性能
— | BST | 红黑树 | Btree |
---|---|---|---|
遍历 | O(n) | ||
插入 | O(h) | O(h) | 4 |
删除 | O(h) | O(h) | 1 |
查询 | O(h) | O(h) | 2 |
最小(大) | O(h) | O(h) | 2 |
后继(前驱) | O(h) | O(h) | |
旋转 | – | O(1) | – |
用途
红黑树的应用比较广泛,主要是用它来存储有序的数据,它的时间复杂度是 O(lgn),效率非常之高。
例如,Java 集合中的 TreeSet 和 TreeMap,C++ STL 中的 set、map,以及 Linux 虚拟内存的管理,都是通过红黑树去实现的。
和AVL比较
AVL比RBtree更加平衡,但是AVL的插入和删除会带来大量的旋转。
所以如果插入和删除比较多的情况,应该使用RBtree, 如果查询操作比较多,应该使用AVL.