算法与数据结构系列源码:https://github.com/ThinerZQ/AllAlgorithmInJava
本篇博客源码下载:https://github.com/ThinerZQ/AllAlgorithmInJava/blob/master/src/main/java/com/zq/algorithm/tree/RedBlackTreeTest.java
什么是二叉查找树?
是不是某一颗子树的根节点左边的节点都小于根节点,右边的的节点都大于根节点。
什么是红黑树?
想想如果二叉查找树插入的元素是有序的,那么二叉查找树的高是不是就是n 了(元素个数),那么二叉查找树的时间复杂度是不是就是o(h)=o(n) 了
而红黑树是一种“平衡”查找树中的一种,可以保证在最坏的情况下基本动态结合操作的时间复杂度为o(lgn)
怎么保证呢?我们给红黑树一个严格的定义就好了嘛
定义:
红黑树是一颗二叉查找树,他在每个节点上增加了一个存储位来表示节点的颜色,可以是Red,或Black。通过对任何一条从根到叶子的简单路径上各个节点的颜色值进行约束,红黑树确保没有一条路径会比其他路径长出2被,因而是近似于平衡的。怎么约束呢?\
约束(性质):
- 每个节点或是红色的,或是黑色的。
- 根节点是黑色的。
- 每个叶节点(NIL)是黑色的
- 如果一个节点是红色的,则它的两个子节点都是黑色的
- 对每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点
说明:
NIL< 是为了方便处理红黑树代码中的边界条件,而设立的哨兵节点。
红黑树如下图所示:
节点类型设计:
/**
* 红黑树节点类
*/
class RedBlackTreeNode {
//数据
private RedBlackTreeNode parent;
private RedBlackTreeNode left;
private RedBlackTreeNode right;
private int data;
private TreeColor color;
public RedBlackTreeNode() {
}
//这里应该是一堆set,get方法
}
/**
* 树的颜色枚举类
*/
enum TreeColor {
Red("red"), Black("black");
TreeColor(String string) {
}
}
哨兵节点是一个与树中普通节点具有相同属性的对象,它的color属性为Black,其他属性可以设置为任意值。
黑高:
从某一个节点x出发(不包含该节点)到达一个叶节点的任意一条简单路径上的黑色节点的个数称为该节点的黑高。
红黑树的黑度为其根节点的黑高。
下面来说说怎么操作红黑树。
红黑树的操作比较难的地方在于插入元素和删除元素,这也是红黑树相对于二叉查找树不同的地方。要想讲清楚这个,必须先说红黑树的旋转操作。
旋转:
指针结构的修改是通过旋转来完成的,这是一种能保持二叉搜索树性质的搜索树局部操作。旋转就是怎么断开指针,和续上指针的过程。旋转分为左旋和右旋。
- 左旋:
- 右旋:
如下图所示:
代码:
/**
* 将x节点向左旋转,并不会改变节点值的数据大小关系,
* 左旋和右旋很简单,不需要考虑颜色的变化,
* 只是将节点断开,旋转,再连上。知道断开和连接的先后顺序就比较好理解了。
*
* @param x 需要旋转的节点
*/
public void left_rotate(RedBlackTreeNode x) {
//记录x的右孩子节点
RedBlackTreeNode y = x.getRight();
//设置x的右孩子为y的左孩子
x.setRight(y.getLeft());
//如果y的左孩子不为哨兵节点,设置y左孩子的父亲为x
if (y.getLeft() != NIL) {
y.getLeft().setParent(x);
}
//设置y的父亲为x的父亲
y.setParent(x.getParent());
if (x.getParent() == NIL) {
//如果x的父亲为哨兵节点,表明x是根节点,那么将y设置为根节点就好了
this.setRoot(y);
} else if (x == x.getParent().getLeft()) {
//如果x是x父亲的左孩子,那么设置x父亲的左孩子为y
x.getParent().setLeft(y);
} else {
//如果x是x父亲的右孩子,那么设置x父亲的右孩子为y
x.getParent().setRight(y);
}
//设置y的左孩子为x
y.setLeft(x);
//设置x的父亲为y
x.setParent(y);
}
/**
* 将x节点向右旋转,并不会改变节点值的数据大小关系,
* 左旋和右旋很简单,不需要考虑颜色的变化,
* 只是将节点断开,旋转,再连上。知道断开和连接的先后顺序就比较好理解了。
*
* @param x 需要旋转的节点
*/
public void right_rotate(RedBlackTreeNode x) {
//记录x的左孩子节点
RedBlackTreeNode y = x.getLeft();
//设置x的左孩子为y的右孩子
x.setLeft(y.getRight());
//如果y的右孩子不为哨兵节点,设置y右孩子的父亲为x
if (y.getRight() != NIL) {
y.getRight().setParent(x);
}
//设置y的父亲为x的父亲
y.setParent(x.getParent());
if (x.getParent() == NIL) {
//如果x的父亲为哨兵节点,表明x是根节点,那么将y设置为根节点就好了
this.setRoot(y);
} else if (x == x.getParent().getLeft()) {
//如果x是x父亲的左孩子,那么设置x父亲的左孩子为y
x.getParent().setLeft(y);
} else {
//如果x是x父亲的右孩子,那么设置x父亲的右孩子为y
x.getParent().setRight(y);
}
//设置y的右孩子为x
y.setRight(x);
//设置x的父亲为y
x.setParent(y);
}
分析:
左旋右旋就是把需要旋转的节点的左右孩子提升一层,然后断开相应的指针,在续上相应的指针保持二叉搜索树的性质。这里的旋转和二叉搜索树基本上一致
旋转说完了,咋们来说插入。
插入:
当然是必备的操作啦,想想看,原来的二叉搜索树的插入过程只需要找到插入位置,判断是插入到左边还是右边就行了,那么可能不行了,因为插入完了之后,会破坏红黑树的性质啊,那么怎么边插入边使红黑树的性质得到保持呢。这就需要插入的过程中修补(fix-up)红黑树的性质。
那么什么情况需要修补呢?假设 z 是刚插入的节点。
当第一次插入的时候,z必然是根节点,但是z的父亲是NIL节点是黑色,所以为了保持性质2,需要在某个地方将根节点设置为Black。当然在插入的过程中也可能出现根节点变为红色的情况,那么无论如何将根节点设置为Black就行了。
因为刚插入的节点 z 的默认设置是Red, 而当 z 的父亲是红色的时候破坏了性质4,需要修补,如果z 的父亲不是红色,没有破坏任何性质。有了这个前提条件接下来再看。
在整个过程中只有性质2,4可能被破坏
z 的叔叔节点是z 的爷爷的右孩子
情况1:z 的叔叔节点 y 是红色的
情况2:z 的叔叔节点 y 是黑色的,且 z 是一个右孩子
情况3:z 的叔叔节点 y 是黑色的,且 z 是一个左孩子
z 的叔叔节点是 z 的爷爷的左孩子
情况4:z 的叔叔节点 y 是红色的
情况5:z 的叔叔节点 y 是黑色的,且 z 是一个右孩子
情况6:z 的叔叔节点 y 是黑色的,且 z 是一个左孩子
1,2,3和4,5,6是对称的,如下图所示,看图和上面的说明仔细思考一下吧
插入:
/**
* 插入函数,用来构造一个插入节点。
*
* @param k 插入的数据值
*/
public void insert(int k) {
RedBlackTreeNode node = new RedBlackTreeNode();
node.setData(k);
node.setColor(TreeColor.Red);
//将需要插入的节点的所有元素都设置成哨兵节点
node.setParent(NIL);
node.setLeft(NIL);
node.setRight(NIL);
//调用红黑二叉树的插入方法
rb_insert(node);
}
/**
* 用于红黑二叉树的插入方法
*
* @param z 插入的节点对象
*/
public void rb_insert(RedBlackTreeNode z) {
//y是待插入的节点位置,默认也是哨兵节点
RedBlackTreeNode y = this.NIL;
//x是,根节点,不断比较数据值,直到直到带插入的位置
RedBlackTreeNode x = this.getRoot();
while (x != this.NIL) {
y = x;
if (z.getData() < x.getData()) {
x = x.getLeft();
} else {
x = x.getRight();
}
}
//将z插入到y后面
z.setParent(y);
if (y == NIL) {
//如果z是第一个插入的值,将z设置为根节点
this.setRoot(z);
} else if (z.getData() < y.getData()) {
//将z插入到y的左边
y.setLeft(z);
} else {
//z插入到y的右边
y.setRight(z);
}
//插入节点可能破坏了红黑树性质,所以调用修补方法
rb_insert_fixup(z);
count++;
System.out.println("insert:" + z.getData());
}
/**
* 红黑树插入修补方法,主要有三种情况需要修补
* 情况1:z的叔叔节点y是红色的
* 情况2:z的叔叔节点y是黑色的,且z是一个右孩子
* 情况3:z的叔叔节点y是黑色的,且z是一个左孩子
*
* @param z 引起了性质变化的节点z
*/
public void rb_insert_fixup(RedBlackTreeNode z) {
//while循环是说z 的父亲的颜色如果是红色,就需要进行修补。
// 第一次插入元素的时候,z的父亲是哨兵节点,所以不用修补,直接执行while后面的设置根节点为黑色的语句
// 之后每次插入都需要判断z节点的父亲节点的颜色进行判断,因为插入的z节点初始设置为红色的,如果z的父亲节点为黑色,就不用修补了,如果为红色,那么z的父节点和z都是红色,那么就需要进行修补
while (z.getParent().getColor() == TreeColor.Red) {
//如果z的父亲 是z的爷爷的左孩子,这里当z是插入的是第二节点的时候,z的父亲是根节点,z的父亲的父亲是一个哨兵节点,所以走了else区域
if (z.getParent() == z.getParent().getParent().getLeft()) {
//得到z的叔叔,叔叔是z的爷爷的右孩子,所有的叔叔都有可能是哨兵节点,如果是哨兵节点的话,那么叔叔的颜色就是黑色
RedBlackTreeNode y = z.getParent().getParent().getRight();
//判断z的叔叔的颜色,如果叔叔是红色,那么z的父亲一定是红色,又因为刚插入的z是红色,所以违背了性质,
if (y.getColor() == TreeColor.Red) {
//情况1:z的叔叔节点y是红色的
//修改父亲的颜色为黑色
z.getParent().setColor(TreeColor.Black);
//叔叔的颜色也设置为黑色
y.setColor(TreeColor.Black);
//z的爷爷的颜色设置为红色,这样在z的爷爷为跟的子树上保持了性质
z.getParent().getParent().setColor(TreeColor.Red);
//开始从叶子节点往上走,可能z 的爷爷那么一辈的性质被破坏了,所以再次循环,去看z的爷爷的父亲的颜色是什么
z = z.getParent().getParent();
} else {
//表明z的叔叔的颜色,是黑色,此时z的父亲的颜色不确定,
//如果z是z的父亲的右孩子
if (z == z.getParent().getRight()) {
//情况2:z的叔叔节点y是黑色的,且z是一个右孩子,将z的父亲左旋,保持性质
z = z.getParent();
left_rotate(z);
}
//情况3:z的叔叔节点y是黑色的,且z是一个左孩子
//将z的父亲的颜色设置为黑色
z.getParent().setColor(TreeColor.Black);
//将z的爷爷的颜色设置为红色
z.getParent().getParent().setColor(TreeColor.Red);
//将z的爷爷右旋
right_rotate(z.getParent().getParent());
//情况2后面紧跟情况3 ,是因为情况2并没有对颜色进行修改,只是调整了大小关系,通过情况3对颜色的修改,和旋转操作,才能达到平衡
}
} else {
//类似于上面
//如果z的父亲 是z的爷爷的右孩子
//得到z的叔叔,叔叔是z爷爷的左孩子
RedBlackTreeNode y = z.getParent().getParent().getLeft();
//判断z的叔叔的颜色,如果叔叔是红色,那么z的父亲一定是红色,又因为刚插入的z是红色,所以违背了性质,
if (y.getColor() == TreeColor.Red) {
//情况1:z的叔叔节点y是红色的
//修改父亲的颜色为黑色
z.getParent().setColor(TreeColor.Black);
//叔叔的颜色也设置为黑色
y.setColor(TreeColor.Black);
//z的爷爷的颜色设置为红色,这样在z的爷爷为跟的子树上保持了性质
z.getParent().getParent().setColor(TreeColor.Red);
//开始从叶子节点往上走,可能z 的爷爷那么一辈的性质被破坏了,所以再次循环,去看z的爷爷的父亲的颜色是什么
z = z.getParent().getParent();
} else {
//表明z的叔叔的颜色,是黑色,此时z的父亲的颜色不确定,
//如果z是z的父亲的右孩子
if (z == z.getParent().getLeft()) {
//情况2:z的叔叔节点y是黑色的,且z是一个左孩子,将z的父亲右旋,保持性质
z = z.getParent();
right_rotate(z);
}
//情况3:z的叔叔节点y是黑色的,且z是一个右孩子
z.getParent().setColor(TreeColor.Black);
//将z的爷爷的颜色设置为红色
z.getParent().getParent().setColor(TreeColor.Red);
//将z的爷爷右旋
left_rotate(z.getParent().getParent());
}
}
}
//不管怎么样,将根节点设置为黑色
this.getRoot().setColor(TreeColor.Black);
}
分析:
仅当情况1或者情况4发生,然后指针沿着树上升2层,while循环才会重复执行。此外,改程序所做的旋转不超过两次,因为只要执行了情况2或情况3,或者5,6 ,while循环就结束了。
接下来,咋们说删除。删除真的是最难的一部分。
咋们先说怎么将一个节点移到另外一个节点位置上,也是一个断开指针和续上指针的过程,很简单的:
代码:
/**
* 将一个元素移动到另外一个元素的位置上,用于删除操作过程
* 这个过程断开了原来的deleted节点和父亲的关系,将replace和deleted的父亲设置成了父子关系
* @param root 树根节点
* @param deleted 被删除的元素
* @param replace 用来移动到被删除元素位置上的元素
*/
public void rb_transplant(RedBlackTreeNode root,RedBlackTreeNode deleted,RedBlackTreeNode replace){
if (deleted.getParent() == NIL){
//被删除的节点是根节点,那么直接用replace节点来作为根节点
this.setRoot(replace);
}else if (deleted == deleted.getParent().getLeft()){
//被删除的节点是它父亲的左孩子,将replace设置为他父亲的左孩子
deleted.getParent().setLeft(replace);
}else {
//被删除的节点是它父亲的右孩子,将replace设置为他父亲的右孩子
deleted.getParent().setRight(replace);
}
//不管怎么样,肯定是要把replace的父亲设置成deleted的父亲的
replace.setParent(deleted.getParent());
}
那么什么情况下需要删除,怎么删除,什么情况下需要修补因为删除破坏了的红黑树性质,怎么调整?
删除操作分为3种情况:
1、待删除结点没有子结点,即它是一个叶子结点,此时直接删除
2、待删除结点只有一个子结点,则可以直接删除;如果待删除结点是根结点,则它的子结点变为根结点;如果待删除结点不是根结点,则用它的子结点替代它的位置。
3、待删除结点有两个子结点,首先找出该结点的后继结点(即右子树中数值最小的那个结点),然后将两个结点进行值交换(即:只交换两个结点的数值,不改变结点的颜色)并将待删除结点删除,由于后继结点不可能有左子结点,对调后的待删除结点也不会有左子结点,因此要把它删除的操作会落入情况(1)或情况(2)中。
上面的三种删除情况和二叉搜索数是一样的,但是在这个过程中需要记录 y 的原始的颜色,和 y的孩子 x。删除完了,如果 y 的原始颜色是黑色就需要修补红黑树的性质。
代码:
二、.红黑树的删除结点算法
1.待删除结点有两个孩子结点,操作如下:
(1)直接把该结点调整为叶结点(对应上面删除操作的,情况3)
(2)若该结点是红色,则可直接删除,不影响红黑树的性质,算法结束
(3)若该结点是黑色,则删除后红黑树不平衡。此时要进行“双黑”操作
记该结点为V,则删除了V后,从根结点到V的所有子孙叶结点的路径将会比树中其他的从根结点到叶结点的路径拥有更少的黑色结点, 破坏了红黑树性质4。此时,用“双黑”结点来表示从根结点到这个“双黑”结点的所有子孙叶结点的路径上都缺少一个黑色结点。
双黑含义:该结点需要代表两个黑色结点,才能维持树的平衡
如上图所示,要删除结点90,则删除后从根结点到结点90的所有子树结点的路径上的黑色结点比从根点到叶结点的路径上的黑结点少。因而,删除结点90后,用子结点NULL代替90结点,并置为“双黑”结点。
2 . 待删除结点有一个孩子结点,操作为:
该节点是黑色,其非空子节点为红色 ;则将其子节点提升到该结点位置,颜色变黑
3.“双黑”结点的处理
分四种情况:
(1)双黑结点 x 的兄弟结点 w 是红色结点
(2)双黑结点 x 的兄弟结点 w 是黑色,且有两个黑色子结点
(3)双黑色结点 x 的兄弟结点 w 是黑色,且w的左孩子是红色的,右孩子是黑色的
(4)双黑色结点 x 的兄弟结点 w 是黑色,且w的右孩子是红色的
(1)的处理方法如下图
(2)的处理方法如下图
(3)的处理方法如下图
(4)的处理方法如下图
删除操作代码:
/**
* 对外提供的删除某一个元素之的函数,
* @param k 需要删除的元素值
*/
public void delete(int k){
//找到需要删除的值对应的函数
RedBlackTreeNode node = searchRecursion(this.getRoot(), k);
//调用红黑二叉树的删除操作
rb_delete(this.getRoot(), node);
}
/**
* 红黑二叉树的删除操作,这个函数直观删除某一个元素,不管调整红黑树性质
* @param root 红黑树的根节点
* @param z 需要删除的节点
*/
public void rb_delete(RedBlackTreeNode root,RedBlackTreeNode z){
//维持节点 y 为 从树中 删除的节点 或者 需要 和删除节点互换 的节点。
RedBlackTreeNode y = z;
//记录下 y 的最开始的颜色
TreeColor y_original_color = y.getColor();
//声明 x 节点为 ,后面设置x 节点为 y节点的右孩子,或者左孩子
RedBlackTreeNode x = null;
//z的左孩子为空
//如果z 只有一个右孩子节点,这种情况只可能是 z 是某一个叶子节点的上一层节点,
if (z.getLeft() == NIL){
//进入到这里面说明z的右孩子可有可无。
// 如果 z 有右孩子的话,那么 z 一定是黑色的;;
// 如果 z 没有右孩子的话,那么 z 可能是红色的可能是黑色的,一定是叶子节点。
//因为如果z是红色节点,如果z存在叶子节点的话,那么必然同时存在2 个黑色叶子节点(性质3)
// 如果 z 是黑色节点,如果 z 存在叶子节点的话,如果存在的右孩子为红色,则正常; 如果右孩子为黑色(违反性质5)因为没有左孩子啊
//记录x 为 z的右孩子节点,不管 z 有没有右孩子,如果没有右孩子,z就是 NIL 节点
x = z.getRight();
//将 z 和 z 的 右孩子互换,不管 z 的右孩子是什么
rb_transplant(root, z, z.getRight());
}else if (z.getRight() == NIL){
//进入到这里面说明z的左孩子一定是有的,但是没有右孩子。
//这时候只有一种情况,就是 z 是黑色的,并且左孩子是红色的。。。
//因为如果 z 是红色节点,又因为z 是有左孩子的,那么z 必然有右孩子(否则违反性质4) ,不会进入到这里的
// 如果 z 是黑色节点,又因为z 是有左孩子的,如果左孩子是黑色的,那么 必然会有右孩子且是黑色的(否则违反性质5)。
//记录x 为 z的左孩子节点,z 一定是有左孩子的
x= z.getLeft();
//将 z和z的左孩子互换
rb_transplant(root, z, z.getLeft());
}else {
//进入到这里,说明 z 既有左孩子,又有右孩子
//记录下 z 的后继节点 y ,以后会将 y 和 z 互换
y= minResursive(z.getRight());
//保存下来 y 的原始颜色
y_original_color=y.getColor();
// 记录下 y 的右孩子节点,x 要么是一个NIL 节点或者一个真实的叶节点。
// 因为y 是某一颗子树的最小值,那么 y 必然是子树最左边的叶节点,或者叶节点的上一层节点。
//因为 如果y 为红色,那么y 必然是叶节点,因为如果 y不是叶节点,那么 y 必然带有黑色的两个子节点(性质4),那么y 就不会是子树最小值
//如果 y 为黑色,那么 y 可能是叶节点,也可能 y 有一个右孩子节点(不可能有左孩子节点,不然y就是不是最小值了),这个右孩子节点一定是红色,(如果不是红色,就违反了性质5)
x = y.getRight();
if (y.getParent() == z){
//如果 y 的父亲是 z,也就是说,z的后继就是 z 的孩子节点,,这时候 y 必然没有孩子节点,这里面的 x 是NIL节点
//将x 的父亲设置为 y,,似乎可以省略这里,待会儿再试
x.setParent(y);
}else {
//y 不是z 的右孩子,那么先将 y 和y 的右孩子互换,
rb_transplant(root,y,y.getRight());
//将y的右孩子设置成 z的右孩子
y.setRight(z.getRight());
//将y的右孩子和 y 续上关系
y.getRight().setParent(y);
}
//接下来,将y 与 z 互换
rb_transplant(root,z,y);
//将 y 和 z 左边部分续上关系
y.setLeft(z.getLeft());
y.getLeft().setParent(y);
//将 y 的颜色设置为 z 的颜色
y.setColor(z.getColor());
}
//如果 y 的颜色原来是黑色,那么可能破坏了性质,需要修补
//因为如果 y 的颜色如果是红色,那么 将 y 和 z 互换位置,不会破坏关系。
// 因为上面 y.setColor(z.getColor()); 将y的颜色设置为了以前 z的颜色,保持了性质1,2,3,4 ,接着y 又是红色,将红色的节点删除不会破坏性质5,
if (y_original_color == TreeColor.Black){
//这里的 x 要么是一个 叶节点,要么是一个NIL节点。
//为什么需要针对 x 进行修补? 因为可以看做 x 最后替换了 y的位置。(参考x 的赋值语句),
//将现在占有y 原来位置的节点 x 视为还有一重 额外的黑色。
rb_delete_fixup(root,x);
}
}
/**
* 修补因为删除了节点破坏了的红黑树性质
* @param root 红黑树的根
* @param x 需要修补的节点
*/
public void rb_delete_fixup(RedBlackTreeNode root,RedBlackTreeNode x){
while (x != root && x.getColor() == TreeColor.Black){
if (x == x.getParent().getLeft()){
RedBlackTreeNode w = x.getParent().getRight();
if (w.getColor() == TreeColor.Red){
w.setColor( TreeColor.Black);
x.getParent().setColor(TreeColor.Red);
left_rotate(x.getParent());
w =x.getParent().getRight();
}
if (w.getLeft().getColor() == TreeColor.Black && w.getRight().getColor() ==TreeColor.Black){
w.setColor(TreeColor.Red);
x=x.getParent();
}else {
if (w.getRight().getColor() == TreeColor.Black) {
w.getLeft().setColor(TreeColor.Black);
w.setColor(TreeColor.Red);
right_rotate(w);
w = x.getParent().getRight();
}
w.setColor( x.getParent().getColor());
x.getParent().setColor(TreeColor.Black);
w.getRight().setColor(TreeColor.Black);
left_rotate(x.getParent());
x=getRoot();
}
}else {
RedBlackTreeNode w = x.getParent().getLeft();
if (w.getColor() == TreeColor.Red){
w.setColor(TreeColor.Black);
x.getParent().setColor(TreeColor.Red);
right_rotate(x.getParent());
w =x.getParent().getLeft();
}
if (w.getRight().getColor() == TreeColor.Black && w.getLeft().getColor() ==TreeColor.Black){
w.setColor(TreeColor.Red);
x=x.getParent();
}else {
if (w.getLeft().getColor() == TreeColor.Black) {
w.getRight().setColor(TreeColor.Black);
w.setColor(TreeColor.Red);
left_rotate(w);
w = x.getParent().getLeft();
}
w.setColor(x.getParent().getColor());
x.getParent().setColor(TreeColor.Black);
w.getLeft().setColor(TreeColor.Black);
right_rotate(x.getParent());
x=getRoot();
}
}
}
x.setColor(TreeColor.Black);
}
引理:一颗有n个内部节点的红黑树的高度至多为:2lg(n+1)