一. 红黑树与2-3树
-
《算法导论》中的红黑树
1. 每个节点或者是红色的, 或者是黑色的
2. 根节点是黑色的
3. 每一个叶子节点(最后的空节点) 是黑色的
4. 如果一个节点是红色的, 那么他的孩子节点都是黑色的
5. 从任意节点到叶子节点,进过的黑色节点是一样的
-
红黑树与2-3树的等价性
理解了2-3树和红黑树之间的关系之后, 红黑树并不难
学习2-3树, 不仅对于理解红黑树有帮助
对于理解B类树(通常用于磁盘存储、文件系统、数据库),也是有巨大帮助的!
-
2-3树
1. 满足二分搜索树的基本性质
2. 节点可以存放一个元素或两个元素
a b c
/ \ / | \
二节点 三节点
2-3树是一棵绝对平衡的树:
从根节点到任意一个叶子节点,所经过的节点一定是相同的!
二.2-3树的绝对平衡性
2-3树如何保持绝对平衡?
保持平衡的关键是: 永远不会在空的地方添加节点
举例: 2-3树依次 加入 37, 42, 12, 18, 6, 11, 5
1. 37
------------------------------------------------
2. 37,42
------------------------------------------------
3. 12,37,42
37
/ \
12 42
------------------------------------------------
4. 37
/ \
12,18 42
------------------------------------------------
5. 37
/ \
6,12,18 42
37
/ \
12 42
/ \
6 18
12,37
/ | \
6 18 42
------------------------------------------------
6. 12,37
/ | \
6,11 18 42
------------------------------------------------
7. 12,37
/ | \
5,6,11 18 42
12,37
/ | \
6 18 42
/ \
5 11
6,12,37
/ | | \
5 11 18 42
12
/ \
6 37
/ \ / \
5 11 18 42
------------------------------------------------
三. 红黑树与2-3树的等价性
红黑树和2-3树本质上是一样的,
但2-3树的节点最多可以存2个元素, 不利于编写代码,
而红黑树每个节点只能存一个元素。
代码的简单编写
在以前二分搜索树BST的基础上修改, 作为红黑树代码的雏形 //****
表示修改代码的地方
RBT.java
import java.util.ArrayList;
public class RBTree<K extends Comparable<K>, V> {
private static final boolean RED = true; //****静态不可修改的变量
private static final boolean BLACK = false;
private class Node{
public K key;
public V value;
public Node left, right;
public boolean color;
public Node(K key, V value){
this.key = key;
this.value = value;
left = null;
right = null;
color = RED;//****默认新创建的节点为红色, 原因后续会讲解
}
}
private Node root;
private int size;
public RBTree(){//*****
root = null;
size = 0;
}
public int getSize(){
return size;
}
public boolean isEmpty(){
return size == 0;
}
//**** 判断节点node的颜色
private boolean isRed(Node node){
if(node == null)
return BLACK;
return node.color;
}
... //其他代码暂时保持BST的编码, 后面会修改
}
四. 红黑树的基本性质和复杂度分析
回顾算法导论中对红黑树的描述
1. 每个节点或者是红色的, 或者是黑色的
2. 根节点是黑色的
3. 每一个叶子节点(最后的空节点) 是黑色的
4. 如果一个节点是红色的, 那么他的孩子节点都是黑色的
5. 从任意节点到叶子节点,进过的黑色节点是一样多的
解读
1. 红黑树的特点, 不多说
2. 查看第一幅图, 红黑树只有两种节点(二节点和三节点),
每种节点的根部都为黑色节点, 所以根结点必然为黑色
3. 相当于定义:在红黑树中, 空节点为叶子节点, 并且为黑色。
对应到第二条性质, 空的红黑树的根节点也是黑色的
4. 依然对应第一幅图,只有两种节点,节点的根部都是黑色的
对于黑色的节点,右孩子一定是黑色的,左孩子不一定
5. 从2-3树推到红黑树, 2-3树每个节点都可以对应到红黑树的一个黑色节点。
总结
1. 红黑树的保持'黑平衡'的二叉树, 本质是因为2-3树是绝对平衡的
2. 严格意义上不是平衡二叉树
3. 最大高度 2logn O(logn)
4. 如果存储的数据会频繁的添加和删除,红黑树是很好的选择
5. 如果存储的数据不会频繁的改动,更多的是查询操作的话, 选择AVL树
五. 保持根节点为黑色和左旋转
首先回顾一下2-3树插入节点的过程:
2-3树中添加一个新的元素
要么添加进一个2-节点,形成一个3-节点
要么添加进一个3-节点,形成一个4-节点
红黑树也一样, 只是红黑树永远添加红色节点, 并且保持根节点为黑色节点。
先将add函数中加入保持根结点为黑色的代码
RBTTree.java
...
// 向红黑树中添加新的元素(key, value)
public void add(K key, V value){
root = add(root, key, value);
root.color = BLACK; // 最终根节点为黑色节点
}
...
红黑树添加新元素
添加的元素在依照二分搜索树那样添加后, 在左子树,无需其他操作。
如果添加的元素在右子树, 根据红黑树的性质, 我需要做左旋转
编写左旋转代码
// node x
// / \ 左旋转 / \
// T1 x ---------> node T3
// / \ / \
// T2 T3 T1 T2
private Node leftRotate(Node node){
Node x = node.right;
// 左旋转
node.right = x.left;
x.left = node;
x.color = node.color;
node.color = RED;
return x;
}
注意点:
如果在过程图中原来的37也是红色的, 那么在左旋转后42依然是红色的。
左旋转只是一个子过程,并没有解决红黑树的添加问题, 后面会完善。
六. 颜色翻转和右旋转
-
颜色翻转
向2-3树的3-节点添加元素,对应到红黑树中,第一种情况 如下图所示
37,42中添加66
2-3树中的最后的拆分过程,对应到红黑树中:
1. 先要把37和66变成黑色
2. 接着42变为红色, 因为红色表示会和父亲节点去融合。
编写颜色翻转代码
// 颜色翻转
private void flipColors(Node node){
node.color = RED;
node.left.color = BLACK;
node.right.color = BLACK;
}
-
右旋转
在3节点中添加元素的另一种情况:
向37,42中添加12
对应的,我们需要进行右旋转,才能和2-3树的逻辑保持一致
我们还需维护一下颜色:
第一步:
在右旋转之后,本质上依然是4节点, 所以37黑色,12和42变为红色
此时又回到了之前颜色翻转的情况
第二步:
再将37变为红色, 12和42变为黑色
右旋转代码(第二步会在颜色翻转的情况时进行维护, 右旋转中先不写)
// node x
// / \ 右旋转 / \
// x T2 -------> y node
// / \ / \
// y T1 T1 T2
private Node rightRotate(Node node){
Node x = node.left;
// 右旋转
node.left = x.right;
x.right = node;
x.color = node.color;
node.color = RED;
return x;
}
七. 红黑树中添加新元素
向3-节点添加元素,我们之前讲解了两种情况,分别是
1. 向37,42 添加66 66处于37,42后面
2. 向36,42 添加12 12处于37,42前面
现在还有第三种情况
3. 向37,42 添加40 40处于37,42中间
对于第三中情况的解决方案其实就是,前面所有方案的综合
解决流程:
这样,我们可以把三种情况放在一套流程中去解决
需要几次判断:
1.判断添加的元素是否处于中间,是的话按流程走,否则跳到下一个流程点中
2.判断添加的元素是否处于左边,是的话按流程走,否则跳到下一个流程点中
3.判断添加的元素是否处于右边(可不判断),最后进行颜色维护
根据上面的逻辑,修改add函数
RBTree.java
...
// 向红黑树中添加新的元素(key, value)
public void add(K key, V value){
root = add(root, key, value);
root.color = BLACK; // 最终根节点为黑色节点
}
// 向以node为根的红黑树中插入元素(key, value),递归算法
// 返回插入新节点后红黑树的根
private Node add(Node node, K key, V value){
if(node == null){
size ++;
return new Node(key, value); // 默认插入红色节点
}
if(key.compareTo(node.key) < 0)
node.left = add(node.left, key, value);
else if(key.compareTo(node.key) > 0)
node.right = add(node.right, key, value);
else // key.compareTo(node.key) == 0
node.value = value;
//****流程逻辑
if (isRed(node.right) && !isRed(node.left))
node = leftRotate(node);
if (isRed(node.left) && isRed(node.left.left))
node = rightRotate(node);
if (isRed(node.left) && isRed(node.right))
flipColors(node);
return node;
}
...
完成add之后,剩下的函数不需要修改于BST一致。
关于几种树结构的总结
1. 二分搜索树
对于完全随机的数据, 普通的二分搜索树很好用!
缺点: 极端情况下退化为链表(或高度不平衡)
2. AVL树
对于查询较多的使用情况,AVL树很好用!
3. 红黑树
牺牲了平衡型(2logn的高度)
统计性能更优(综合增删改查所有操作)