目录
在学习红黑树的实现原理之前,首先要明白为什么会出现红黑树?它是怎么来的?它存在的意义是什么以及解决了什么问题?要搞懂这些问题,就不得不先谈谈二叉搜索树和AVL树了。
一、二叉搜索树
1、 二叉搜索树的概念
二叉搜索树又称二叉排序树,它要么是一棵空树,要么是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值。
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值。
- 它的左右子树也分别为二叉搜索树。
上图就是一棵标准的二叉搜索树,从上述概念以及图中可以看出,二叉搜索树具有以下特性:
- 二叉搜索树中最左侧的节点是树中最小的节点,最右侧节点一定是树中最大的节点;
- 采用中序遍历遍历二叉搜索树,可以得到一个有序的序列,比如上图中序遍历的结果就是:0123456789。
2、二叉搜索树查找节点的过程
在二叉搜索树查找某个节点时,我们必须从根节点开始查找。
- ①、 如果查找值小于当前节点值,则搜索左子树。
- ②、 如果查找值大于当前节点值,则搜索右子树;
- ③、 如果查找值等于当前节点值,则停止搜索(终止条件)。
3、查找性能分析
进行插入和删除操作时,都必须先查找,所以查找效率在很大程度上决定了二叉搜索树中各个操作的性能。
对于有n个结点的二叉搜索树,查询一个节点的效率跟要查询的节点的深度呈正相关。即结点越深,则查询时要比较次数越多。
但是对于同一个关键码集合,如果各关键码插入的次序不同,就可能得到不同结构的二叉搜索树。此时查询效率也天差地别,如下图:
在上面两个二叉搜索树中,同样是查找9这个节点,第一个图只需要比较三次,而第二个图则需要比较7次。所以二叉搜索树的查询效率很依赖插入的次序:
- 最优情况下,二叉搜索树为完全二叉树,其平均比较次数为: l o g 2 N log_2 N log2N
- 最差情况下,二叉搜索树退化为单支树,其平均比较次数为: N 2 \frac{N}{2} 2N
4、如何避免二叉搜索树出现查询效率过低的问题?
从上面分析我们可以知道,二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年 发明了一种解决上述问题的方法:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(插入时需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。也就是AVL树。
二、AVL树
1、AVL树的特点
由于AVL树是从二叉搜索树发展而来的,所以:
- ①、 具有二叉搜索树的全部特性;
- ②、 每个节点的左子树和右子树的高度差最多等于1;
- ③、 它的左右子树都是AVL树。
从上图可以看到AVL树的每个节点,它的左右子树高度差都不会超过1。
在插入或者删除时,会发生左旋、右旋操作,使这棵树再次左右保持一定的平衡。AVL树基于这些特点,就可以保证不会出现大量节点偏向于一边的情况了。
2、为什么有了AVL平衡树还需要红黑树呢?
虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在 l o g 2 N log_2 N log2N。但是因为平衡树要求每个节点的左子树和右子树的高度差最多等于1,这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏这个规则。
而一旦破坏了这个规则,我们就需要不断地通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树。情况最差的时候,有可能一直要让旋转持续到根的位置才可以。
显然,如果在插入、删除很频繁的场景中,AVL平衡树就需要频繁地进行调整,这就会使的AVL平衡树的性能大打折扣,为了解决这个问题,于是就有了红黑树!!!
三、红黑树
1、红黑树概念
红黑树,是一种特殊的二叉搜索树,它给每个结点上都增加一个存储位表示结点的颜色,可以是红色或黑色。并且任何一条路径都不会比其他路径长出两倍,因而可以看作是接近平衡的。
红黑树的性质如下:
- ①、 每个节点要么是黑色,要么是红色;
- ②、 红黑树的根节点一定是黑色的;
- ③、 每个叶子结点都是黑色的(此处的叶子结点指的是最后的空结点NIL);
- ④、 如果一个节点是红色的,则它的两个孩子结点都是黑色的。不能有两个红色节点相连;
- ⑤、 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。
由性质5其实还可以推出一条性质,就是:
如果某一个节点存在一个黑色子节点,那么该结点肯定有两个子节点。否则黑色节点就不平衡了。
2、红黑树的特点
从上面的性质可以得知,红黑树并不是一个完美平衡二叉查找树,因为有可能根结点的左子树比右子树高,但左子树和右子树中的黑结点的层数是相等的,即任意一个结点到到每个叶子结点的路径都包含数量相同的黑结点(性质5)。
所以我们称红黑树的这种平衡为黑色完美平衡。只要一棵树满足上面五个性质,那么这棵树就是趋近与平衡状态的。
3、红黑树的定义
红黑树节点的定义如下:
class RBTreeNode{
RBTreeNode left = null; //左子节点
RBTreeNode right = null; //右子节点
RBTreeNode parent = null; //父节点
COLOR color = RED; //节点的颜色
int val; //节点值
public RBTreeNode(int val){
this.val = val;
}
}
思考一个问题:在节点的定义中,为什么要将节点的默认颜色给成红色的?
答:理由很简单,就是如果插入节点默认为红色,那么在父节点存在且为黑色节点时,红黑树的黑色平衡不会被破坏,也就不需要做自平衡操作。但是如果插入的结点默认是黑色,那么不管父节点是什么颜色,插入位置所在的子树的黑色结点总是多1,必须做自平衡。
4、红黑树中的操作
前面讲到红黑树能自平衡,它靠的是什么?靠的就是三种操作,分别为:左旋、右旋和变色。
-
①、变色: 这个很好理解,就是将一个结点的颜色由红变黑或者由黑变红;
-
②、左旋: 以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变,如下图:
看上图就很好理解左旋操作了,上图中以E为支点进行左旋操作。具体就是,让它的右子节点S成为它的父节点,并且让S的左子节点,成为E的右子节点。 -
③、右旋: 以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变,如下图:
上图中以S为支点进行右旋操作。具体就是,让它的左子节点E成为它的父节点,并且让E的右子节点,成为S的右子节点。
5、红黑树插入情景分析
插入操作包含两部分内容:
- ①、 查找插入的位置;
- ②、 插入后的自平衡操作。
情景1:红黑树为空树
这是最简单的一种情景,直接把插入结点作为根结点就行。但是根据红黑树性质2:根节点是黑色。所以还需要把插入结点变为黑色。
情景2:要插入的结点的Key已存在
这种情况下只需要更新节点的值就可以了。如下图:
情景3:插入结点的父结点为黑结点
由于插入的结点默认是红色的,所以当插入结点的父节点为黑色时,并不会影响红黑树的平衡。此时直接插入即可,无需做自平衡操作。如下图:
情景4:插入节点的父节点为红色
由于红黑树的性质2:根结点必须是黑色的,所以如果插入节点的父结点为红色时,那么该父结点必不可能是根结点,所以这时插入的结点总是存在祖父结点。这一点很关键,因为后续的旋转操作肯定需要祖父结点的参与。
而叔叔节点此时就有两种情况了,叔叔节点要么为红,要么为黑或者空。
由于叔叔节点存在不确定性,所以在上面的基础上,还需要进行分开讨论:
插入情景4–1:叔叔结点存在并且为红结点
父节点为红色,根据红黑树的性质4可知,两个红色节点不能相连,所以祖父结点肯定为黑结点。
那么插入这个红色节点后的红黑层数的情况就是:黑红红。因为不可以同时存在两个相连的红结点,所以最简单的处理方式就是把其改为:红黑红。具体的操作步骤就是:
- ①、 将父节点和叔叔节点改为黑色;
- ②、 将祖父节点改为红色;
- ③、 将祖父设置为当前节点,进行后续处理。
如果祖父节点的父结点是黑色,那么就无需再做任何处理了。
但如果祖父节点的父结点是红色,则违反红黑树性质了。需要将祖父节点设置为当前节点,继续做自平衡处理,直到平衡为止(这里可以使用递归操作来完成)。
插入情景4–2:叔叔结点不存在或为黑结点,并且插入结点的父亲结点是其祖父结点的左子结点
单纯从插入前情况来看,叔叔节点非红即空(NIL节点)。但是如果为黑的话,就会比当前插入节点的这条路径多一个黑色节点。这样就破坏了红黑树的性质5,造成此路径会比其它路径多一个黑色节点,所以叔叔节点肯定为NIL。
在4-2的基础上,插入节点的位置,是其父节点的左子节点还是右子节点,也会影响自旋操作,所以这时候又要分开讨论了。
插入情景4–2--1:新插入节点,为其父节点的左子节点
这种情况下分两步来处理:
- ①、 变颜色,将父节点设置为黑色,将祖父节点设置为红色;
- ②、 对祖父节点进行右旋操作。
插入情景4–2--2:新插入节点,为其父节点的右子节点
这依旧是在4-2的基础上来说的,即叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的左子结点。
这种情况下处理操作分为三步:
- ①、 对父节点进行左旋;
- ②、 将父节点设置为当前节点,得到4-2-1的情况,也就是LL红色情况;
- ③、 按照LL红色情况处理(1.变颜色 2.右旋祖父节点)
插入情景4–3:叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的右子结点
该情景对应情景4.2,只是方向反转。
这时候依旧要分开讨论插入节点的位置,是其父节点的左子节点还是右子节点。
插入情景4–3--1:新插入节点,为其父节点的右子节点
这种情况下的处理方法为:
- ①、 变颜色,将父节点设置为黑色,将祖父节点设置为红色;
- ②、 对祖父节点进行左旋。
插入情景4–3--2:新插入节点,为其父节点的左子节点
这依旧是在4–3的基础上来说的,即叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的右子结点。
这种情况下的处理操作分为三步:
- ①、 对父节点进行右旋;
- ②、 将父节点设置为当前节点,得到4–3--1的情况,也就是RR红色情况;
- ③、 按照RR红色情况处理(1.变颜色 2.左旋祖父节点)。
四、手写一棵红黑树
1、代码
package dataStructure;
public class RBTree<K extends Comparable<K>,V> {
//定义颜色常量
private static final boolean RED = true;
private static final boolean BLACK = false;
//树根节点的引用
private RBNode root;
public RBNode getRoot() {
return root;
}
//获取当前节点的父节点
private RBNode parentOf(RBNode node) {
if(node != null) {
return node.parent;
}
return null;
}
//判断节点是否为红色
private boolean isRed(RBNode node) {
if(node != null) {
return node.isColor() == RED;
}
return false;
}
//判断节点是否为黑色
private boolean isBlack(RBNode node) {
if(node != null) {
return node.isColor() == BLACK;
}
return false;
}
//设置节点为红色
private void setRed(RBNode node) {
if(node != null) {
node.setColor(RED);
}
}
//设置节点为黑色
private void setBlack(RBNode node) {
if(node != null) {
node.setColor(BLACK);
}
}
//中序打印二叉树方法
public void inOrderPrint() {
if(this.root != null) {
inOrderPrint(this.root);
}
}
//封装重载,方便调用
private void inOrderPrint(RBNode node) {
if(node != null) {
inOrderPrint(node.left);
System.out.println("key -> " + node.key + ", value -> " + node.value);
inOrderPrint(node.right);
}
}
/**
* 左旋方法
* 左旋示意图:左旋x节点
* p p
* | |
* x y
* / \ ----> / \
* lx y x ry
* / \ / \
* ly ry lx ly
*
* 左旋做了几件事?
* 1.将x的右子节点设置为y的左子节点,并将y的左子节点的父节点设置为x;
* 2.当x的父节点不为空时,将y的父节点设置为x的父节点,并将x的父节点指定为y
* 3.//将x的父节点更新为y,将y的左子节点更新为x
*/
private void leftRotate(RBNode x) {
RBNode y = x.right;
//将x的右子节点设置为y的左子节点
x.right = y.left;
//将y的左子节点的父节点设置为x
if(y.left != null) {
y.left.parent = x;
}
//当x的父节点不为空时,将y的父节点设置为x的父节点,并将x的父节点指定为y
if(x.parent != null) {
y.parent = x.parent;
//x是其父节点的左子树时,将y设置为x父节点的左子树
if(x == x.parent.left) {
x.parent.left = y;
}else {
x.parent.right = y;
}
}else {
//x没有父节点,说明x为根节点
this.root = y;
this.root.parent = null;
}
//将x的父节点更新为y,将y的左子节点更新为x
y.left = x;
x.parent = y;
}
/**
* 右旋方法
* 右旋示意图:右旋y节点
*
* p p
* | |
* y x
* / \ ----> / \
* x ry lx y
* / \ / \
*lx ly ly ry
*
* 右旋做了几件事?
* 1.将y的左子节点设置为x的右子节点,并将x的右子节点的父节点设置为y;
* 2.当y的父节点不为空时,将x的父节点设置为y的父节点,并将y的父节点指定为x
* 3.将y的父节点更新为x,将x的右子节点更新为y
*/
private void rightRotate(RBNode y) {
RBNode x = y.left;
//1.将y的左子节点设置为x的右子节点,并将x的右子节点的父节点设置为y;
y.left = x.right;
if(x.right != null) {
x.right.parent = y;
}
//2.当y的父节点不为空时,将x的父节点设置为y的父节点,并将y的父节点指定为x
if (y.parent != null) {
x.parent = y.parent;
if (y == y.parent.left) {
y.parent.left = x;
}else {
y.parent.right = x;
}
}else {
this.root = x;
this.root.parent = null;
}
//3.将y的父节点更新为x,将x的右子节点更新为y
x.right = y;
y.parent = x;
}
//对外公开的插入方法
public void insert(K key, V value) {
RBNode node = new RBNode();
node.setKey(key);
node.setValue(value);
node.setColor(RED);
insert(node);
}
//内部封装的插入方法
private void insert(RBNode node) {
//找到node的父节点
RBNode parent = null;
RBNode x = this.root;
while (x != null) {
parent = x;
//cmp > 0说明node.key大于x.key需要到x的右子树查找,否则cmp小于0,相等cmp=0
int cmp = node.key.compareTo(parent.key);
if(cmp > 0) {
x = x.right;
}else if(cmp < 0) {
x = x.left;
}else {
parent.setValue(node.getValue());
return;
}
}
node.parent = parent;
//判断node与parent的key谁大,然后决定node是它的左节点还是右节点
if(parent != null) {
if(node.key.compareTo(parent.key) < 0) {
parent.left = node;
} else {
parent.right = node;
}
}else {
this.root = node;
}
//插入之后需要进行修复红黑树,让红黑树再次平衡。
insertFixUp(node);
}
/**
* 插入后修复红黑树平衡的方法
* |---情景1:红黑树为空树。
* 处理方法:将根节点染为黑色
* |---情景2:插入节点的key已经存在
* 处理方法:不需要处理,这种情况不会调用到这个方法
* |---情景3:插入节点的父节点为黑色
* 处理方法:由于插入的结点是红色的,当插入结点的父节点为黑色时,并不会影响红黑树的平衡,
* 直接插入即可,无需做自平衡。
*
* 情景4 需要咱们去处理
* |---情景4:插入节点的父节点为红色
* |---情景4.1:叔叔节点存在,并且为红色(父-叔 双红)
* 处理方法:将父节点和叔叔节点改为黑色,将祖父节点改为红色,将祖父设置为当前节点,进行下一轮处理
* |---情景4.2:叔叔节点不存在,或者为黑色,父节点为爷爷节点的左子树
* |---情景4.2.1:插入节点为其父节点的左子节点(LL情况)
* 处理方法:将父节点设置为黑色,将祖父节点设置为红色,然后对祖父节点进行右旋
* |---情景4.2.2:插入节点为其父节点的右子节点(LR情况)
* 处理方法:对父节点进行左旋,得到LL双红情况(也就是4.2.1),然后父节点成为当前节点,再按照4.2.1的方法处理
* |---情景4.3:叔叔节点不存在,或者为黑色,父节点为爷爷节点的右子树
* |---情景4.3.1:插入节点为其父节点的右子节点(RR情况)
* 处理方法:将父节点设置为黑色,将祖父节点设置为红色,对祖父节点进行左旋处理
* |---情景4.3.2:插入节点为其父节点的左子节点(RL情况)
* 处理方法:对父节点进行右旋,得到RR双红色情况(也就是4.3.1),然后将父节点设置为当前节点,再按照4.3.1的方法处理
*/
private void insertFixUp(RBNode node) {
RBNode parent = parentOf(node);
RBNode gparent = parentOf(parent);
//情景4:插入节点的父节点为红色
//如果父节点是红色,因为根节点不可能是红色,所以一定存在爷爷节点
if(parent != null && isRed(parent)) {
RBNode uncle = null;
//如果parent是gparent的左节点,那么uncle一定是gparent的右节点
if(parent == gparent.left) {
uncle = gparent.right;
//情景4.1:叔叔节点存在,并且为红色(父-叔 双红)
if(uncle != null && isRed(uncle)) {
setBlack(parent);
setBlack(uncle);
setRed(gparent);
insertFixUp(gparent);
return;
}
//情景4.2:叔叔节点不存在,或者为黑色,父节点为爷爷节点的左子树
if(uncle == null || isBlack(uncle)) {
//情景4.2.1:插入节点为其父节点的左子节点(LL双红情况)
if (node == parent.left) {
setBlack(parent);
setRed(gparent);
rightRotate(gparent);
return;
}
//情景4.2.2:插入节点为其父节点的右子节点(LR情况)
if(node == parent.right) {
leftRotate(parent);
insertFixUp(parent);
return;
}
}
}else { //父节点为爷爷节点的右子树
uncle = gparent.left;
//情景4.1:叔叔节点存在,并且为红色(父-叔 双红)
if(uncle != null && isRed(uncle)) {
setBlack(parent);
setBlack(uncle);
setRed(gparent);
insertFixUp(gparent);
return;
}
//情景4.3:叔叔节点不存在,或者为黑色,父节点为爷爷节点的右子树
if(uncle == null || isBlack(uncle)) {
//情景4.3.1:插入节点为其父节点的右子节点(RR情况)
if(node == parent.right) {
setBlack(parent);
setRed(gparent);
leftRotate(gparent);
return;
}
//情景4.3.2:插入节点为其父节点的左子节点(RL情况)
if(node == parent.left) {
rightRotate(parent);
insertFixUp(parent);
return;
}
}
}
}
setBlack(this.root);
}
static class RBNode<K extends Comparable<K>,V> {
//父节点
private RBNode parent;
//左子节点
private RBNode left;
//右子节点
private RBNode right;
//颜色
private boolean color;
//key
private K key;
//value
private V value;
public RBNode() {
}
public RBNode(RBNode parent, RBNode left, RBNode right, boolean color, K key, V value) {
this.parent = parent;
this.left = left;
this.right = right;
this.color = color;
this.key = key;
this.value = value;
}
public RBNode getParent() {
return parent;
}
public void setParent(RBNode parent) {
this.parent = parent;
}
public RBNode getLeft() {
return left;
}
public void setLeft(RBNode left) {
this.left = left;
}
public RBNode getRight() {
return right;
}
public void setRight(RBNode right) {
this.right = right;
}
public boolean isColor() {
return color;
}
public void setColor(boolean color) {
this.color = color;
}
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
}
}
2、插入测试
就简单拿key测试一下,不输入value了
import java.util.Scanner;
public class RBTreeTest {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
RBTree<String,Object> rbt = new RBTree<>();
while (true) {
System.out.println("请输入key:");
String key = scan.next();
System.out.println();
rbt.insert(key,null); //value置为null就可以了
TreeOperation.show(rbt.getRoot()); //这个是封装好的打印红黑树的代码
}
}
}
运行结果:
请输入key:
a
a-B
请输入key:
b
a-B
\
b-R
请输入key:
c
b-B
/ \
a-R c-R
请输入key:
d
b-B
/ \
a-B c-B
\
d-R
请输入key:
d
b-B
/ \
a-B c-B
\
d-R
请输入key:
e
b-B
/ \
a-B d-B
/ \
c-R e-R
请输入key:
f
b-B
/ \
a-B d-R
/ \
c-B e-B
\
f-R
请输入key:
g
b-B
/ \
a-B d-R
/ \
c-B f-B
/ \
e-R g-R
请输入key:
h
d-B
/ \
b-R f-R
/ \ / \
a-B c-B e-B g-B
\
h-R
请输入key:
i
d-B
/ \
b-R f-R
/ \ / \
a-B c-B e-B h-B
/ \
g-R i-R
最后可以看到每条路径中的黑色节点数目都是2,且满足红黑树的其他性质,所以插入测试成功。