avatar

什么是红黑树以及java实现

在学习红黑树的时候我们需要知道一些关于树的基础知识,还有什么是二叉查找树。
在学习的时候我发现人的智慧真的是无穷的,能设计出这么优秀的算法,真的是超级厉害。
30张图带你彻底理解红黑树
java代码实现红黑树源码

二叉查找树

二叉查找树也叫做二叉搜索树,二叉排序树,二叉排序树主要是为了实现二分查找,让查找更加高效

二叉查找树特征

  • 若它的左子树不为空,那么左子树所有节点的值都小于它的根节点的值
  • 若它的右子树不为空,那么右子树上所有节点的值都大于它的根节点的值,相等的值左右子树均可
  • 它的左右子树也分别是二叉查找树

平衡二叉查找树

别名AVL树,Balanced Binary Tree
要么它是一个空树,要么它的左子树和右子树都是平衡二叉树,并且左子树和右子树的深度之差的绝对值不超过1。

红黑树

红黑树也是二叉查找树,二叉查找树并不难,但是红黑树难的地方在于它是一个自平衡的二叉查找树,能够在破坏二叉树平衡的时候自己去找平衡通过左旋和右旋,这就是非常牛逼的,真的是非常牛逼。
红黑树是一个非完美平衡二叉树,是一个完美黑色平衡二叉树

红黑树的定义和性质

红黑树是一种含有红黑结点并能自平衡的二叉查找树,这里我插一嘴红黑只是为了区分的两个对立元素不是只能叫红黑,你叫黄黑也行。。
红黑树必须满足下面几个特性

  • 每个节点要么是黑色要么是红色
  • 根节点为黑色
  • 每个叶子节点为黑色(null)是黑色的
  • 每个红色节点的两个子节点都是黑色的。(就是说红色节点不能相互连接)
  • 从任意节点到其每个叶子的所有路径都包含相同的黑色节点。
    红黑树不是完美平衡二叉树,可能出现左右子树的深度大于1的情况。但是从任意节点出发到叶子节点都有相同数量的黑色节点,称之为黑色完美平衡二叉树

红黑树各部分名称

为了后面的讲解不至于混淆,我们约定一下红黑树一些节点的叫法

我们把正在处理的节点叫做当前节点,把他的父亲叫做父节点,父亲的另一个孩子叫做兄弟节点,父亲的父亲叫做祖父节点

红黑树的基本操作

就是这些操作让红黑树能够变成一个自平衡的二叉查找树。

左旋

以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。

右旋

以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。

变色

结点的颜色由红变黑或由黑变红。

红黑树数据结构的定义

package RedBlackTree;

/**
* 红黑树的节点值
* @param <T> 是实现了比较接口的键值
* @param <D> 真实数据,存啥值都行
*/
public class Node<T extends Comparable<T>, D> {
private RedBlackColor color;//节点颜色,这里仅仅是一个象征性的东西,换成啥都是可以的
private T key;//键值 真正在红黑树中进行比较的值
private D data;//具体的数据,键值对应的数据
private Node<T, D> parent;
private Node<T, D> leftChild;
private Node<T, D> rightChild;
/**
* 红黑树节点对象的构造函数
*/
public Node(RedBlackColor col, T key, D data, Node<T, D> parent, Node<T, D> leftChild, Node<T, D> rightChild) {
this.color = col;
this.key = key;
this.data = data;
this.parent = parent;
this.leftChild = leftChild;
this.rightChild = rightChild;
}
public Node<T, D> GetParent() {
return this.parent;
}
public Node<T, D> GetLeftChild() {
return this.leftChild == null ? new Node<T, D>(RedBlackColor.BLACK,null,null,this,null,null) : this.leftChild;
}
public Node<T, D> GetRightChild() {
return this.rightChild == null ? new Node<T, D>(RedBlackColor.BLACK,null,null,this,null,null) : this.rightChild;
}
public T GetKey() {
return this.key;
}
public RedBlackColor GetColor() {
return this.color;
}
public D GetData()
{
return this.data;
}
public void setParent(Node<T,D> parent) {
this.parent = parent;
}
public void setColor(RedBlackColor color) {
this.color = color;
}
public void setLeftChild(Node<T, D> leftChild) {
this.leftChild = leftChild;
}
public void setRightChild(Node<T, D> rightChild) {
this.rightChild = rightChild;
}
public void setData(D data)
{
this.data = data;
}
}

