一、2-3树
在了解什么是红黑树之前,首先需要补充一下什么是2-3树,因为它有助于我们对红黑树的理解,包括对B类树的理解。
2-3树可以有两个孩子或三个孩子,所以也就被称为2-3树,且2-3满足二分搜索树的基本性质。如左图中,a的左孩子值 < a,a右孩子的值 > a,在右图中可以存放两个元素b、c,该树有三个孩子,左孩子的值 < b,b < 中间孩子的值 < c,右孩子的值 > c。通常称左图含有两个孩子的节点为2节点,称右图中含有三个孩子的节点为3节点。
当我们需要对2-3进行搜索的时候,实际上也是和二分搜索树思路一致的,当我们搜索的过程来到一个3节点的话,搜索数x如果小于3节点的左值,则在3节点的左子树中继续寻找,如果搜索的这个树大于3节点的右值,到三节点的右子树中继续查找,如果搜索数x的值介于3节点的左值与右值之间,则在3节点的中间子树继续寻找。
对于2-3树来说,它是一个绝对平衡的树。即从根节点到任意一个叶子节点,所经过的节点数量一定是相同的。
理解在2-3树中添加节点时,是如何维护绝对平衡的,对我们理解红黑树的机制很有帮助。
2-3树在添加节点时,如果子树为空,则进行融合,如果融合之后是四节点,则分裂。
如图:
在添加元素6时,元素12不存在左子树,则元素12与元素6融合形成3节点
融合形成4节点则分裂
插入后形成3节点,且父亲节点为2节点时
插入后形成3节点,且父亲节点为3节点时
二、2-3树与红黑树的等价性
黑色为普通节点,红色为特殊节点,红黑组合相当于2-3树中的3节点,我们把3节点中的左节点b作为右节点c的 左子节点 ,因为左节点 < 右节点 ,之所以b为红色是因为在2-3树中,元素b元素c是作为3节点相融在一起的,但在红黑树中只有2节点,红色表示【红色的b】与其父节点【黑色的c】在2-3树是以3节点相融的,在红黑树中,所有的红色节点都是向左倾斜的。
转化为对应的红黑树:
三、红黑树的五大特性
1、每个节点或者是红色的或者是黑色的。
2、根节点是黑色的。
3、每一个叶子节点(最后的空节点NIL)是黑色的。
4、如果一个节点是红色的,那么他的孩子节点一定是黑色的。
5、从任意一个节点到叶子节点,所经过的黑色节点数量相同。
性质2证明: 根节点是黑色的
性质3是一种性质:每一个叶子节点(最后的空节点NIL)是黑色的
对于一棵空树本身,其根节点一定是黑色的,在极端的环境下,空树即是叶节点又是根节点,都是黑色的
性质4证明:如果一个节点是红色的,那么他的孩子节点一定是黑色的
在红黑树中,红黑节点组合代表的是2-3树中的3节点,3节点中左侧元素b对应红黑树中的红色节点,该红色节点的孩子为对应2-3树中的左孩子和中间孩子,若该孩子节点为2节点,如左边图所示,则一定为黑色;若孩子节点为3节点,则连接的形状如右图所示,先连接黑色节点,黑色节点的左孩子才为空色节点,所以如果一个节点它是红色的,它的孩子节点一定是黑色的。
拓展:对于一个黑色节点,他的左孩子可能为红色【对应2-3树的融合】,也可能为黑色。 总的来说:它的左孩子右可能为红色,有可能为黑色,但右节点一定是黑色节点。
性质5【红黑树核心特性】证明:从任意一个节点到叶子节点,所经过的黑色节点数量相同
因为红黑树和2-3树是等价的,2-3树是一棵绝对平衡的树。对于2-3树的任意一个节点出发,到叶子节点所经过的节点数一样多,由于2-3树是绝对平衡的树,所有的叶子节点都在同一层中。
在2-3树转化为红黑树时,对应2节点/3节点分别转化为黑色节点/红色和黑色节点2个节点,不管如何转化一定会存在一个黑色节点,所以从任意一个节点到叶子节点,对于2-3树来说经过的节点数相同,对红黑树来说经过的黑色节点数量相同。
红黑树是保持"黑平衡"的二叉树,严格意义上讲红黑树并不是平衡二叉树,左右子树的黑色节点保持着绝对的平衡,对红黑树来说,如果存在节点个数n,那么最大高度为2logn,时间复杂度为O(logn)。
AVL vs 红黑树
查找效率: AVL树 > 红黑树
增删改效率: AVL树 < 红黑树
四、向红黑树中添加新元素
由于2-3树和红黑树具有等价性,先回忆在2-3树中添加新节点时,永远不会添加到一个空节点【要么融合,要么产生临时4节点分裂】,所以添加进2节点时,会形成一个3节点,当添加进3节点时,会暂时形成一个4节点后分裂。
在红黑树中添加新元素时,设置为永远是红色节点,在递归结束后,我们需要手动设置根节点为黑色BLACK(boolean)。
新增元素时,有几种情况:
添加到对应2-3树中的2节点下:
1、新增元素添加到左右子树都为空的黑色节点[2-node]
①新插入节点比其根节点小,则作为根节点的左子节点,这种情况比较简单【相当于2-3树融合】
②新插入节点比根节点大,则作为根节点的右子节点,我们知道红色节点不可能在右侧,所以需要做调整
执行左旋转
步骤1:
步骤2:
为了表示37、42是一个3节点,则原来的元素37需要变为红色节点
其中第三句x.color = node.color,如果原来node颜色为红色节点,则打破了红黑树的基本性质,在这里左旋转只是一个子过程,左旋转后形成子树新的根节点x将会被返回做后续处理,在左旋转时并不维持红黑树的基本性质,只需要保证37、42两个元素对应2-3树中的三节点即可。
左旋转代码:
// 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;
}
2、新增元素添加到黑色节点[3-node]
①向原3节点的红黑树中添加一个大于其根节点42的值
步骤1:根据二分搜索树的添加规则,66添加到37的右节点上
对应的是一个临时的4节点【融合】 处理方式为:
步骤2:拆分成3个2节点
对应红黑树为3个黑色节点,让42的左右子节点都改变为黑色
步骤3:
此时其根节点42应该与42的父节点进行融合,所以42需要变为红色
我们发现42由黑色变为了红色,而37和66都由红色变为了黑色,该动作称为颜色翻转【flipColors】
//颜色翻转
private void flipColors(Node node){
node.color = RED;
node.left.color = BLACK;
node.right.color = BLACK;
}
②向原3节点的红黑树中添加一个小于其父节点的值
添加后:
此时产生了一个临时的4节点,处理方式仍然为分裂,即变为由3个2节点的子树,如图:
此时应该执行右旋转,步骤1:
旋转之前:
旋转之后:
步骤2:此时需要变换颜色,x的颜色需要改变为node的颜色,node的颜色需要设置为红色
最后执行我们编写过的颜色翻转即可,变为:
右旋转代码:
// node x
// / \ 右旋转 / \
// x T2 -------> y node
// / \ / \
// y T1 T1 T2
public Node rightNode(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进行左旋转,如图
步骤2:对节点42进行右旋转,如图:
步骤3:变色操作,对节点40变为42的颜色,将节点42变为红色
步骤4:执行颜色翻转
添加元素总结
这种过程即为我上述的向红黑树3节点中添加元素【添加的元素小于根节点但大于添加位置的父节点】的全过程
向红黑树3节点中添加元素【添加的元素小于根节点且小于添加位置的父节点】的全过程,即直接跳到了第三步
向红黑树3节点中添加元素【添加的元素大于根节点】的全过程,即直接跳到了第四步
左右旋转条件总体逻辑代码编写:
左旋转 右节点为红色且左节点不为红色
右旋转 左节点为红色且左节点的左节点也为红色
颜色翻转
在add方法中:【使用3个if 每次都顺序判断】
// 向以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 = rightNode(node);
//颜色翻转
if(isRed(node.left) && isRed(node.right))
flipColors(node);
return node;
}
红黑树的性能总结:
红黑树完整代码:
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;
}
// 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;
}
// 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;
}
// 颜色翻转
private void flipColors(Node node){
node.color = RED;
node.left.color = BLACK;
node.right.color = BLACK;
}
// 向红黑树中添加新的元素(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;
}
// 返回以node为根节点的二分搜索树中,key所在的节点
private Node getNode(Node node, K key){
if(node == null)
return null;
if(key.equals(node.key))
return node;
else if(key.compareTo(node.key) < 0)
return getNode(node.left, key);
else // if(key.compareTo(node.key) > 0)
return getNode(node.right, key);
}
public boolean contains(K key){
return getNode(root, key) != null;
}
public V get(K key){
Node node = getNode(root, key);
return node == null ? null : node.value;
}
public void set(K key, V newValue){
Node node = getNode(root, key);
if(node == null)
throw new IllegalArgumentException(key + " doesn't exist!");
node.value = newValue;
}
// 返回以node为根的二分搜索树的最小值所在的节点
private Node minimum(Node node){
if(node.left == null)
return node;
return minimum(node.left);
}
// 删除掉以node为根的二分搜索树中的最小节点
// 返回删除节点后新的二分搜索树的根
private Node removeMin(Node node){
if(node.left == null){
Node rightNode = node.right;
node.right = null;
size --;
return rightNode;
}
node.left = removeMin(node.left);
return node;
}
// 从二分搜索树中删除键为key的节点
public V remove(K key){
Node node = getNode(root, key);
if(node != null){
root = remove(root, key);
return node.value;
}
return null;
}
private Node remove(Node node, K key){
if( node == null )
return null;
if( key.compareTo(node.key) < 0 ){
node.left = remove(node.left , key);
return node;
}
else if(key.compareTo(node.key) > 0 ){
node.right = remove(node.right, key);
return node;
}
else{ // key.compareTo(node.key) == 0
// 待删除节点左子树为空的情况
if(node.left == null){
Node rightNode = node.right;
node.right = null;
size --;
return rightNode;
}
// 待删除节点右子树为空的情况
if(node.right == null){
Node leftNode = node.left;
node.left = null;
size --;
return leftNode;
}
// 待删除节点左右子树均不为空的情况
// 找到比待删除节点大的最小节点, 即待删除节点右子树的最小节点
// 用这个节点顶替待删除节点的位置
Node successor = minimum(node.right);
successor.right = removeMin(node.right);
successor.left = node.left;
node.left = node.right = null;
return successor;
}
}
}