欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。


删除元素

删除元素本身比较简单,就是采用二叉树的删除规则。

(1)如果删除的位置有两个叶子节点,则从其右子树中取最小的元素放到删除的位置,然后把删除位置移到替代元素的位置,进入下一步。

(2)如果删除的位置只有一个叶子节点(有可能是经过第一步转换后的删除位置),则把那个叶子节点作为替代元素,放到删除的位置,然后把这个叶子节点删除。

(3)如果删除的位置没有叶子节点,则直接把这个删除位置的元素删除即可。

(4)针对红黑树,如果删除位置是黑色节点,还需要做再平衡。

(5)如果有替代元素,则以替代元素作为当前节点进入再平衡。

(6)如果没有替代元素,则以删除的位置的元素作为当前节点进入再平衡,平衡之后再删除这个节点。

public V remove(Object key) {
// 获取节点
Entry<K,V> p = getEntry(key);
if (p == null)
return null; V oldValue = p.value;
// 删除节点
deleteEntry(p);
// 返回删除的value
return oldValue;
} private void deleteEntry(Entry<K,V> p) {
// 修改次数加1
modCount++;
// 元素个数减1
size--; if (p.left != null && p.right != null) {
// 如果当前节点既有左子节点,又有右子节点
// 取其右子树中最小的节点
Entry<K,V> s = successor(p);
// 用右子树中最小节点的值替换当前节点的值
p.key = s.key;
p.value = s.value;
// 把右子树中最小节点设为当前节点
p = s;
// 这种情况实际上并没有删除p节点,而是把p节点的值改了,实际删除的是p的后继节点
} // 如果原来的当前节点(p)有2个子节点,则当前节点已经变成原来p的右子树中的最小节点了,也就是说其没有左子节点了
// 到这一步,p肯定只有一个子节点了
// 如果当前节点有子节点,则用子节点替换当前节点
Entry<K,V> replacement = (p.left != null ? p.left : p.right); if (replacement != null) {
// 把替换节点直接放到当前节点的位置上(相当于删除了p,并把替换节点移动过来了)
replacement.parent = p.parent;
if (p.parent == null)
root = replacement;
else if (p == p.parent.left)
p.parent.left = replacement;
else
p.parent.right = replacement; // 将p的各项属性都设为空
p.left = p.right = p.parent = null; // 如果p是黑节点,则需要再平衡
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) {
// 如果当前节点就是根节点,则直接将根节点设为空即可
root = null;
} else {
// 如果当前节点没有子节点且其为黑节点,则把自己当作虚拟的替换节点进行再平衡
if (p.color == BLACK)
fixAfterDeletion(p); // 平衡完成后删除当前节点(与父节点断绝关系)
if (p.parent != null) {
if (p == p.parent.left)
p.parent.left = null;
else if (p == p.parent.right)
p.parent.right = null;
p.parent = null;
}
}
}

删除再平衡

经过上面的处理,真正删除的肯定是黑色节点才会进入到再平衡阶段。

因为删除的是黑色节点,导致整颗树不平衡了,所以这里我们假设把删除的黑色赋予当前节点,这样当前节点除了它自已的颜色还多了一个黑色,那么:

(1)如果当前节点是根节点,则直接涂黑即可,不需要再平衡;

(2)如果当前节点是红+黑节点,则直接涂黑即可,不需要平衡;

(3)如果当前节点是黑+黑节点,则我们只要通过旋转把这个多出来的黑色不断的向上传递到一个红色节点即可,这又可能会出现以下四种情况:

(假设当前节点为父节点的左子节点)