这里的定义稍微有点复杂,大家想实现可以尽可能的少的去做,或者做成一个RedBlackTree类的内部类

红黑树的查询

红黑树的查询操作其实就是树的查找操作,只需要比较根节点大小递归或者非递归方式查询即可

public D GetData(T key)
{
Node<T,D> node = search(key,root);
return node == null ? null : node.GetData();
}
public Node<T,D> search(T key,Node<T,D> root)
{
if(root!=null)
{
int com = key.compareTo(root.GetKey());
if(com < 0) return search(key,root.GetLeftChild());//com小于零代表key小于root的key值,所以应该去左子树
else if(com > 0) return search(key,root.GetRightChild());//com大于零代表key大于root的key值所以应该去右子树
else return root;
}
return null;
}

红黑树的左旋

红黑树左旋有这么几步

  • 当前节点下移
  • 当前节点的右孩子上移
  • 当前节点的右孩子的左子树给当前节点
    当前节点的父节点变成左孩子,右孩子取代当前节点的位置,当前节点的右子树变成右孩子的左子树。
/**
* 对某个节点进行左旋
* (当前节点就是父亲节点,整体过程就是 父亲下沉,右孩子上升,然后右孩子的左节点变成了原父亲的右节点)
* @param node 要旋转的当前节点
*/
public void leftRonate(Node<T,D> node)
{
//得到当前节点的右孩子
Node<T,D> right = node.GetRightChild();
if(right.GetLeftChild() != null)
{
right.GetLeftChild().setParent(node);//将自己的左子树的父节点变为当前节点
}
node.setRightChild(right.GetLeftChild());//将当前节点的右子树设置为右子树的左孩子
right.setLeftChild(node);//右孩子的左子树等于当前节点
right.setParent(node.GetParent());//将右子树的父节点设置为当前节点的父节点
if(node.GetParent() !=null)//判断node是在父节点的左子树还是右子树
{
if(node.GetParent().GetLeftChild() == node) node.GetParent().setLeftChild(right);
else node.GetParent().setRightChild(right);
}
else
{
this.root = right;//如果node是根节点那么将根节点设置为right
}
node.setParent(right);//将node的父节点设置为right
}

红黑树的右旋

右旋跟左旋类似,只不过是子树的切换和方向不同

  • 当前节点下移,
  • 当前节点的左孩子上移
  • 当前节点的左孩子的右子树较给当前节点
public void Rightonate(Node<T,D> node)
{
Node<T,D> left = node.GetLeftChild();
if(left.GetRightChild()!=null)
{
left.GetRightChild().setParent(node);
}
node.setLeftChild(left.GetRightChild());
left.setRightChild(node);
left.setParent(node.GetParent());
if(node.GetParent()!=null)
{
if(node.GetParent().GetLeftChild() == node) node.GetParent().setLeftChild(left);
else node.GetParent().setRightChild(left);
}
else
{
this.root = left;
}
node.setParent(left);
}

红黑树的插入

红黑树的插入操作包括两部分:第一部分是查找插入的位置;第二部分是插入后的自平衡。查找插入的父节点很简单,跟查找操作区别不大

当插入的位置找好以后,把插入的节点放到正确的位置就可以了,插入的节点一律代表是红色,只有当插入的节点是红色才会触发自平衡保持红黑树的性质,其中插入的场景有很多,各自的场景有各自的一些处理方式

场景一:红黑树为空

把插入结点作为根结点,并把结点设置为黑色。

插入情景2:插入结点的Key已存在

插入结点的Key已存在,既然红黑树总保持平衡,在插入前红黑树已经是平衡的,那么把插入结点设置为将要替代结点的颜色,再把结点的值更新就完成插入。

  • 把I设为当前结点的颜色
  • 更新当前结点的值为插入结点的值

插入情景3:插入结点的父结点为黑结点

由于插入的结点是红色的,当插入结点的黑色时,并不会影响红黑树的平衡,直接插入即可,无需做自平衡。

插入情景4:插入结点的父结点为红结点

再次回想下红黑树的性质2:根结点是黑色。如果插入的父结点为红结点,那么该父结点不可能为根结点,所以插入结点总是存在祖父结点。这点很重要,因为后续的旋转操作肯定需要祖父结点的参与。

