背景: 红黑树是一种相对平衡的二叉树。之所以相对平衡,是因为它是一个"黑平衡"的二叉树,并不绝对平衡(AVL 绝对平衡)相较于AVL, 红黑树的查询速度略慢, 但维护起来没有AVL代价那么高,所以被更加广泛使用。java 中的 hashmap, 当发生 hash 冲突时, 就使用了红黑树。红黑树极其复杂,网上很少有彻底讲通的教程,很多教程一上来就讲红黑树的定义,让人看了十分的头大。为此专门写一篇技术贴。完整代码可以访问我的 GitHub, 也欢迎大家底下留言。
2-3 树 与 红-黑 树
要想讲清楚什么是红黑树,就得从2-3树讲起。红黑树,本质上就是2-3树
什么是2-3树?
一句话,2-3树一个绝对平衡的多叉树,每一个节点的左右子树高度差不超过1。
2-3树中有两种节点。一种是2节点,与二分搜索树中的节点相同,每个2节点有两个子树。另一种是3节点,每个节点包含两个元素,每个节点有三个子树。 假设依次向2-3树中添加 21,17,15,13,11节点,整个过程如下:
当只有一个节点“21”时,这就是一个普通的二分搜索树,只有一个2节点。随后加入新节点“17”,“17”选择与“21”融合,形成一个3节点。继续加入新节点“15”,“15”先选择与现有的3节点融合形成4节点,随后分裂,形成3个2节点。继续加入新节点“13”,新节点直接与“15”融合形成一个3节点。接续加入新节点“11”, 新节点“11”先于“13”“15”融合形成一个4节点,随后分裂成三个 2节点,“13”节点继续向上融合,与上一层的“17”节点融合成3节点。
可以看出,即使我们以最极端的方式,从大到小(或是 从小到大)插入节点,2-3树依旧能保持平衡。
总结一下2-3树的操作规则:
-
新节点加入的时候永远选择与现有的节点进行融合
-
若融合成一个3节点,则到此为止
-
若融合成一个4节点(一个节点有三个元素),则进行分裂,分裂出来的上面一个节点向上继续融合,进入步骤1。
红-黑 树 的本质
红-黑树 本质上 就是将 2-3树 转换为 二叉树。其中的红色节点就是 2-3 树中的3节点的 左边一个节点,且红色节点是 2-3 树中的3节点的右边节点的左孩子
根据形象化地对比 红-黑 树 与 2-3 树可以很轻松地得出红黑树具有以下特征:
- 红黑树的根节点一定是黑色。
- 红黑树是一个相对平衡的二叉树,或者说是一个绝对 黑平衡 的二叉树,任意节点的左右子树的 黑高度 相等,任意两个叶子节点到他们的祖先节点经过的黑节点个数一定相等。
红-黑 树的实现
构造方法
红黑树的构造方法部分与二叉树相差不大,对于每一个节点额外用了一个 color 变量来表示节点的颜色。特别地,我们将新节点永远设置成红色节点,这样做的目的是为了在与2节点,三节点融合时标志出来,后续操作再根据实际情况设置颜色。
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;//2-3树插入一个节点,永远先去和叶子节点去融合
}
public Node(){
this.key=null;
this.value=null;
left=null;
right=null;
}
}
private Node root;
private int size;
public RBTree(){
root=null;
size=0;
}
}
其他常用方法
红黑树额外多了一个 getColor 方法用来返回节点 颜色
private boolean getColor(Node node){
if (node==null){
return BLACK;
}
return node.color;
}
下面几个方法与一般的二分搜索树一样,不做解释
public boolean contains(K key) {
return contains(root ,key);
}
private boolean contains(Node node,K key){
if (node==null){
return false;
}
if (key.compareTo(node.key)==0){
return true;
}else if (key.compareTo(node.key)<0){
return contains(node.left,key);
}else {
return contains(node.right,key);
}
}
public V get(K key) {
Node retNode=getNode(root,key);
if (retNode!=null){
return retNode.value;
}else {
return null;
}
}
private Node getNode(Node node,K key) {
if (node==null){
return null;
}
if (key.compareTo(node.key)==0){
return node;
}else if (key.compareTo(node.key)<0){
return getNode(node.left,key);
}else {
return getNode(node.right,key);
}
}
public void set(K key, V newValue) {
Node setNode=getNode(root,key);
if (setNode!=null){
setNode.value=newValue;
}else{
throw new IllegalArgumentException(key + "doesn't exist!");
}
}
public int getSize() {
return size;
}
public boolean isEmpty() {
return size==0;
}
向 红-黑 树 中增加节点
上一节说过 红-黑 树 本质上就是 2-3 树的二叉树表现形式。向红黑树中增加节点本质上就是向2-3树中增加节点。那么根据新节点的插入位置可以分为以下5中情况:
-
在二节点的左侧插入
-
在二节点的右侧插入
-
在三节点左侧插入
-
在三节点中间插入
-
在三节点右侧插入
在二节点左侧插入
这种情况最简单,直接与二节点进行合并形成一个三节点,在红黑树中只需要将新节点设置成红色即可。
特别的是,这里的 “37” 节点仍有左右子树,这是因为 “37” 节点可能是新加入的节点,也可能是下面节点向上进行融合的节点。这是不失一般性地表示。
在二节点的右侧插入
在二节点的右侧插入,需要与二节点融合成一个三节点。根据 红颜色 的节点定义,红节点是三节点左侧的节点,且红色节点是 2-3 树中的3节点的右边节点的左孩子。为此,需要对三节点的左侧节点进行左旋操作,然后再更改三节点中左右两个节点的颜色,将左侧设置为红色,将右侧设置为黑色。整个过程如下图所示:
特别的是,这里的 “42” 节点仍有左右子树,这是因为 “42” 节点可能是新加入的节点,也可能是下面节点向上进行融合的节点。这是不失一般性地表示。
左旋操作代码
private Node leftRotate(Node node){
Node x=node.right;
Node T1=node.left;
Node T2=x.left;
Node T3=x.right;
x.left=node;
node.right=T2;
return x;
}
在三节点的左侧插入
在三节点的左侧插入,融合成一个四节点。四节点的中间节点与两侧节点分离,两侧节点形成新的二节点,中间节点继续向上,与上面一层的节点进行融合。为此需要将中间节点设置为红色,两侧节点设置为黑色。在对应的红黑树中,需要对四节点的右侧节点进行右旋,然后进行颜色变换,将两侧节点设置为黑色,中间节点设置为红色,继续与上一层的节点进行融合。整个过程如下图所示:
右旋操作代码
private Node rightRotate(Node node){
Node x=node.left;
Node T1=x.right;
Node T2=node.right;
x.right=node;
node.left=T1;
return x;
}
在三节点中间插入
在三节点中间插入新节点,融合成一个四节点。四节点的中间节点与两侧节点分离,两侧节点形成新的二节点,中间节点继续向上,与上面一层的节点进行融合。为此需要将中间节点设置为红色,两侧节点设置为黑色。在对应的红黑树中,需要对四节点的左侧节点进行左旋,再对原四节点的右侧节点进行右旋。然后进行颜色变换,将两侧节点设置为黑色,中间节点设置为红色,继续与上一层的节点进行融合。整个过程如下图所示:
在三节点右侧插入
在三节点中间插入新节点,融合成一个四节点。四节点的中间节点与两侧节点分离,两侧节点形成新的二节点,中间节点继续向上,与上面一层的节点进行融合。为此需要将中间节点设置为红色,两侧节点设置为黑色。在红黑树中,直接进行颜色变换即可,无需额外操作。整个过程如下图所示:
至此增加节点的所有情况都已经分析完毕, 我们将其整合到一起就是这样:
public void add(K key, V value) {
root=add(root, key,value);
root.color=BLACK;
}
//向以node为根的二分搜索树中加入e
//并且返回node
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.value=value;
}else if (key.compareTo(node.key)<0 ){
node.left=add(node.left,key,value);
}else {
node.right=add(node.right,key,value);
}
//node字树改变了,需要维护平衡
//2节点左侧插入
if (getColor(node.left)==RED ){
return node;
}
//2节点右侧 插入
if (getColor(node)==BLACK && getColor(node.right)==RED){
node=leftRotate(node);
node.color=BLACK;
node.left.color=BLACK;
}
//3节点左侧插入
if (getColor(node.left)==RED && getColor(node.left.left)==RED ){
node=rightRotate(node);
//改变颜色
node.color=RED;
node.left.color=BLACK;
node.right.color=BLACK;
return node;
}
//3节点中间插入
if (getColor(node)==BLACK && getColor(node.left)==RED && getColor(node.left.right)==RED){
node.left=leftRotate(node.left);
node=rightRotate(node);
node.left.color=BLACK;
node.right.color=BLACK;
node.color= RED;
return node;
}
//3节点右侧插入
if (getColor(node)==BLACK && getColor(node.left)==RED && getColor(node.right)==RED){
node.color=RED;
node.left.color=BLACK;
node.right.color=BLACK;
return node;
}
//其他情况,说明当前节点的子树没有节点向上融合,直接return
return node;
}
我们可以看出,与二分搜索树相比,红黑树主要多出了在各种插入新节点的情况下,维护树的黑平衡。特别的是,每当插入新节点时,都需要逐层检查,看下层是否有新节点对与当层节点进行融合。
删除节点
红黑树删除节点更加复杂, 以后有空了再更新hhh。相信大家看到这里,已经完全明白了为什么要设计红黑树,红黑树如何维护平衡了哈