情况 策略
1)x是黑+黑节点,x的兄弟是红节点 (1)将兄弟节点设为黑色;
(2)将父节点设为红色;
(3)以父节点为支点进行左旋;
(4)重新设置x的兄弟节点,进入下一步;
2)x是黑+黑节点,x的兄弟是黑节点,且兄弟节点的两个子节点都是黑色 (1)将兄弟节点设置为红色;
(2)将x的父节点作为新的当前节点,进入下一次循环;
3)x是黑+黑节点,x的兄弟是黑节点,且兄弟节点的右子节点为黑色,左子节点为红色 (1)将兄弟节点的左子节点设为黑色;
(2)将兄弟节点设为红色;
(3)以兄弟节点为支点进行右旋;
(4)重新设置x的兄弟节点,进入下一步;
3)x是黑+黑节点,x的兄弟是黑节点,且兄弟节点的右子节点为红色,左子节点任意颜色 (1)将兄弟节点的颜色设为父节点的颜色;
(2)将父节点设为黑色;
(3)将兄弟节点的右子节点设为黑色;
(4)以父节点为支点进行左旋;
(5)将root作为新的当前节点(退出循环);

(假设当前节点为父节点的右子节点,正好反过来)

情况 策略
1)x是黑+黑节点,x的兄弟是红节点 (1)将兄弟节点设为黑色;
(2)将父节点设为红色;
(3)以父节点为支点进行右旋;
(4)重新设置x的兄弟节点,进入下一步;
2)x是黑+黑节点,x的兄弟是黑节点,且兄弟节点的两个子节点都是黑色 (1)将兄弟节点设置为红色;
(2)将x的父节点作为新的当前节点,进入下一次循环;
3)x是黑+黑节点,x的兄弟是黑节点,且兄弟节点的左子节点为黑色,右子节点为红色 (1)将兄弟节点的右子节点设为黑色;
(2)将兄弟节点设为红色;
(3)以兄弟节点为支点进行左旋;
(4)重新设置x的兄弟节点,进入下一步;
3)x是黑+黑节点,x的兄弟是黑节点,且兄弟节点的左子节点为红色,右子节点任意颜色 (1)将兄弟节点的颜色设为父节点的颜色;
(2)将父节点设为黑色;
(3)将兄弟节点的左子节点设为黑色;
(4)以父节点为支点进行右旋;
(5)将root作为新的当前节点(退出循环);

让我们来看看TreeMap中的实现:

/**
* 删除再平衡
*(1)每个节点或者是黑色,或者是红色。
*(2)根节点是黑色。
*(3)每个叶子节点(NIL)是黑色。(注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!)
*(4)如果一个节点是红色的,则它的子节点必须是黑色的。
*(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
*/
private void fixAfterDeletion(Entry<K,V> x) {
// 只有当前节点不是根节点且当前节点是黑色时才进入循环
while (x != root && colorOf(x) == BLACK) {
if (x == leftOf(parentOf(x))) {
// 如果当前节点是其父节点的左子节点
// sib是当前节点的兄弟节点
Entry<K,V> sib = rightOf(parentOf(x)); // 情况1)如果兄弟节点是红色
if (colorOf(sib) == RED) {
// (1)将兄弟节点设为黑色
setColor(sib, BLACK);
// (2)将父节点设为红色
setColor(parentOf(x), RED);
// (3)以父节点为支点进行左旋
rotateLeft(parentOf(x));
// (4)重新设置x的兄弟节点,进入下一步
sib = rightOf(parentOf(x));
} if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
// 情况2)如果兄弟节点的两个子节点都是黑色
// (1)将兄弟节点设置为红色
setColor(sib, RED);
// (2)将x的父节点作为新的当前节点,进入下一次循环
x = parentOf(x);
} else {
if (colorOf(rightOf(sib)) == BLACK) {
// 情况3)如果兄弟节点的右子节点为黑色
// (1)将兄弟节点的左子节点设为黑色
setColor(leftOf(sib), BLACK);
// (2)将兄弟节点设为红色
setColor(sib, RED);
// (3)以兄弟节点为支点进行右旋
rotateRight(sib);
// (4)重新设置x的兄弟节点
sib = rightOf(parentOf(x));
}
// 情况4)
// (1)将兄弟节点的颜色设为父节点的颜色
setColor(sib, colorOf(parentOf(x)));
// (2)将父节点设为黑色
setColor(parentOf(x), BLACK);
// (3)将兄弟节点的右子节点设为黑色
setColor(rightOf(sib), BLACK);
// (4)以父节点为支点进行左旋
rotateLeft(parentOf(x));
// (5)将root作为新的当前节点(退出循环)
x = root;
}
} else { // symmetric
// 如果当前节点是其父节点的右子节点
// sib是当前节点的兄弟节点
Entry<K,V> sib = leftOf(parentOf(x)); // 情况1)如果兄弟节点是红色
if (colorOf(sib) == RED) {
// (1)将兄弟节点设为黑色
setColor(sib, BLACK);
// (2)将父节点设为红色
setColor(parentOf(x), RED);
// (3)以父节点为支点进行右旋
rotateRight(parentOf(x));
// (4)重新设置x的兄弟节点
sib = leftOf(parentOf(x));
} if (colorOf(rightOf(sib)) == BLACK &&
colorOf(leftOf(sib)) == BLACK) {
// 情况2)如果兄弟节点的两个子节点都是黑色
// (1)将兄弟节点设置为红色
setColor(sib, RED);
// (2)将x的父节点作为新的当前节点,进入下一次循环
x = parentOf(x);
} else {
if (colorOf(leftOf(sib)) == BLACK) {
// 情况3)如果兄弟节点的左子节点为黑色
// (1)将兄弟节点的右子节点设为黑色
setColor(rightOf(sib), BLACK);
// (2)将兄弟节点设为红色
setColor(sib, RED);
// (3)以兄弟节点为支点进行左旋
rotateLeft(sib);
// (4)重新设置x的兄弟节点
sib = leftOf(parentOf(x));
}
// 情况4)
// (1)将兄弟节点的颜色设为父节点的颜色
setColor(sib, colorOf(parentOf(x)));
// (2)将父节点设为黑色
setColor(parentOf(x), BLACK);
// (3)将兄弟节点的左子节点设为黑色
setColor(leftOf(sib), BLACK);
// (4)以父节点为支点进行右旋
rotateRight(parentOf(x));
// (5)将root作为新的当前节点(退出循环)
x = root;
}
}
} // 退出条件为多出来的黑色向上传递到了根节点或者红节点
// 则将x设为黑色即可满足红黑树规则
setColor(x, BLACK);
}