插入情景4.1:叔叔结点存在并且为红结点

从红黑树性质4可以,祖父结点肯定为黑结点,因为不可以同时存在两个相连的红结点。那么此时该插入子树的红黑层数的情况是:黑红红。显然最简单的处理方式是把其改为:红黑红。

  • 将P和S设置为黑色
  • 将PP设置为红色
  • 把PP设置为当前插入结点 可以看到,我们把PP结点设为红色了,如果PP的父结点是黑色,那么无需再做任何处理;但如果PP的父结点是红色,根据性质4,此时红黑树已不平衡了,所以还需要把PP当作新的插入结点,继续做插入操作自平衡处理,直到平衡为止。
插入情景4.2:叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的左子结点

单纯从插入前来看,也即不算情景4.1自底向上处理时的情况,叔叔结点非红即为叶子结点(Nil)。因为如果叔叔结点为黑结点,而父结点为红结点,那么叔叔结点所在的子树的黑色结点就比父结点所在子树的多了,这不满足红黑树的性质5。

插入情景4.2.1:插入结点是其父结点的左子结点
  • 将P设为黑色
  • 将PP设为红色
  • 对PP进行右旋
插入情景4.2.2:插入结点是其父结点的右子结点
  • 对P进行左旋
  • 把P设置为插入结点,得到情景4.2.1
  • 进行情景4.2.1的处理
插入情景4.3:叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的右子结点

情景对应情景4.2,只是方向反转.

插入情景4.3.1:插入结点是其父结点的右子结点
  • 将P设为黑色
  • 将PP设为红色
  • 对PP进行左旋
插入情景4.3.2:插入结点是其父结点的右子结点
  • 对P进行右旋
  • 把P设置为插入结点,得到情景4.3.1
  • 进行情景4.3.1的处理

好了,讲完插入的所有情景了。可能又同学会想:上面的情景举例的都是第一次插入而不包含自底向上处理的情况,那么上面所说的情景都适合自底向上的情况吗?答案是肯定的。理由很简单,但每棵子树都能自平衡,那么整棵树最终总是平衡的。

