红黑树全称是 自平衡的二叉红黑排序树。
即红黑树具有如下特性:
- 自平衡,可以自己维护平衡
- 二叉树
- 有颜色:红黑
- 具备排序能力,即树中节点有序
所以在学习红黑树数据结构之前,我们需要了解
- 树结构
- 二叉树(红黑树是二叉树)
- 二叉排序树(红黑树是二叉排序树)
- 平衡的二叉排序树(红黑树是自平衡的二叉排序树)
- 2-3-4树(红黑树起源于2-3-4树)
只有有了以上的基础知识,我们才能取学习红黑树。
生活中我们认识的树,如下图
树根在最下面
树干上有很多树杈,树杈上还有小树杈,树杈顶端有叶子
但是在数据结构中,树结构描述有所不同
树根在最上面,树根称为根节点
根节点可以有多个子节点,子节点又可以又多个子节点
最末端的子节点称为叶子节点
树的高度就是叶子节点到根节点的节点数
树(Tree) 是 n (n>=0) 个节点的有限集。在任一非空树中,只有一个根节点(root),其余节点可以组合为根节点的子树(subTree)。
树的节点:保存了具体的值,以及指向其他子树的引用。
节点的度:节点拥有的子树个数称为该节点的度。
叶子节点:度为0的节点称为叶子(Leaf)节点
分支节点:度不为0的节点称为分支节点
孩子节点:节点A的子树的根节点B称为A的孩子(Child)节点
双亲节点:A称为B的双亲(Parent)节点
兄弟节点:同一个根节点的多个节点称为兄弟(Sibling)节点
节点的层次:根节点为第一层,根节点的孩子节点为第二层,依次往后加
树的高度: 树中节点层次最大的层次数称为树的高度(或者叫深度)
上图就是一颗二叉树
二叉树特点:
1.树的任一节点的度不大于2。(即树中任一节点的子树个数不大于2)
2.二叉树中子树有左右区分。左边的子树称为左子树,右边的子树称为右子树。
二叉查找树的结构和二叉树没有区别。只是二叉查找树有如下要求:
1.若左子树不为空,则左子树上所有节点的值均小于它的根节点的值
2.若右子树不为空,则右子树上所有节点的值均大于它的根节点的值
3.树中没有两个值相等的节点
4.左右子树也为二叉查找树
从根节点开始查找,
- 要查找的值比当前节点的值大,则搜索右子树
- 要查找的值比当前节点的值小,则搜索左子树
- 要查找的值和当前节点的值相同,则返回该节点
要插入节点,则必须先找到插入节点的位置。
从根节点开始查找,
- 要插入的值比当前节点的值大,则比较右子树
- 要插入的值比当前节点的值小,则比较左子树
- 要插入的值和当前节点的值相等,因为二叉查找树不存在两个相同值的节点,所以不执行插入操作。
直到比较到左右子树为空时,则插入到对应位置。
- 前(根)序遍历:根左右
- 中(根)序遍历:左根右
- 后(根)序遍历:左右根
上图中:
前序遍历结果:10,8,7,9,12,11,13
中序遍历结果:7,8,9,10,11,12,13
后序遍历结果:7,9,8,11,13,12,10
BST三种遍历操作中,中序遍历用的最多。
从根节点开始,不断查询当前节点的左子节点,直到最后一个不为空的节点,该节点就是整棵树的最小值节点。
node = root;
while(node != null){
if(node.left != null){
node = node.left;
}
return node;
}
return null;
从根节点开始,不断查询当前节点的右子节点,直到最后一个不为空的节点,该节点就是整棵树的最大值节点。
node = root;
while(node != null){
if(node.right != null){
node = node.right;
}
return node;
}
return null;
前驱节点:小于当前节点的最大值节点
即:
- 当前节点有左子树时,其前驱节点就是其左子树中的最大值节点,如上图中,16的前驱节点是15
- 当前节点没有左子树,则向上递归到第一次左拐时节点的父节点就是其前驱节点,如上图中,如18的前驱节点是16
node = currentNode;
if(node != null){
if((beforeNode = node.left) != null){
while(beforeNode.right != null){
beforeNode = beforeNode.right;
}
return beforeNode;
} else {
p = node.parent;
c = node;
while(p!=null&&p.left==c){
c = p;
p = p.parent;
}
return p;
}
}
return null;
后继节点:大于当前节点的最小值节点
即:
- 当前节点有右子树时,其后继节点就是其右子树中的最小值节点,如上图中,21的后继节点就是23
- 当前节点没有右子树时,则向上递归到第一次右拐时节点的父节点就是其后继节点,如上图中,14的后继节点就是15
node = currentNode;
if(node != null){
if((afterNode = node.right) != null){
while(afterNode.left != null){
afterNode = afterNode.left;
}
return afterNode;
} else {
p = node.parent;
c = node;
while(p!=null&&p.right=c){
c = p;
p = p.parent;
}
return p;
}
}
return null;
删除操作前提:删除要保证删除后,BST树的节点还能保证有序
删除操作本质:是用被删除节点的前驱节点或者后继节点来替代。
- 叶子节点直接删除(叶子节点即没有前驱和后继节点的节点)
- 只有一个子节点的节点被删除,就用它的唯一的子节点来代替。(单子节点即只有前驱节点,或者只有后继节点的节点)
- 有两个子节点的,需要找到替代节点(双子节点,既有前驱节点,也有后继节点的节点)
删除叶子节点7
删除单子节点8,则可以使用8唯一的子节点代替它的位置
删除双子节点12,则既可以用12的前驱节点11代替它,
也可使使用12的后继节点13代替它
需要注意的是:
原始红黑树的删除操作的算法中:使用的是前驱节点代替删除节点。(即这个网站的红黑树删除实现:Red/Black Tree Visualization (usfca.edu))
而TreeMap的作者Doug Lea使用的是后继节点代替删除节点。
BST有一种极端情况,就是全左倾树,或者全右倾树。
这种情况下的BST就违背了初衷。BST的初衷是为了实现快速查找。
全左倾,全右倾二叉查找树,其实就是链表。此时查询操作的时间复杂度为O(N)或者使用单次二分查找优化为O(N/2)
所以为了防止这两种情况的出现,就提出了二叉平衡树,即AVL树。
AVL树的特点:
AVL树是一个高度自平衡的树,即AVL树的根节点的左右子树的高度差不超过绝对值1。且左右子树本身也是二叉平衡树。
另外AVL树具备BST树的全部特性。
AVL树查询的时间复杂度为O(logN),即每次查询都是二分查找。
如上图就是一个AVL树,它的左右子树高度差为1,没有超过1。且左右子树本身也是AVL树。
当AVL树中插入新节点时,就可能出现左右子树高度差超过1的情况,此时AVL树就会进行旋转操作,来改变树的结构以保证平衡性,即将左右子树高度差降为1.
其中旋转操作分为左旋和右旋。
第一张图中,当根节点右子树在插入一个节点,就会导致AVL树不平衡。所以需要将右子树左旋。
左旋就是:
将节点的右分支往左拉,右子节点变成父节点,并把升级后的多余的左子节点送给降级节点,作为其右子节点。
第一张图中,当根节点的左子树再插入一个节点,就会导致AVL树不平衡。所以需要将左子树右旋。
右旋就是:
将节点的左分支往右拉,左子节点变成父节点,并把升级后的多余的右子节点送给降级节点,作为其左子节点。
此时10节点的左子树高度3,右子树高度1,高度差绝对值大于1,即:左子树重,右子树轻,所以需要将10节点右旋。
即将10节点补充给右子树,将左子树根节点提升为整颗树的根节点,这样就能做到平衡左右子树了。
左左失衡情况,将根节点右旋,即可恢复平衡。
此时10节点左子树高度3,右子树高度1,高度差绝对值大于1,即:左子树重,右子树轻。但是此时如果直接将10节点右旋,则会发现还是不符合平衡要求。
所以需要先将左右失衡情况,先变为左左失衡
即:将左右失衡中的”左“进行左旋,例如上上图左右失衡中的8左旋
此时发现,已经变为了左左失衡,则直接将10节点右旋即可
着色帮助理解
左右失衡情况,需要将根节点的左孩子节点左旋转换为左左失衡,再将根节点右旋,即可恢复平衡。
此时10节点的左子树高度1,右子树高度3,高度差绝对值大于1,且右子树重,左子树轻,所以需要将10节点左旋。
即将10节点补充给左子树,将右子树根节点提升为整颗树的根节点,这样就能做到平衡左右子树了。
右右失衡情况,直接将根节点左旋,即可恢复平衡。
右左失衡情况,先将根节点的右孩子节点右旋,使得结构变为右右失衡,再将根节点左旋,即可恢复平衡。
AVL树的高度自平衡要求既是AVL树的最大优点,也是最大缺点。
因为高度自平衡带来高效率的查询的同时,保持高平衡所需要代价也是很高的。
下面演示一下将1到10添加到AVL树中的过程。并且对比BST树。
全右倾二叉搜索树 的高度为10
二叉平衡树的 高度4,但是自旋了6次
234树是四阶的平衡树(Balance Tree),它属于多路查找树,它的结构有如下限制【四阶的意思是一个父节点最多可以有四个子节点】
- 所有叶子节点都拥有相同的深度
- 节点只能是2-节点,3-节点,4-节点之一
2-节点:包含一个元素的节点,有两个子节点
3-节点:包含两个元素的节点,有三个子节点
4-节点:包含三个元素的节点,有四个子节点
所有节点至少包含一个元素
- 元素始终保持排序顺序,整体上保持二叉查找树的特性,节点的元素大于它的左子树所有节点的元素,小于它的右子树所有节点的元素。当节点有多个元素时,每个元素必须大于它左边和它左子树中的元素。
上图就是一个234树
其中:[3][4][5][6][8]节点都只有一个元素,且有两个子节点(没有子节点,默认有Nil叶子节点)。所以它们是2-节点。
[1,2] [7,9]节点有两个元素,且有三个子节点(没有子节点,则默认有Nil叶子节点)。所以它们是3-节点。
[10,11,12]节点有三个元素,有四个子节点(没有子节点,则默认有Nil叶子节点),所以它是4-节点。
1单独形成一个2-节点
1和2组成3-节点
1,2,3组成4-节点
由于2-3-4树的节点最多只能包含3个元素,所以4-节点已经是最大元素节点,无法再合并4进来
所以4-节点需要裂变:
即将[123]中间元素2提升为父节点,1和3成为2的左右孩子,且3与加入的4组成新的3-节点
同理,不再赘述
6添加进来,使得[345]裂变,4被提升为父节点
发现2和4有了共同的孩子3,由于树结构中一个孩子节点只有一个父亲节点,所以将2和4组成新的3-节点
同理,不再赘述
同理,不再赘述
同理,不再赘述
同理,不再赘述
红黑树起源于2-3-4树,一颗红黑树对应一颗确定的2-3-4树,一颗2-3-4树对应多个红黑树。
2-3-4树中可以有2-节点,3-节点,4-节点,这三种节点内部有多个元素,而多个元素之间也有联系,这种联系可以对应到红黑树的常见结构。
在讨论对应关系前,我们需要知道2-3-4树和红黑树节点的一些知识:
- 2-3-4树中2-节点,3-节点,4-节点,虽然各个类型的节点中可以包含多个元素,但是它们本身就只是一个节点,内部的多个元素,会有引用互相关联。
- 红黑树的节点只能是红色或者黑色,且任意相连的两个节点的颜色不都是红色,即红黑树不存在红红相连,只存在红黑相连,黑黑相连。
- 红黑树新加入的节点都当成红色节点
- 红黑树的根节点只能黑色节点
上图中
①就是一个2-节点
[24]就是一个3-节点
[567]就是一个4-节点
2-节点
2-3-4树的2-节点,就对应红黑树的黑色单节点
3-节点
2-3-4树的3-节点可以对于两种红黑树结构,但是这两种结构都必须保持“上黑下红”的着色要求。
上面两种红黑树结构:
红色在右边的叫做 “右倾”
红色在左边的叫做 “左倾”
4-节点
2-3-4树的4-节点只能对应一种红黑树结构,这种结构也必须保证“上黑下红”的着色要求
裂变状态
需要注意的是2-3-4树中4-节点再加入新元素的话,会导致4-节点裂变。
而这个裂变过程对应到红黑树结构来看,就是红黑树结构中节点的变色操作。
红黑树规定新加入的节点都默认是红色,所以原4-节点结构“上黑下红”需要反转变色,才能将红色节点加入进来。
红黑树的平衡要求是各简单路径保持黑色平衡。即任意节点到叶子节点的黑色节点个数相同。
红黑树是一种节点带颜色属性的二叉查找树。
红黑树的每个节点都有存储为表示节点的颜色,只能是红或者黑。
红黑树的平衡是指任意节点到叶子节点的不同简单路径中所拥有的黑色节点个数相同。
即红黑树的平衡是一种黑色平衡。
1.红黑树的每个节点只能是红色或者黑色。(非黑即红)
2.根节点必须是黑色。(黑根)
3.每个叶子节点是黑色的。(叶子节点是Nil)(黑叶)
4.如果某个节点是红色,则它的子节点必须是黑色,不能出现两个红色节点相连的情况。(红红互斥)
5.对于每个节点,从该节点到其后代的叶子节点的简单路径上,均包含相同数目的黑色节点。(黑色平衡)
2-3-4树的2-节点,3-节点,4-节点都能转换成只包含红色,黑色节点的红黑树结构,
且对应红黑树结构要么只有一个黑色节点,要么上黑下红。这就保证了红黑树的根总是黑色的。
2-3-4树的三种节点对应的红黑树结构没有红红相连的情况。
2-3-4树是一种满树,所以它每个叶子节点的深度都相同。
而红黑树的黑色平衡正是源于2-3-4树这一特性。将上图中红黑树恢复成2-3-4树。
可以发现变成2-3-4树后,它的每个节点都只有一个黑色元素。而每条简单路径上的黑色元素的个数就是2-3-4树叶子的深度。
红黑树的红色节点变为黑色节点,黑色节点变为红色节点
下面是红黑树数据结构的代码实现
package treemap;
public class RBTree {
/**
* 红黑树的节点颜色只有两种,可以用boolean来表示
*/
private static final boolean BLACK = true;
private static final boolean RED = false;
/**
* 红黑树的根节点必须单独拎出来
* 因为红黑树的查询和插入删除都是从根节点开始
*/
private Node root;
/**
* 红黑树节点具有双向性,即可以向上找到自己的父节点,向下找到自己的子节点
* 而红黑树作为满二叉树,每个节点都有两个子节点,分为左子节点和右子节点
* 红黑树的节点有颜色,且只能是红色和黑色
* 红黑树节点可以存储数据,这里模仿TreeMap底层,支持存储key-value
* @param key
* @param value
*/
static class Node{
K key;
V value;
Node parent;
Node left;
Node right;
boolean color;
public Node() {
}
public Node(K key, V value, Node parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
}
}
以某节点为旋转点,其右子节点变为旋转点的父节点,右子节点的左子节点变为旋转点的右子节点,旋转点的左子节点不变。
下面是左旋逻辑代码
/**
* 左旋
* 可以分解为如下动作:
* 1.旋转点的右子节点变为其父节点(即右子节点代替了旋转点的位置)
* 2.旋转点的右子节点的左子节点补偿给旋转节点作为其右子节点
* @param rotate 旋转点
*/
public void leftRotate(Node rotate){
// 如果旋转点不存在,则无法左旋
if(rotate==null)
return;
Node p = rotate.parent;//旋转点的父节点//可能存在
Node r = rotate.right;//旋转点的右子节点//一定存在
Node rl = r.left;//旋转点的右子节点的左子节点//可能存在
/**
* 下面代码建立p和r之间的联系
*/
if(p.left == rotate){//旋转点是其父节点的左子节点
p.left = r;
} else if(p.right == rotate){//旋转点是其父节点的右子节点
p.right = r;
} else {//旋转点没有父节点,即旋转点是根节点
root = r;
}
r.parent = p;
/**
* 下面代码建立r和rotate之间的联系
*/
r.left = rotate;
rotate.parent = r;
/**
* 下面代码建立rotate和rl之间的联系
*/
rotate.right = rl;
if(rl != null){
rl.parent = rotate;
}
}
以某节点为旋转点,其左子节点变为旋转点的父节点,左子节点的右子节点变为旋转点的左子节点,旋转点的右子节点不变。
/**
* 右旋
* 可以分解为如下动作:
* 1.旋转点的左子节点变为其父节点(即左子节点代替了旋转点的位置)
* 2.旋转点的左子节点的右子节点补偿给旋转点,做旋转点的左子节点
* @param rotate 旋转点
*/
public void rightRotate(Node rotate){
//如果旋转点不存在,则无法右旋
if(rotate==null)
return;
//备份几个需要变动双向引用的节点
Node p = rotate.parent;//旋转点的父节点//可能不存在
Node l = rotate.left;//旋转点的左子节点//一定存在
Node lr = l.right;//旋转点的左子节点的右子节点
/**
* 建立p和l之间的联系
*/
if(p.left == rotate){
p.left = l;
} else if(p.right == rotate){
p.right = l;
} else {
root = l;
}
l.parent = p;
/**
* 建立l和rotate之间的联系
*/
l.right = rotate;
rotate.parent = l;
/**
* 建立rotate和lr之间的联系
*/
rotate.left = lr;
if (lr!=null){
lr.parent = rotate;
}
}
插入逻辑可以分为两部分:
- 找到插入位置,并插入(可能是新增节点,也可能是覆盖老value)
- 如果插入操作新增了节点,则需要自平衡(左旋,右旋,变色)
/**
* 红黑树插入节点
* 如果是空树的话,则插入节点作为根节点
* 如果是非空树,则从根节点开始比较,
* 如果比较结果小于0,则说明插入节点在比较节点的左子树中,继续比较
* 如果比较结果大于0,则说明插入节点在比较节点的右子树中,继续比较
* 如果比较结果等于0,则说明插入节点对应的key已存在,只需要替换value即可
* 比较直至节点的左子树或右子树为null
* @param key
* @param value
* @return 覆盖的value
*/
public V put(K key,V value){
Node node = root;
/**
* 空树
*/
if (node == null){
// 插入节点作为根节点
root = new Node(key,value,null);
return null;
}
/**
* 非空树
*/
int cmp;
Node parent;
//从根节点开始比较
do{
parent = node;
// key的类型是K extends Comparable,即key的类型必须实现Compareable接口,且自然排序的元素是K
cmp = key.compareTo(node.key);
if (cmp<0){
// 如果插入的key小于当前比较节点的key,则继续和当前节点的左子节点比较
node = node.left;
} else if (cmp>0){
// 如果插入的key大于当前比较节点的key,则继续和当前节点的右子节点比较
node = node.right;
} else {
// 如果插入的key和当前比较节点的key相同,则覆盖对应value
V oldValue = node.value;
node.value = value;
return oldValue;
}
}while(node!=null);
// 如果上面逻辑比较到叶子节点还没有发现相同key的节点,则新增节点,新增节点的父节点是当前比较节点
Node newNode = new Node(key,value,parent);
// 建立当前比较节点和新增节点的联系
if (cmp>0){
parent.right = newNode;
} else {
parent.left = newNode;
}
fixAfterInsertion(newNode);//对于新增节点的插入操作,需要自平衡处理
return null;
}
红黑树新增节点默认是红色
首先需要分析一下,红黑树将插入的新增节点定位什么颜色比较好?
如果定为黑色,则对应分支的黑色节点个数会增加1,而其他分支的黑色节点个数不会增加,所以会破坏黑色平衡。
如果定为红色,则不会破坏红黑色整体的黑色平衡。
所以红黑色将新增节点颜色认定为红色。
从红黑树和234树节点的对应关系,理解红黑色插入操作引发的旋转和变色行为
下图中【234树】列和【红黑树最终状态】列是234树节点和红黑树结构的对应关系
【红黑树中间状态】列是红黑树新增节点的寻找插入位置并插入操作,即5.8.1中的操作
中间状态变为最终状态,即为5.8.2需要研究的红黑树的自平衡操作
- 如果插入的是第一个节点(即根节点),节点颜色从红色变为黑色,无需旋转。
- 如果父节点为黑色,则可以不需要变色,直接插入,无需旋转。
- 如果父节点是红色,没有叔叔节点,或者叔叔节点是黑色(此时只可能是NIL节点),则判断
1.如果新增节点是其父节点的左孩子,则继续判断其父节点是否为其爷爷节点的左孩子,即是否为左倾结构,
若是,则【新增节点的父节点变为黑色,爷爷节点变为红色】,绕其爷爷节点右旋(即右右失衡处理)
若不是,则【新增节点的爷爷节点变为红色,新增节点变为黑色】,先绕其父节点右旋,再绕其爷爷节点左旋(即右左失衡处理)
2.如果新增节点是其父节点的右孩子,则继续判断其父节点是否为其爷爷节点的右孩子,即是否为右倾结构,
若是,则【新增节点的父节点变为黑色,爷爷节点变为红色】,绕其爷爷节点左旋(即左左失衡处理)
若不是,则【新增节点的爷爷节点变为红色,新增节点变为黑色】,先绕其父节点左旋,再绕其爷爷节点右旋(即左右失衡处理)
- 如果父节点为红色,且叔叔节点也为红色(此时爷爷节点一定为黑色),则父节点和叔叔节点变为黑色,爷爷节点变为红色,爷爷节点此时需要递归(把爷爷节点当作新插入节点继续向上判断) 注意:如果爷爷节点为根节点,则插入后,还需要将爷爷节点变为黑色。
下面演示如下数值添加到红黑树的过程
16 | 12 | 20 | 14 | 15 | 22 | 21 | 18 |
private boolean colorOf(Node node) {
return node==null?BLACK:node.color;
}
private void setColor(Node node,boolean color){
if (node != null){
node.color = color;
}
}
private Node parentOf(Node node){
return node==null?null:node.parent;
}
private Node grandOf(Node node){
return node==null?null:parentOf(parentOf(node));
}
private Node leftOf(Node node){
return node==null?null:node.left;
}
private Node rightOf(Node node){
return node==null?null:node.right;
}
private void fixAfterInsertion(Node newNode) {
newNode.color = RED;
// 只有新增节点的父节点是红色时,才需要调整
while (newNode!=root && colorOf(parentOf(newNode))==RED){
// 判断新增节点是否有右叔叔,左爸爸?
if (leftOf(grandOf(newNode))==parentOf(newNode)){
Node uncleNode = rightOf(grandOf(newNode));
// 没有叔叔节点,则为234树3节点添加元素
if (colorOf(uncleNode)==BLACK){
// 非左倾结构
if (newNode!=leftOf(parentOf(newNode))){
// 转成左倾结构
rotateLeft(parentOf(newNode));
newNode = parentOf(newNode);
}
// 左倾结构
setColor(grandOf(newNode),RED);
setColor(parentOf(newNode),BLACK);
rotateRight(grandOf(newNode));
}
// 有叔叔节点,则为234树4节点添加元素
else {
setColor(parentOf(newNode),BLACK);
setColor(uncleNode,BLACK);
setColor(grandOf(newNode),RED);
newNode = grandOf(newNode);
}
}
// 判断新增节点是否有左叔叔,右爸爸?
else {
Node uncleNode = leftOf(grandOf(newNode));
// 没有叔叔节点,则为234树3节点添加元素
if (colorOf(uncleNode)==BLACK){
// 非右倾结构
if (newNode!=rightOf(parentOf(newNode))){
// 转成右倾结构
rotateRight(parentOf(newNode));
newNode = parentOf(newNode);
}
// 左倾结构
setColor(grandOf(newNode),RED);
setColor(parentOf(newNode),BLACK);
rotateLeft(grandOf(newNode));
}
// 有叔叔节点,则为234树4节点添加元素
else {
setColor(parentOf(newNode),BLACK);
setColor(uncleNode,BLACK);
setColor(grandOf(newNode),RED);
newNode = grandOf(newNode);
}
}
}
setColor(root,BLACK);
}
我们知道红黑树其实是一种特殊的BST,即自平衡的二叉排序树。
而BST删除节点后,为了保证二叉树的排序性,会使用删除节点的前驱节点或者后继节点来填充被删除节点的位置。
红黑树也有这样的特性。但是红黑树还可以优化该特性。
从上图红黑树我们可以知道,红黑树的删除的节点有三类:
- 双子节点,如4,10,16
- 单子节点,如14
- 叶子节点,如2,8,15,18
基于这三种节点,我们可以简化红黑树的保证排序性的删除动作:
- 叶子节点:如果删除的节点是叶子节点,则可以直接删除。如只考虑排序性,不考虑黑色平衡,则2,8,15,18直接删除后,不影响红黑树的排序性。
- 单子节点:如果删除的节点是单子节点,则可以用它的唯一子节点填充删除位置。如删除14后,可以用15代替它的位置,不影响红黑树的排序性。
- 双子节点:如果删除的节点是双子节点,则可以用它的前驱或后继节点来填充删除位置。如10删除后,使用8或14代替它,不影响红黑树的排序性。
上面我们讨论了红黑树删除节点的种类,即只能是叶子节点,单子节点,双子节点。
其中单子节点,双子节点都有一个“代替”的过程,即:
单子节点删除后,可以用它的唯一子节点(叶子节点)的值代替它,所以单子节点的删除可以通过“值代替”转化成叶子节点的删除。
双子节点删除后,可以用它的前驱或后继节点代替它。而双子节点的前驱和后继节点有什么特点呢?
如上图红黑树中,10的前驱是8(叶子节点),后继是14(单子节点)
4的前驱是2(叶子节点),后继是8(叶子节点)
16的前驱是14(叶子节点),后继是17(叶子节点)
所以双子节点的前驱或后继只能是叶子节点或单子节点。
而使用假设双子节点的前驱或后继是一个单子节点,那么使用单子节点代替被删除的双子节点,其实单子节点本质也被删除了,而单子节点的删除也可以被叶子节点代替。
所以双子节点删除,最终可以通过”双重值代替“转化成叶子节点的删除。
综上所述:红黑树的删除操作,最终都是对叶子节点的删除。
红黑树删除的节点要么是叶子节点,要么是单子节点,(其实都可以转化为叶子节点)
对应234树,其实就是234树的叶子节点。
红黑树是一种自平衡的二叉树,删除操作不能破坏红黑树的平衡性。
而红黑树的平衡是指黑色平衡。所以只有删除黑色节点会破坏红黑树的平衡,删除红色节点不会破坏红黑树的平衡。
而红黑树的删除操作最终都可以转化成对叶子节点的删除。
所以当删除的叶子节点是黑色时,需要对红黑树做调整。
例如上图中,删除红色叶子节点2,8,17不会破坏红黑树的平衡性,所以可以直接删除。(此处注意删除16红色节点,其实可以用它的前驱或后继节点来值代替)
但是删除14节点,就会破坏红黑树的黑色平衡。那么如果我们非要删除14黑色叶子节点,该如何调整(旋转变色)红黑树来保持黑色平衡呢?
我们知道红黑树来源于234树,将上图红黑树转成234树,可以看出红黑树中黑色叶子节点其实就是234树中的一个2节点,另外234树中[10,16]节点是个3节点,即必须有3个节点,如果删除14,则该3节点非法。
所以可以这样解决:
234树删除14节点后,从它的父节点处“借”一个元素给被删除的节点,来保证父节点的子节点个数。
但是这样就会出现新的问题,父节点借出一个元素后,自身就退化成2节点了,不能再带3个子节点了。
所以父节点还要从它的另一个子节点(该子节点和被删除节点是兄弟,且是被删除节点的相邻右兄弟)处借一个元素,来保持自身元素个数。
这样略显麻烦,为什么删除节点不能直接从它的右兄弟处“借”元素呢?
比如删除14,从它的右兄弟处借17。这样会导致234树的排序性丧失。
兄弟节点有的借
234树删除叶子2节点 “兄弟有的借” 情况分类:
父节点是2节点
兄弟节点是3节点:情况1
兄弟节点是4节点:情况2
父节点是3节点
兄弟节点是3节点:情况3
兄弟节点是4节点:情况4
父节点是4节点
兄弟节点是3节点:情况5
兄弟节点是4节点:情况6
(以下讨论删除节点是其父节点的左子节点情况,另外一种情况正好相反,不再赘述)
1.要找到删除节点的真正兄弟节点
通过情况1,2,5,6的234树和红黑树可以发现,当兄弟节点为黑色时,则该兄弟是真兄弟。
通过情况3,4的234树和红黑树可以发现,当兄弟节点为红色时,则该兄弟为假兄弟。这个假兄弟和父节点一起组成了234树的3节点(右倾),所以只需要将该3节点转成左倾结构(绕父节点左旋),即可帮助删除节点获得真正的兄弟节点。
2.兄弟节点是3节点时
2.1 兄弟3节点是左倾结构(即兄弟节点的右孩子为红色,左孩子为Nil黑色),绕父节点左旋,此时兄弟节点转到了父节点的位置上,则变色为父节点的颜色,父节点转到了删除节点的位置上,即变为黑色,兄弟节点的右孩子节点转到了原兄弟节点位置,则变为黑色。
2.2 兄弟3节点是右倾结构(即兄弟节点的右孩子为Nil黑色,左孩子为红色),绕兄弟节点右旋,将结构变为左倾,且依旧保持上黑下红。之后按照2.1处理。
3.兄弟节点时4节点时
当兄弟节点是4节点时,有两种借法,一种是借一个元素,一种是借两个元素。
可以发现借两个元素的方式,可以直接获得删除节点和其真兄弟节点。所以借两个元素的方式更好。
实现逻辑是:绕父节点左旋,兄弟节点变为父节点颜色,父节点变为黑色,兄弟节点右孩子变为黑色。和2.1逻辑一致。
兄弟节点没的借
兄弟节点没得借:
首先我们需要直到要删除的节点是黑色的,其兄弟节点一定也是黑色的。如果其兄弟不是黑色的,则其父节点的为根节点的子树不平衡。
然后要删除节点的父节点可以是红色的,也可以是黑色。且父节点可能是根节点,也可能是其他子树的子节点。
/**
* 根据key删除红黑树中对应的节点
*
* @param key 要删除节点对应的key
* @return 被删除节点的value
*/
public V remove(K key) {
// 先根据key找到要删除的节点
Node node = getNode(key);
V oldValue = null;
if (node != null) {
oldValue = node.value;
// 删除节点
deleteNode(node);
}
return oldValue;
}
/**
* 根据key找对应节点
*
* @param key 要找节点的key
* @return 要找的节点
*/
private Node getNode(K key) {
// 由于这里只有自然排序,所以key==null时无法使用compareTo方法
if (key != null) {
// 从红黑树根节点开始比较节点值大小
Node x = root;
int cmp;
while (x != null) {
cmp = key.compareTo(x.key);
if (cmp < 0) {
x = x.left;
} else if (cmp > 0) {
x = x.right;
} else {
// 如果找到key相同的节点,则返回该节点
return x;
}
}
//走到此步,则说明没有找到key相同的节点,则返回null
}
return null;
}
/**
* 删除节点
*
* @param node 删除的节点
*/
private void deleteNode(Node node) {
if (node == null) {
return;
}
// 删除节点是双子节点,则使用它的前驱或后继节点来代替它,这里模仿TreeMap,使用后继节点来代替他
if (node.left != null && node.right != null) {
Node replace = prevNode(node);
node.key = replace.key;
node.value = replace.value;
node = replace;//实际删除后继节点,且后继节点只可能是叶子节点或单子节点,不可能是双子节点
}
// 删除节点是单子节点
if ((node.left != null && node.right == null) || (node.left == null && node.right != null)) {
Node replace;
if (node.left != null) {
replace = node.left;
} else {
replace = node.right;
}
node.key = replace.key;
node.value = replace.value;
node = replace;//实际删除单子节点的唯一子节点,且唯一子节点只能是叶子节点
}
// 删除节点是叶子节点
if (node.left == null && node.right == null) {
Node p = node.parent;
if (p == null) {//如果叶子节点没有父节点,则说明该节点就是根节点,删除根节点后,不需要任何调整,因为已经是空树了
root = null;
size--;
return;
}
if (colorOf(node) == BLACK) {
fixAfterDeletion(node);
}
//解除叶子节点和其父节点的双向联系,方便叶子节点被垃圾回收
if (p.left == node) {
p.left = null;
} else {
p.right = null;
}
node.parent = null;
}
}
/**
* 删除节点后,调整红黑树
* 如果被删除的节点是红色的,则直接删除,无需调整
* 如果被删除的节点是黑色,
* 被删除节点是其父节点的左孩子
* 判断其兄弟节点是否为红色
* 若为红色,则为假兄弟节点,需要将假兄弟节点变黑,父节点变红,绕父节点左旋,而后得到真兄弟节点
* 若为黑色,则为真兄弟节点
* 判断其兄弟节点有几个孩子
* 若没有孩子
* 则兄弟节点没的借,兄弟节点自损变红
* 判断其父节点是否为红色,
* 若为红色,则变黑,结束调整
* 若为黑色,则继续将父节点当成被删除节点,从头开始循环,直到父节点为root或者父节点为黑色,结束循环
* 若有孩子
* 则判断兄弟节点右孩子是否为黑色
* 若为黑色,则将其兄弟节点变红,其兄弟右孩子变黑,绕兄弟节点右旋,重新获得兄弟节点
* 若为红色,则无需调整
* 其兄弟节点变为父节点颜色
* 其父节点变为黑色
* 其兄弟右孩子变为黑色
* 绕其父节点左旋
*
*
* @param node 将要被节点
*/
private void fixAfterDeletion(Node node) {
while(node!=root&&colorOf(node)==BLACK){
if (node==leftOf(parentOf(node))){
Node brother = rightOf(parentOf(node));
if (colorOf(brother)==RED){
setColor(brother,BLACK);
setColor(parentOf(node),RED);
rotateLeft(parentOf(node));
brother = rightOf(parentOf(node));
}
if (colorOf(leftOf(brother))==BLACK&&colorOf(rightOf(brother))==BLACK){
setColor(brother,RED);
node = parentOf(node);
} else {
if (colorOf(rightOf(brother))==BLACK){
setColor(brother,RED);
setColor(leftOf(brother),BLACK);
rotateRight(brother);
brother = rightOf(parentOf(node));
}
setColor(brother,colorOf(parentOf(node)));
setColor(parentOf(node),BLACK);
setColor(rightOf(brother),BLACK);
rotateLeft(parentOf(node));
node = root;
}
} else {
Node brother = leftOf(parentOf(node));
if (colorOf(brother)==RED){
setColor(brother,BLACK);
setColor(parentOf(node),RED);
rotateRight(parentOf(node));
brother = leftOf(parentOf(node));
}
if (colorOf(rightOf(brother))==BLACK&&colorOf(leftOf(brother))==BLACK){
setColor(brother,RED);
node = parentOf(node);
} else {
if (colorOf(leftOf(brother))==BLACK){
setColor(brother,RED);
setColor(rightOf(brother),BLACK);
rotateLeft(brother);
brother = leftOf(parentOf(node));
}
setColor(brother,colorOf(parentOf(node)));
setColor(parentOf(node),BLACK);
setColor(leftOf(brother),BLACK);
rotateRight(parentOf(node));
node = root;
}
}
}
setColor(node,BLACK);
}