删除元素举例

假设我们有下面这样一颗红黑树。

我们删除6号元素,则从右子树中找到了最小元素7,7又没有子节点了,所以把7作为当前节点进行再平衡。

我们看到7是黑节点,且其兄弟为黑节点,且其兄弟的两个子节点都是红色,满足情况4),平衡之后如下图所示。

我们再删除7号元素,则从右子树中找到了最小元素8,8有子节点且为黑色,所以8的子节点9是替代节点,以9为当前节点进行再平衡。

我们发现9是红节点,则直接把它涂成黑色即满足了红黑树的特性,不需要再过多的平衡了。

这次我们来个狠的,把根节点删除,从右子树中找到了最小的元素5,5没有子节点,所以把5作为当前节点进行再平衡。

我们看到5是黑节点,且其兄弟为红色,符合情况1),平衡之后如下图所示,然后进入情况2)。

对情况2)进行再平衡后如下图所示。

然后进入下一次循环,发现不符合循环条件了,直接把x涂为黑色即可,退出这个方法之后会把旧x删除掉(见deleteEntry()方法),最后的结果就是下面这样。


未完待续,下一节我们一起探讨红黑树遍历元素的操作。

现在公众号文章没办法留言了,如果有什么疑问或者建议请直接在公众号给我留言。


欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