代码实现

 /**
* 插入后的自平衡过程
* @param node 新插入的节点
*/
public void balanceInsertion(Node<T,D> node)
{
Node<T,D> parent,gparent;//父亲和祖父节点
//一开始插入的节点首先肯定是红色,假如父亲不为空且父亲也是红色,那就出现了“双红情况”
//必须做调整,进入循环
//假如父亲是黑色,没必要调整,为什么?
// 因为加一个红节点,不会影响这条路径上的黑色节点个数发生变化,所以不会影响红黑树的性质
while (((parent = node.GetParent())!=null )&& parent.GetColor() == RedBlackColor.RED)
{
//拿到父亲的父亲,也就是祖父
gparent = parent.GetParent();
//根据祖父来判断,父亲是祖父的左子树还是右子树,确定了之后的目的是为了拿到叔父
if(gparent.GetLeftChild() == parent)
{
//假如父亲是祖父的左孩子,那通过祖父的右孩子指针就能拿到叔叔节点
Node<T,D> uncle = gparent.GetRightChild();
//进一步,再分两种情况,会发生以下两种情况的前提是父亲为红色 :
if(uncle.GetColor() == RedBlackColor.RED)
{
//叔父是红色
//父亲变黑,叔父变黑,祖父变红
gparent.setColor(RedBlackColor.RED);
uncle.setColor(RedBlackColor.BLACK);
parent.setColor(RedBlackColor.BLACK);
//回溯,向上,这一步指针赋值,把祖父变成当前节点,继续向上判断,直到终止
node = gparent;
continue;
}
else
{
//叔父是黑色,父亲为红
if(parent.GetRightChild() == node)
{
//假如当前节点是父亲的右孩子
//父亲要左旋
leftRonate(parent);
Node<T,D> temp = node;
node = parent;
parent = temp;//这里对当前指针做交换改为父节点为当前指针
}
//因为上面发生了指针调换,这里parent其实已经指当前节点的指针
// 所以是当前节点位置上升变黑,父亲下沉还是红色,祖父变红
parent.setColor(RedBlackColor.BLACK);
gparent.setColor(RedBlackColor.RED);
//祖父右旋
//这里为什么祖父还要右旋一次呢?
/*
黑祖 黑祖
/ \ / \
红父 黑叔 --> 红插 黑叔
\ /
红插 红父
可以看到上面,经过了一次旋转之后,平衡性仍然满足,但是颜色就不对了,仍然存在双红情况
其实理解了AVL树的平衡调整,这里其实也是一个“双旋转”的过程,只有被删节点与父亲不同侧时才需要双旋
继续接着上面的双红情况,因为颜色不对,这时需要变色
黑祖 红祖
/ \ / \
红插 黑叔 --> 黑插 黑叔
/ /
红父 红父
可以看到,变色之后,右边的子树和左边子树的黑色节点个数保持一致,明显和原来不同,原来的图,看左上角,右子树的黑色节点要多一个
原来的树肯定是满足红黑树性质的,所以这里只需要做一次简单的右旋,就可以让右子树的黑色节点个数再次比左边多一个,满足了原来的情况
*/
rightonate(gparent);
}
}
else
{
//同理,先找到叔父
Node<T,D> uncle = gparent.GetLeftChild();
if(uncle.GetColor() == RedBlackColor.RED)
{
//这个过程其实和上面的是 一致的,不需要旋转,只需要重新染色即可
parent.setColor(RedBlackColor.BLACK);
uncle.setColor(RedBlackColor.BLACK);
gparent.setColor(RedBlackColor.RED);
node = gparent;
continue;
}
else
{
if(parent.GetLeftChild() == node)
{
//假如当前插入节点是父亲的左孩子,那就对父亲做右旋
//左孩子上升,父亲下沉
rightonate(node);
Node<T,D> temp = node;
node = parent;
parent = temp;//这里对当前指针做交换改为父节点为当前指针
}
//同理,祖父左旋
parent.setColor(RedBlackColor.BLACK);
gparent.setColor(RedBlackColor.RED);
leftRonate(gparent);
}
}
}
//调整后,假如都调整到根处了,必须要再次检验,让根为黑色,让它继续满足红黑树的性质
if (root == node) {
node.setColor(RedBlackColor.BLACK);
}
}
/**
* 黑树添加操作
*/
public void insertNode(T key,D data)
{
int com;//用于比较的返回值
Node<T,D> root = this.root;
Node<T,D> temp = null;
while (root!=null)
{
temp = root;
com = key.compareTo(root.GetKey());
if(com == 0)
{
root.setData(data);
return;
}
if(com < 0) root = root.GetLeftChild();
else root=root.GetRightChild();
}
Node<T,D> newNode = new Node<T, D>(RedBlackColor.BLACK,key,data,temp,null,null);
if(temp!=null)
{
com = newNode.GetKey().compareTo(temp.GetKey());
if(com<0) temp.setLeftChild(newNode);
else temp.setRightChild(newNode);
}
else
{
this.root = newNode;
//如果是根节点就直接返回就可以了
return;
}
//根据红黑树的性质,把默认节点设置为红色,向上回溯,更容易列举可能出现的情况,
// 所以这里新节点都默认设置成红色
newNode.setColor(RedBlackColor.RED);
//接下来这个就是最关键的方法 ,插入后的自平衡过程,调整让它保持红黑树的性质
balanceInsertion(newNode);
}

红黑树的删除

红黑树插入已经够复杂了,但删除更复杂,也是红黑树最复杂的操作了。但稳住,胜利的曙光就在前面了!
红黑树的删除操作也包括两部分工作:一查找目标结点;而删除后自平衡。查找目标结点显然可以复用查找操作,当不存在目标结点时,忽略本次操作;当存在目标结点时,删除后就得做自平衡处理了。删除了结点后我们还需要找结点来替代删除结点的位置,不然子树跟父辈结点断开了,除非删除结点刚好没子结点,那么就不需要替代。
二叉树删除结点找替代结点有3种情情景:

  • 情景1:若删除结点无子结点,直接删除
  • 情景2:若删除结点只有一个子结点,用子结点替换删除结点
  • 情景3:若删除结点有两个子结点,用后继结点(大于删除结点的最小结点)替换删除结点
    补充说明下,情景3的后继结点是大于删除结点的最小结点,也是删除结点的右子树中最左结点。那么可以拿前继结点(删除结点的左子树最左结点)替代吗?可以的。但习惯上大多都是拿后继结点来替代,后文的讲解也是用后继结点来替代。另外告诉大家一种找前继和后继结点的直观的方法:把二叉树所有结点投射在X轴上,所有结点都是从左到右排好序的,所有目标结点的前后结点就是对应前继和后继结点。 讲一个重要的思路:删除结点被替代后,在不考虑结点的键值的情况下,对于树来说,可以认为删除的是替代结点! 基于此,上面所说的3种二叉树的删除情景可以相互转换并且最终都是转换为情景1!
  • 情景2:删除结点用其唯一的子结点替换,子结点替换为删除结点后,可以认为删除的是子结点,若子结点又有两个子结点,那么相当于转换为情景3,一直自顶向下转换,总是能转换为情景1。(对于红黑树来说,根据性质5.1,只存在一个子结点的结点肯定在树末了)
  • 情景3:删除结点用后继结点(肯定不存在左结点),如果后继结点有右子结点,那么相当于转换为情景2,否则转为为情景1。
  • 综上所述,删除操作删除的结点可以看作删除替代结点,而替代结点最后总是在树末。有了这结论,我们讨论的删除红黑树的情景就少了很多,因为我们只考虑删除树末结点的情景了。
    同样的,我们也是先来总体看下删除操作的所有情景

