死磕 java集合之TreeMap源码分析(三)- 内含红黑树分析全过程
欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。
删除元素
删除元素本身比较简单,就是采用二叉树的删除规则。
(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源码分析(三)- 内含红黑树分析全过程的更多相关文章
- 死磕 java集合之TreeMap源码分析(四)-内含彩蛋
欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 二叉树的遍历 我们知道二叉查找树的遍历有前序遍历.中序遍历.后序遍历. (1)前序遍历,先遍历 ...
- 死磕 java集合之TreeMap源码分析(一)- 内含红黑树分析全过程
欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 简介 TreeMap使用红黑树存储元素,可以保证元素按key值的大小进行遍历. 继承体系 Tr ...
- 死磕 java集合之TreeMap源码分析(二)- 内含红黑树分析全过程
欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 插入元素 插入元素,如果元素在树中存在,则替换value:如果元素不存在,则插入到对应的位置, ...
- 死磕 java集合之DelayQueue源码分析
问题 (1)DelayQueue是阻塞队列吗? (2)DelayQueue的实现方式? (3)DelayQueue主要用于什么场景? 简介 DelayQueue是java并发包下的延时阻塞队列,常用于 ...
- 死磕 java集合之PriorityBlockingQueue源码分析
问题 (1)PriorityBlockingQueue的实现方式? (2)PriorityBlockingQueue是否需要扩容? (3)PriorityBlockingQueue是怎么控制并发安全的 ...
- 死磕 java集合之PriorityQueue源码分析
问题 (1)什么是优先级队列? (2)怎么实现一个优先级队列? (3)PriorityQueue是线程安全的吗? (4)PriorityQueue就有序的吗? 简介 优先级队列,是0个或多个元素的集合 ...
- 死磕 java集合之CopyOnWriteArraySet源码分析——内含巧妙设计
问题 (1)CopyOnWriteArraySet是用Map实现的吗? (2)CopyOnWriteArraySet是有序的吗? (3)CopyOnWriteArraySet是并发安全的吗? (4)C ...
- 死磕 java集合之LinkedHashSet源码分析
问题 (1)LinkedHashSet的底层使用什么存储元素? (2)LinkedHashSet与HashSet有什么不同? (3)LinkedHashSet是有序的吗? (4)LinkedHashS ...
- 死磕 java集合之ConcurrentHashMap源码分析(三)
本章接着上两章,链接直达: 死磕 java集合之ConcurrentHashMap源码分析(一) 死磕 java集合之ConcurrentHashMap源码分析(二) 删除元素 删除元素跟添加元素一样 ...
随机推荐
- Mac下brew安装JDK的教程
---恢复内容开始--- 安装命令: brew cask install java 默认应该会下载jdk7 也可以指定下载版本brew cask install java6 注意: brew inst ...
- 腾讯云服务器、nginx部署loopback
最近在研究学习nginx,买了腾讯云服务器.在阿里上申请了域名,部署项目遇到很多问题记录一下,以备后用: 1.在腾讯服务器买好,阿里域名申请好后(也可以在腾讯上申请域名),需要添加安全组,创建不同的规 ...
- C#的排序Sort和OrderBy扩展方法
可以实现一个IComparable接口的CompareTo方法,或者是给予List的Sort扩展方法,传入委托实现,举个例子: list.Sort((a, b) => { var o = a.s ...
- 磁共振成像SENSE 并行加速重建 g-factor计算方法(待更新)
MRI SENSE 并行图像加速重建 g-factor计算方法: Matlab代码如下: function g=gfactor_noise(map,LOSS,Rx,Ry) % map -> se ...
- BZOJ5335 : [TJOI2018]智力竞赛
二分答案,转化成求最少的路径,覆盖住所有权值$\leq mid$的点. 建立二分图,若$i$的后继为$j$,则连边$i\rightarrow j$,求出最大匹配,则点数减去最大匹配数即为最少需要的路径 ...
- django -使用jinja2模板引擎 自定义的过滤器
setting.py中 TEMPLATES = [ { 'BACKEND': 'django.template.backends.jinja2.Jinja2', 'DIRS': [os.path.jo ...
- 虚拟机上的Ubuntu 文件系统成为只读模式的解决办法
虚拟机环境的Linux系统由于是虚拟化虚拟出来的主机环境,因此 经常会出现一些操作系统的问题,今天我遇到了一个Ubuntu操作系统文件系统成了只读模式,无法进行系统的操作,由于出问题的主机是我个人搭建 ...
- Unity-批量修改Prefab上的属性
问题描述:今天发现工程中有些prefab上的脚本丢失了一些引用,本以为手动拖拽上去搞定,后来查看其它prefab,也有类似的问题,于是写了一个小工具,批量修改下. 上代码: [ExecuteInEdi ...
- node简介
1. Node.js是什么? 简单的说 Node.js 就是运行在服务端的 JavaScript. Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台. Node.js ...
- Android中RadioGroup的初始化和简单的使用
一简介: RadioGroup作为一个单选按钮组,可以设置为性别选择男或则女,地址选择等等,作为一个android入门级选手,就简单的说一下RadioGroup组中RadioButton的布局和初始化 ...