死磕 java集合之TreeMap源码分析(三)- 内含红黑树分析全过程的更多相关文章

  1. 死磕 java集合之TreeMap源码分析(四)-内含彩蛋

    欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 二叉树的遍历 我们知道二叉查找树的遍历有前序遍历.中序遍历.后序遍历. (1)前序遍历,先遍历 ...

  2. 死磕 java集合之TreeMap源码分析(一)- 内含红黑树分析全过程

    欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 简介 TreeMap使用红黑树存储元素,可以保证元素按key值的大小进行遍历. 继承体系 Tr ...

  3. 死磕 java集合之TreeMap源码分析(二)- 内含红黑树分析全过程

    欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 插入元素 插入元素,如果元素在树中存在,则替换value:如果元素不存在,则插入到对应的位置, ...

  4. 死磕 java集合之DelayQueue源码分析

    问题 (1)DelayQueue是阻塞队列吗? (2)DelayQueue的实现方式? (3)DelayQueue主要用于什么场景? 简介 DelayQueue是java并发包下的延时阻塞队列,常用于 ...

  5. 死磕 java集合之PriorityBlockingQueue源码分析

    问题 (1)PriorityBlockingQueue的实现方式? (2)PriorityBlockingQueue是否需要扩容? (3)PriorityBlockingQueue是怎么控制并发安全的 ...

  6. 死磕 java集合之PriorityQueue源码分析

    问题 (1)什么是优先级队列? (2)怎么实现一个优先级队列? (3)PriorityQueue是线程安全的吗? (4)PriorityQueue就有序的吗? 简介 优先级队列,是0个或多个元素的集合 ...

  7. 死磕 java集合之CopyOnWriteArraySet源码分析——内含巧妙设计

    问题 (1)CopyOnWriteArraySet是用Map实现的吗? (2)CopyOnWriteArraySet是有序的吗? (3)CopyOnWriteArraySet是并发安全的吗? (4)C ...

  8. 死磕 java集合之LinkedHashSet源码分析

    问题 (1)LinkedHashSet的底层使用什么存储元素? (2)LinkedHashSet与HashSet有什么不同? (3)LinkedHashSet是有序的吗? (4)LinkedHashS ...

  9. 死磕 java集合之ConcurrentHashMap源码分析(三)

    本章接着上两章,链接直达: 死磕 java集合之ConcurrentHashMap源码分析(一) 死磕 java集合之ConcurrentHashMap源码分析(二) 删除元素 删除元素跟添加元素一样 ...

随机推荐

  1. Scrapy:Python实现scrapy框架爬虫两个网址下载网页内容信息——Jason niu

    import scrapy class DmozSpider(scrapy.Spider): name ="dmoz" allowed_domains = ["dmoz. ...

  2. 2018-2019-2 网络对抗技术 20162329 Exp3 免杀原理与实践

    目录 免杀原理与实践 一.基础问题回答 1.杀软是如何检测出恶意代码的? 2.免杀是做什么? 3.免杀的基本方法有哪些? 二.实验内容 1. 正确使用msf编码器 2. msfvenom生成如jar之 ...

  3. Ubuntu 插入鼠标自动禁用触控板

    http://ubuntuhandbook.org/index.php/2016/11/install-touchpad-indicator-in-ubuntu-16-04/ arrive@HP443 ...

  4. Java 跨平台原理

    Java的跨平台基于编译器和虚拟机.其中,CPU处理器和操作系统的整体称为平台.编译器把源文件编译成与平台无关的基于Unicode的字节码class文件,虚拟机把该文件解释成与平台有关的机器码指令,可 ...

  5. IntelliJ IDEA 2016.2 配置Tomcat 运行Web项目

    1.可能会出现的问题 Run-->Edit Configurations 中点击"+"号没有tomcat server... 解决办法:File--->Setting- ...

  6. Error: Unable to access xxx.jar

    在cmd中运行java -jar xxx.jar出现如下错误: Error: Unable to access xxx.jar 解决方法: 使用绝对路径:java -jar D:\Program Fi ...

  7. 文本在div中始终垂直居中

    如果是文本字数固定,在div中垂直居中,相信大家都会 这边分享个不固定文本在div中垂直居中的方法 html代码 <div class="box"> <div c ...

  8. zstd --压缩工具

    Zstandard (也被称为zstd )是一款免费的开源,快速实时数据压缩程序,具有更好的压缩比 (约为 10:1). 安装 yum group install "Development ...

  9. pyhton中常用的基础模块

    目前在看Google的DetectionAPI源码,自己的Python功底不是很好,看到了哪些模块就随手记一下. 1.abc模块,参考https://www.cnblogs.com/wancy86/p ...

  10. Java课程之团队开发(NABCD需求分析)

    N.需求 1.学生基本的录入课程功能 2.学生对于空教室使用的需求(自习或者是活动占用) 3.学生对于具体课程的查询需求 A.做法 1.制作出基于安卓的课程查询,录入以及教室查询应用软件 B.好处 1 ...