删除情景1:替换结点是红色结点

我们把替换结点换到了删除结点的位置时,由于替换结点时红色,删除也不会影响红黑树的平衡,只要把替换结点的颜色设为删除的结点的颜色即可重新平衡。

删除情景2:替换结点是黑结点

当替换结点是黑色时,我们就不得不进行自平衡处理了。我们必须还得考虑替换结点是其父结点的左子结点还是右子结点,来做不同的旋转操作,使树重新平衡。

删除情景2.1:替换结点是其父结点的左子结点

删除情景2.1.1:替换结点的兄弟结点是红结点

若兄弟结点是红结点,那么根据性质4,兄弟结点的父结点和子结点肯定为黑色,不会有其他子情景,我们按照以下进行处理,得到删除情景2.1.2.3(后续讲解,这里先记住,此时R仍然是替代结点,它的新的兄弟结点SL和兄弟结点的子结点都是黑色)。

  • 将S设为黑色
  • 将P设为红色
  • 对P进行左旋,得到情景2.1.2.3
  • 进行情景2.1.2.3的处理
删除情景2.1.2:替换结点的兄弟结点是黑结点

当兄弟结点为黑时,其父结点和子结点的具体颜色也无法确定(如果也不考虑自底向上的情况,子结点非红即为叶子结点Nil,Nil结点为黑结点),此时又得考虑多种子情景。

删除情景2.1.2.1:替换结点的兄弟结点的右子结点是红结点,左子结点任意颜色

即将删除的左子树的一个黑色结点,显然左子树的黑色结点少1了,然而右子树又又红色结点,那么我们直接向右子树“借”个红结点来补充黑结点就好啦,此时肯定需要用旋转处理了。

  • 将S的颜色设为P的颜色
  • 将P设为黑色
  • 将SR设为黑色
  • 对P进行左旋
替换结点的兄弟结点的右子结点为黑结点,左子结点为红结点

兄弟结点所在的子树有红结点,我们总是可以向兄弟子树借个红结点过来,显然该情景可以转换为情景2.1.2.1。

  • 将S设为红色
  • 将SL设为黑色
  • 对S进行右旋,得到情景2.1.2.1
删除情景2.1.2.3:替换结点的兄弟结点的子结点都为黑结点

了,此次兄弟子树都没红结点“借”了,兄弟帮忙不了,找父母呗,这种情景我们把兄弟结点设为红色,再把父结点当作替代结点,自底向上处理,去找父结点的兄弟结点去“借”。但为什么需要把兄弟结点设为红色呢?显然是为了在P所在的子树中保证平衡(R即将删除,少了一个黑色结点,子树也需要少一个),后续的平衡工作交给父辈们考虑了,还是那句,当每棵子树都保持平衡时,最终整棵总是平衡的。

  • 将S设为红色
  • 把P作为新的替换结点
  • 重新进行删除结点情景处理

删除情景2.2:替换结点是其父结点的右子结点

删除情景2.2.1:替换结点的兄弟结点是红结点
  • 将S设为黑色
  • 将P设为红色
  • 对P进行右旋,得到情景2.2.2.3
  • 进行情景2.2.2.3的处理
删除情景2.2.2:替换结点的兄弟结点是黑结点
删除情景2.2.2.1:替换结点的兄弟结点的左子结点是红结点,右子结点任意颜色
  • 将S的颜色设为P的颜色
  • 将P设为黑色
  • 将SL设为黑色
  • 对P进行右旋
删除情景2.2.2.2:替换结点的兄弟结点的左子结点为黑结点,右子结点为红结点
  • 将S设为红色
  • 将SR设为黑色
  • 对S进行左旋,得到情景2.2.2.1
  • 进行情景2.2.2.1的处理
删除情景2.2.2.3:替换结点的兄弟结点的子结点都为黑结点
  • 将S设为红色
  • 把P作为新的替换结点
  • 重新进行删除结点情景处理

综上,红黑树删除后自平衡的处理可以总结为:
1、自己能搞定的自消化(情景1)
2、自己不能搞定的叫兄弟帮忙
3、兄弟都帮忙不了的,通过父母,找远方亲戚

代码实现

  /**
* 找到树的最小值
* @param node
* @return
*/
public Node<T,D> min(Node<T,D> node)
{
if(node.GetLeftChild()==null)
{
return node;
}
while (node.GetLeftChild()!=null)
{
node = node.GetLeftChild();
}
return node;
}
// public Node<T,D> max(Node<T,D> node)
// {
// if(node.GetRightChild()== null)
// {
// return node;
// }
// while (node.GetRightChild()!=null)
// {
// node = node.GetRightChild();
// }
// return node;
// }
/**
* 寻找待删节点的后继节点
* 因为这个节点即将要被删了,所以要选个后继节点补到这个位置来,
* 选择节点的规则:
* 这个规则和普通二叉树是一样的,要么就是找左子树的最大值,要么就是右子树的最小值)
*/
public Node<T,D> FindDeleteNodeReplace(Node<T,D> node)
{
if(node.GetRightChild()!=null)
{
return min(node.GetRightChild());//赵右子树的最小值
}
//下面这里是不会进入的,因为只有node的两个孩子都不为null时才会进入这个方法
//当node没有子节点的时候,那如果node是左孩子那个他的后继节点就是他的父节点
//如果node是右孩子,那么他的后继节点就是祖父节点
Node<T, D> y = node.GetParent();
while ((y != null) && (y.GetRightChild() == node)) {
node = y;
y = y.GetParent();
}
return y;
}
/**
* 红黑树删除节点
* @param node 传入的是要删除的节点
*/
public void delete(Node<T,D> node)
{
Node<T,D> child,parent,replace;
RedBlackColor color = RedBlackColor.BLACK;
//删除从整体上也分两种情况,然后两种情况下再细分
//如果要删除的节点,左右孩子都在,这种情况下比较复杂
if(node.GetLeftChild()!=null && node.GetRightChild()!=null)
{
//找到了替换节点,就是要接替待删节点指针的新节点
replace = FindDeleteNodeReplace(node);//这里会去找右子树的最小节点,不会走后面的代码
//找到替换节点的父亲节点
parent = replace.GetParent();
//因为替换节点已经是右子树中的最小值了,所以只有右孩子
child = replace.GetRightChild();
//在这里为什么要获取替换节点的颜色呢?
//可以这样想,因为替换节点的指针最终肯定是会丢失的,因为替换节点即将接受待删节点的指针,所以替换节点的指针就不再保留了
//既然不再保留,那说明原来替换节点这里肯定就少了一环,少了一个节点,所以这里就有判断它颜色的必要
//假如少的恰恰是黑色,那说明它会影响整棵树的平衡性,不再满足红黑树性质
color = replace.GetColor();
if(node == replace.GetParent())
{
//假如替换节点的父亲节点就是当前待删除节点
//那就直接把待删除节点的指针赋值给parent
parent = replace;
}
else
{
//假如替换节点的父亲不是待删除节点的父亲
if(child!=null)
{
//因为替换节点待会是要被删掉的,因为它的值会被放置到待删除节点中,然后把替换节点删除就相当于完成整个删除操作
//所以要为替换节点的孩子节点找到新父亲
child.setParent(replace.GetParent());
}
//然后替换节点的右孩子设置成替换节点的父亲的左孩子
replace.GetParent().setLeftChild(child);
replace.setRightChild(node.GetRightChild());
node.GetRightChild().setParent(replace);
}
//把目标删除节点node的父亲设置成替换节点的父亲
replace.setParent(node.GetParent());
replace.setLeftChild(node.GetLeftChild());
node.GetLeftChild().setParent(replace);
//除了指针的调整,颜色也要覆盖,替换节点既然来到了待删节点的位置,那么颜色也要沿用之前的颜色,这样才能满足整棵树的性质
replace.setColor(node.GetColor());
if(node.GetParent()!=null)
{
//待删节点的父亲节点假如不为空,那就要调整父亲节点的左右孩子指针
if(node.GetParent().GetLeftChild()==node)
{
node.GetParent().setLeftChild(replace);
}
else node.GetParent().setRightChild(replace);
}
else {
this.root = replace;
}
//上面整个过程就是用replace的指针完全取代了node节点,到此为止,node节点就是一个孤立节点了,就算是删除了
if (color == RedBlackColor.BLACK) {
balanceDeletion(child, parent);
}
}
else
{
//假如待删节点只有左子树或者右子树
if(node.GetLeftChild() !=null)
{
replace = node.GetLeftChild();
}
else
{
replace = node.GetRightChild();
}
//找到待删节点的父亲节点
parent = node.GetParent();
if(parent!=null)
{
//判断待删节点属于父亲的左还是右
if(parent.GetLeftChild()==node)
{
parent.setLeftChild(replace);
}
else
{
parent.setRightChild(replace);
}
}
else
{
//假如待删节点的父亲为空,那说明它原来就是根
//它被删了,所以孩子升为根
this.root = replace;
}
replace.setParent(parent);
color = node.GetColor();
//假如待删节点是黑色节点,那说明本次删除肯定会影响红黑树的性质
//删黑色节点,需要调整平衡,反之,删除红色节点,不需要调整
if(color == RedBlackColor.BLACK)
{
balanceDeletion(replace,parent);
}
}
}
/**
* 红黑树删除后的平衡调整
* (删除操作比较复杂,所以我加了很多过程状态追踪)
*
* @param node
* @param parent
*
* 入参只可能是下面这两种情况:
* 1. node=替换节点 parent=替换节点的父亲节点
* 2. node=替换节点的孩子节点 parent=替换节点
* 3. node=替换节点的孩子节点 parent=替换节点的父节点
*
* 无论是上面哪种情况,都满足:node和parent是儿子父亲的关系
*
* 首先要清楚,什么情况下会进入到这个方法?
* 1.待删节点是黑色节点(且待删节点只有一个子树)
* 或者
* 2.替换节点是黑色节点(待删节点的左右子树都不为空)
*
* 下面仔细分析下上面两种情况,为何只有这两种情况下,才有必要进行调整。
* 对于上面的情况1:
* 待删节点只有一个子树,说明只有一条路径,待删节点移除掉之后,只会影响一条路径上的黑色节点个数。
* 回忆下红黑树的性质,同一个节点出发直到叶子,每一个不同的路径下的黑色节点个数必须是相同的。
* 那既然它只有一个子树,说明只有一条路径。假如待删节点是红色,它移除掉之后,不会影响这条路劲下的黑色节点个数,所以不需要调整。
* 反之,假如它是黑色,它一旦被移除,这一条路径上的黑色个数就减少了,这样会导致这一条路径的黑色节点直接减一
* 这样就会导致这一条路径和 其他由树根出发的路径相比,不再相等。
*
* 对于上面的情况2:
* 因为待删节点下,有两个子树,那要让树继续保持二叉树的关系,那待删位置上必须放入一个符合规则的元素。
* 符合什么规则呢?也就是必须比左子树大,比右子树小,所以我们编程上通常选右子树的最小值。
* 待删元素被删,就要选右子树的最小值放到待删元素位置,那替换节点的指针就会丢失,因为做了移动。
* 既然右子树中有个元素被移除了,假如这个被移除指针的替换节点,是红色,不会对整棵树的平衡性有任何影响。
* 但假如这个替换节点是黑色,一旦被移除,会导致右子树的黑色个数减一,不再相等,所以需要调整。
*
* 对于一次调整过程,最多不会超过三次,这是为什么?
* 上述的情况1比较简单,不需要做任何旋转,只需要染色即可。
* 复杂的是情况2。。
* 情况2又可以分为细分两种情况:
* 1.替换节点在新父亲的左侧
* 2.替换节点在新父亲的右侧
* (只要理解了其中一种情况,另一种其实也清楚了)
*
*
* 下面只选其中一种情况,做总结
* 对于替换节点在新父亲的左侧这种情况,又可以细分成几种情况:
* 1.兄弟节点是红色:兄弟变黑,父亲变红,父亲左旋
* 2.兄弟节点是黑色
* 2.1.兄弟节点的左右孩子都是黑色:兄弟染红,整体指针(包括替换节点和其parent)向上回溯一步
* 2.2.兄弟节点的左孩子是红色,右孩子是黑色:兄弟染红,左孩子染黑,兄弟右旋
* 2.3.兄弟节点的右孩子是红色:父亲的颜色赋值到兄弟,父亲染黑,兄弟的右孩子染黑,父亲左旋
*/
public void balanceDeletion(Node<T,D> node,Node<T,D> parent)
{
Node<T,D> other;
while (node.GetColor() == RedBlackColor.BLACK && node!=this.root)
{
//假如替代节点的颜色也为黑,这里为什么要用“也”字,能进这个方法,肯定说明被删节点也是黑色
//替代节点也是黑色,那肯定要做调整了,不然整条路径,就少了一个黑色节点,最终肯定是不符合红黑树条件的
if(parent.GetLeftChild() == node)
{
//假如替代节点是其新父亲的左节点,那就通过右指针拿到其兄弟节点
other = parent.GetRightChild();
if(other.GetColor()==RedBlackColor.RED)
{
//假如兄弟是红色,那么父亲肯定是黑色
//兄弟与父亲调换颜色,父亲做左旋
parent.setColor(RedBlackColor.RED);
other.setColor(RedBlackColor.BLACK);
leftRonate(parent);
continue;
}
else
{
if(other.GetLeftChild().GetColor()==RedBlackColor.BLACK && other.GetRightChild().GetColor()==RedBlackColor.BLACK)
{
//假如兄弟节点没有任何孩子节点,也会进入这个代码分支,因为叶子也相当于是黑色的
other.setColor(RedBlackColor.RED);
node = parent;
parent = node.GetParent();
//这样就往上找
}
else if(other.GetLeftChild().GetColor() == RedBlackColor.RED && other.GetRightChild().GetColor() ==RedBlackColor.BLACK)
{
//兄弟节点左孩子是红色,右孩子是黑色
other.setColor(RedBlackColor.RED);
other.GetLeftChild().setColor(RedBlackColor.BLACK);
rightonate(other);
}
else if(other.GetRightChild().GetColor() == RedBlackColor.RED)
{
other.setColor(parent.GetColor());
parent.setColor(RedBlackColor.BLACK);
other.GetRightChild().setColor(RedBlackColor.BLACK);
leftRonate(parent);
break;
}
}
}else//代替节点是父节点的右节点
{
other = parent.GetLeftChild();
if(other.GetColor() == RedBlackColor.RED)
{
other.setColor(RedBlackColor.BLACK);
parent.setColor(RedBlackColor.RED);
rightonate(parent);
continue;
}
else
{
if(other.GetLeftChild().GetColor() == RedBlackColor.BLACK && other.GetRightChild().GetColor()==RedBlackColor.BLACK)
{
other.setColor(RedBlackColor.RED);
node = parent;
parent = node.GetParent();
}
else if(other.GetLeftChild().GetColor() == RedBlackColor.BLACK && other.GetRightChild().GetColor()==RedBlackColor.RED)
{
parent.setColor(RedBlackColor.RED);
other.GetRightChild().setColor(RedBlackColor.BLACK);
leftRonate(other);
}
else if(other.GetLeftChild().GetColor() == RedBlackColor.RED)
{
other.setColor(parent.GetColor());
parent.setColor(RedBlackColor.BLACK);
other.GetLeftChild().setColor(RedBlackColor.BLACK);
rightonate(parent);
break;
}
}
}
}
//在这里,node其实是即将放入被删位置的替代节点
//假如node是红色,那同时被删节点是黑色,
// 那说明,直接把node的颜色由红变黑,就直接满足了,不需要做任何旋转
node.setColor(RedBlackColor.BLACK);
}

全部代码

java实现红黑树

文章作者: zenshin
文章链接: https://zlh.giserhub.com/2020/03/12/cl35o0ngb00iwp4tgcpkb993f/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 zenshin's blog
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论