TreeMap分析(下)

通过上篇文章,大家已经能够清楚的了解到treeMap插入结点的过程,那么本篇文章就来分析下TreeMap删除一个结点时,内部数据结构发生了怎样的变化。

TreeMap删除某个结点的源码分析

 1     /**
 2      * 删除节点,并平衡红黑树的操作
 3      *
 4      * @Param Entry<K,V> p  要删除的节点Entry
 5      */
 6     private void deleteEntry(Entry<K,V> p) {
 7         modCount++;
 8         size--;  //节点总数-1
 9
10         if (p.left != null && p.right != null) {  //当前要删除的节点左右子节点都不为空
11             Entry<K,V> s = successor(p);  //找到一个待删除节点的继承者节点s
12             //将指向s节点,后续所有对p的节点判断其实都是对s节点判断
13             p.key = s.key;
14             p.value = s.value;
15             p = s;
16         }
17
18         //替代节点选择为当前被删除节点的左子节点(优先)或右子节点
19         Entry<K,V> replacement = (p.left != null ? p.left : p.right);
20
21         if (replacement != null) {  //替代节点(被删除节点的子节点)不为空
22
23             replacement.parent = p.parent;  //将替代节点的父节点指向被删除节点的父节点
24             if (p.parent == null)  //如果被删除节点的父节点为null (即被删除的节点就是树的根节点,且根节点下面还有其他节点)
25                 root = replacement;  //将根节点设置为替换节点
26             else if (p == p.parent.left)  //如果原先被删除节点是左子树 插入
27                 p.parent.left  = replacement;  //则将替换节点也保持左子树插入(将替换节点与被删除节点的父节点左子节点建立引用)
28             else   //如果原先被删除节点是右子树 插入
29                 p.parent.right = replacement;  //则将替换节点也保持右子树插入(将替换节点与被删除节点的父节点右子节点建立引用)
30
31             //将被删除节点的左子节点、右子节点、父节点引用全部置为null
32             p.left = p.right = p.parent = null;
33
34             //删除后要执行后续的保证红黑树规则的操作
35             if (p.color == BLACK)  //如果被删除节点是黑色的
36                 fixAfterDeletion(replacement);  //调用删除后修正红黑树规则的方法
37         } else if (p.parent == null) { //被删除节点就是根节点,且整个树中就只有一个根节点的情况
38             root = null;  //将根节点置为null(此时整个树中就没有节点了)
39         } else {  //被删除节点没有子节点可替代的情况 (被删除节点是叶子节点)
40             if (p.color == BLACK)   //如果被删除节点是黑色的
41                 fixAfterDeletion(p);  //调用删除后修正红黑树规则的方法
42
43             if (p.parent != null) {  //如果被删除节点的父节点不为null
44                 if (p == p.parent.left) //如果原先被删除节点是左子树 插入
45                     p.parent.left = null;  //删除节点后将被删除节点的父节点的左子节点置为null
46                 else if (p == p.parent.right)  //如果原先被删除节点是右子树 插入
47                     p.parent.right = null;  //删除节点后将被删除节点的父节点的右子节点置为null
48                 p.parent = null;  //将被删除节点的父节点引用置为null(即将被删除节点从树中移除)
49             }
50         }
51     }

源码逻辑很简单,主要就是分为删除结点有子结点和无子结点,而无子结点又分为删除的是根结点或叶子结点这三种情况

然后如果被删除结点是黑色的,那么要注意下可能继承者和被删除结点的父结点都是红色的情况,此时需要做平衡红黑树的操作。

这里需要注意的方法有两个: 第11行的 successor() 方法  以及  第36行的  fixAfterDeletion() 方法。

分别贴一下这两个方法的源码:

 1     /**
 2      * 返回被删除节点的继承者节点
 3      */
 4     static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
 5         if (t == null)  //如果被删除节点为空,则直接返回null
 6             return null;
 7         else if (t.right != null) {  //如果被删除节点的右子节点不为空
 8             Entry<K,V> p = t.right;  //将被删除节点的右子节点记录下来
 9             while (p.left != null)  //从该节点开始循环向下查找左子节点,直至找到叶子节点后返回该叶子节点
10                 p = p.left;
11             return p;
12         } else {  //如果被删除节点的右子节点为空
13             Entry<K,V> p = t.parent;  //将被删除节点的父节点用p变量记录
14             Entry<K,V> ch = t;   //被删除节点用ch变量记录
15             while (p != null && ch == p.right) {//从被删除节点开始循环向上查找父节点,直到父节点为空或者父节点没有右子节点,返回该父节点
16                 ch = p;
17                 p = p.parent;
18             }
19             return p;
20         }
21     }

 1    /**
 2      * 树删除一个节点后,将其根据红黑树的规则进行修正
 3      * @param x  当前删除的节点
 4      */
 5     private void fixAfterDeletion(Entry<K,V> x) {
 6        //循环遍历,x刚开始为被删除的节点
 7         while (x != root && colorOf(x) == BLACK) {  //如果当前遍历到的节点不是根节点且为黑色
 8             if (x == leftOf(parentOf(x))) {  //如果当前遍历到的节点是其父节点的左子节点
 9                 Entry<K,V> sib = rightOf(parentOf(x));  //将当前遍历到的节点的父节点的右子节点用sib变量保存(即和当前节点平级的另一个节点)
10
11                 if (colorOf(sib) == RED) {  //如果sib引用的节点是红色的
12                     setColor(sib, BLACK);  //将sib引用的节点设置为黑色
13                     setColor(parentOf(x), RED);  //将当前遍历到节点的父节点设置为红色
14                     rotateLeft(parentOf(x));  //对当前遍历到节点的父节点进行一次左旋操作
15                     sib = rightOf(parentOf(x)); //sib引用的节点变更为旋转后被删除节点的父节点的右子节点
16                 }
17
18                 if (colorOf(leftOf(sib))  == BLACK &&
19                     colorOf(rightOf(sib)) == BLACK) { //如果sib引用节点的左、右子节点都是黑色的
20                     setColor(sib, RED);  //将sib引用的节点设置为红色
21                     x = parentOf(x);  //下一次遍历的节点变更为当前遍历到节点的父节点
22                 } else {  //如果sib引用节点的左、右子节点不全是黑色的
23                     if (colorOf(rightOf(sib)) == BLACK) {  //如果sib引用节点的右子节点是黑色的
24                         setColor(leftOf(sib), BLACK);  //将sib引用节点的左子节点设置为黑色
25                         setColor(sib, RED);   //sib引用节点设置为红色
26                         rotateRight(sib);  //对sib节点进行一次右旋操作
27                         sib = rightOf(parentOf(x)); //sib引用的节点变更为当前遍历到的节点的父节点的右子节点
28                     }
29                     setColor(sib, colorOf(parentOf(x)));  //将sib引用节点的颜色设置为 当前遍历到节点的父节点 一样的颜色
30                     setColor(parentOf(x), BLACK);  //将当前遍历到节点的父节点设置为黑色
31                     setColor(rightOf(sib), BLACK);  //将sib引用节点的右子节点设置为黑色
32                     rotateLeft(parentOf(x));  //对当前遍历到的节点的父节点进行一次左旋操作
33                     x = root;  //下一次遍历的节点变更为根节点
34                 }
35             } else { // 当前遍历到的节点是其父节点的右子节点,和上述情况相似,不再赘述
36                 Entry<K,V> sib = leftOf(parentOf(x));
37
38                 if (colorOf(sib) == RED) {
39                     setColor(sib, BLACK);
40                     setColor(parentOf(x), RED);
41                     rotateRight(parentOf(x));
42                     sib = leftOf(parentOf(x));
43                 }
44
45                 if (colorOf(rightOf(sib)) == BLACK &&
46                     colorOf(leftOf(sib)) == BLACK) {
47                     setColor(sib, RED);
48                     x = parentOf(x);
49                 } else {
50                     if (colorOf(leftOf(sib)) == BLACK) {
51                         setColor(rightOf(sib), BLACK);
52                         setColor(sib, RED);
53                         rotateLeft(sib);
54                         sib = leftOf(parentOf(x));
55                     }
56                     setColor(sib, colorOf(parentOf(x)));
57                     setColor(parentOf(x), BLACK);
58                     setColor(leftOf(sib), BLACK);
59                     rotateRight(parentOf(x));
60                     x = root;
61                 }
62             }
63         }
64
65         setColor(x, BLACK);
66     }

可能光看这两个方法的源码不太好理解,下面就以删除几个结点的实例,一步一步跟踪下源码看看数据结构到底发生了哪些变化。

先回顾下上一篇文章插入结点完成后的红黑树状态图:

好,那么首先来删除根结点30。因为30结点既有左子结点,又有右子结点,所以在deleteEntry()方法中需要调用successor() 方法寻找继承者结点。

查看successor() 方法源码,可以发现会走第7行的 else if 逻辑,然后从70结点开始循环查找左子结点,直到找到叶子结点50为止,所以最终的继承者结点就是50,下面的三行代码会将根结点30的key-value变为继承者的key-value,所以现在根结点为50,黑色。

然后下一行会取得replacement变量,因为现在p=s了,即这里的p.left实际判断的是叶子结点50是否有左子结点,很显然叶子结点50既没有左子结点也没有右子结点,所以最后replacement = null。

而叶子结点50是有父结点的,所以不会走 else if (p.parent == null) 分支,而是走下面的else{}分支。叶子结点50目前是红色的,所以不需要做平衡红黑树的操作。

再往下,很显然叶子结点50是有父结点的,所以会走 if (p.parent != null) 的分支,而这个分支里面又是一个if-else分支,这里判断的是当前结点是左子树插入还是右子树插入,叶子结点50很明显是左子树插入,所以会执行  p.parent.left = null 这行代码,即把叶子结点50给删除掉了。

全部过程执行完后,红黑树当前的状态图如下所示:

经过一次删除操作后,童鞋们是否对这两个方法有点印象了呢?不过这次删除30结点我们找到的继承者结点50是红色的,所以没有走修正红黑树平衡的操作。那么下面就再删除一个结点,让它进行平衡红黑树的操作。

还是以最原始的红黑树状态图为基准,这次我们来删除结点70。70也需要寻找继承者结点,因为70有右子结点,所以会走  else if (t.right != null) 这个分支逻辑,然后指针移动到85结点,而又因为85没有左子结点,所以不符合下面的 while (p.left != null)循环条件,方法结束,最后选择的继承者结点就是85结点。

然后和上面一样,会走三行代码,将70结点变更为85结点,然后将指针指向85结点。很显然85结点既没有左子结点也没有右子结点,所以replacement = null。然后85结点是有父结点的,所以和上面一样走的是else{}分支。

注意,这里和上面的不同之处在于,目前指针是指向85这个节点的,而这个节点是黑色的,所以在else{}分支里的  if (p.color == BLACK) 为true,则会调用 fixAfterDeletion(p) 这个方法进行红黑树的修正操作。

在调用修正操作之前,红黑树的状态是这样的:

好,接下来我们看一下具体的修正操作干了哪些事情。

首先此时指针指向的黑色85结点是满足  while (x != root && colorOf(x) == BLACK) 这个循环条件的,然后这个节点是右子树插入的,所以会走else{}分支。

然后下一步的sib节点取得是父结点的左子结点,即60结点,而60结点是黑色,所以不会走  if (colorOf(sib) == RED) 这个分支逻辑。

然而60结点的左右子结点都不是黑色的,所以会走下面的else{}分支,并且不会走 else分支中的  if (colorOf(leftOf(sib)) == BLACK) 这个判断,只会从第 56 行的代码开始走

setColor(sib, colorOf(parentOf(x)))  是将60结点的颜色设置为何黑色85结点的父结点(即红色85结点)的颜色,所以此时60结点变为红色。

setColor(parentOf(x), BLACK)  这一行又把红色的85结点设置为了黑色

setColor(leftOf(sib), BLACK)  这一行又将50结点设置成了黑色,此时除了60结点为红色,其余的50、85、85结点都是黑色

rotateRight(parentOf(x))  这一行以之前为红色的85结点为中心,做一次右旋操作。忘记旋转操作的童鞋看看前面的帖子,就不难理解啦。

然后将指针移动回根结点,并设置根结点为黑色,此时整个修正红黑树操作就结束了。此时红黑树状态图如下所示:

注意:不要忘记再回到之前的deleteEntry方法中去,只是走完了修正红黑树的方法,但整个删除操作还没全部结束呢!

回到原方法的  if (p.parent != null)分支,此时85结点是有父结点的,所以会走这个分支逻辑,然后85又是右子树插入的,所以会走 else if (p == p.parent.right) 这个分支逻辑

然后这行  p.parent.right = null  会将叶子结点85删除,此时才是真正走完了整个删除节点的操作。此时童鞋们小本本上的红黑树应该是这样的哟:

好了,大家是不是觉得其实红黑树的操作并不是很困难呢?只要肯踏踏实实、一步一步的去仔细分析每一步红黑树是如何变化的就能够轻松得到结果。

最后还有一个删除叶子结点的没有讲,比较简单就让童鞋们自己去研究实践吧!

在这里放上两张状态图,以便童鞋们进行验证(图中的状态是以删除叶子结点85为例所得到的结果):

1.进行红黑树修正操作之后的状态图:

2.删除叶子结点85操作全部结束后的状态图:

注:以上图片出处均为 博客园——五月的仓颉,非本人原创!

到这里,关于红黑树的所有知识全部都讲解完毕,并且集合的基础知识也暂告一段落了。

有的童鞋会问,怎么只讲了List、Map接口下的一些常用工具类,Set接口的怎么不讲了呢?

大家可以去看下HashSet、TreeSet的源码相关实现,其实就是HashSet、TreeSet去掉了Value而已,绝大多数实现都是一样的,所以我这里就不再去细说了

注意我在这个集合分类中所讲的集合类都是非线程安全的,像CopyOnWriteArrayList、ConcurrentHashMap、BlockingQueue等线程安全的集合工具类我会放在多线程的分类主题中去讲解。

OK,接下来我会更新一些多线程相关的知识,下期见!

原文地址:https://www.cnblogs.com/smartchen/p/9104106.html

时间: 2024-10-19 05:32:49

TreeMap分析(下)的相关文章

TreeMap分析(上)

因为TreeMap的相关知识较多,故TreeMap的分析将会分成三篇文章来写,望大家谅解. 本篇文章先给大家介绍一下红黑树基本概念,并分析一下在红黑树中查找某个结点的相关源码实现. TreeMap是啥 从TreeMap的类名上就能知道它的底层存储结构其实是红黑树.首先简单介绍一下红黑树的相关知识,以便理解后续内容. 什么是红黑树?先放一张红黑树的示意图看看: 注:图片出处为 博客园 -- 五月的仓颉 简单解释一下树的相关术语的含义: 1.根结点(即0026结点):整个树结构图中最上面的一个结点,

分析下自己写的SQL Server同步工具的性能和缺陷

分析下自己写的SQL Server同步工具的性能和缺陷 1. C#同步SQL Server数据库Schema 2. C#同步SQL Server数据库中的数据--数据库同步工具[同步新数据] 通过测试我写的同步程序,得出结论: 1.程序第一次调用SQLBulkCopy会耗时较长 2.同步程序放在目标机器在耗时方面相对少些 测试数据: declare @varI varchar(200) set @varI=0 while(@varI<100000) begin set @[email prote

TIOBE11月份编程语言排行榜:C非常接近Java,分析下中美的就业情况

TIOBE公布11月份编程语言排行榜:C非常接近Java Swift挤进前10,分析下中美的就业情况. 我们先看看他们官方对数据的解读 本月TIOBE指数前20位出现了一些有趣的变动.首先,C语言现在非常接近Java.差异只有0.2%.也许C会在年底前再次成为第一.看到谁排在前十也很令人兴奋.这种情况几乎每个月都在变化.两个月前是SQL,上个月是Objective-C,但是这个月Swift接管了.与排名第11位的Ruby的差距几乎为0.4%,这可能意味着至少在未来几个月里,Swift仍将保持前1

产品经理之竞品分析下

竞品分析下 成果目的与竞品选取 收集高相关竞品动态报 关注行业新趋势/新技术 引发创新思考与讨论 2.分类分级的重要性 1.1分类让动态更好用 基于用户体验分层分类:战略层.范围层.框架层.结构层.表现层基于用户体验旅程分类:导购.加购支付.物流.客服.售后基于变更类型分类:功能迭代.体验优化.投融资.运营活动 1.2分级让动态更可用: 重要需关注: 直接对手重大调整 国家政策/行业规定出台 互联网巨头的本行业动作 核心优势被挑战 3.以小见大,洞察趋势 4.成果形式与特点 1. 以专项调研报告

Linux内核中断和异常分析(下)

这节,我们继续上,中(以前的日志有)篇目进行分析,结合一个真实的驱动案例来描述linux内核中驱动的中断机制,首先我们先了解一下linux内核中提供的中断接口. 这个接口我们需要包含一个头文件:#include <linux/interrupt.h> 在中断接口中,最重要的是以下的接口函数: 1.这个是请求中断函数 int request_irq(unsigned int irq, irq_handler_t handler, unsigned long irqflags, const cha

百度登录加密协议分析(下)

上一篇百度登录加密协议分析(上)主要讲解了codestring,gid,token,rsakey等参数的产生.好了,废话不多说,咱们进入今天的主题,咱们接着上一篇的内容往下讲解,最后还剩三个字段 callback,password,ppui_logintime. 第三部分: 分析第一次post已经产生,第二次post内容发生变化的字段 callback password ppui_logintime 通过之前的分析,可以了解到callback 可能没啥用,所以放到后面再分析. 一般来说passw

原理剖析-Netty之服务端启动工作原理分析(下)

一.大致介绍 1.由于篇幅过长难以发布,所以本章节接着上一节来的,上一章节为[原理剖析(第 010 篇)Netty之服务端启动工作原理分析(上)]: 2.那么本章节就继续分析Netty的服务端启动,分析Netty的源码版本为:netty-netty-4.1.22.Final: 二.三.四章节请看上一章节 四.源码分析Netty服务端启动 上一章节,我们主要分析了一下线程管理组对象是如何被实例化的,并且还了解到了每个线程管理组都有一个子线程数组来处理任务: 那么接下来我们就直接从4.6开始分析了:

Chapter4 复杂度分析(下):浅析最好,最坏,平均,均摊时间复杂度

四个复杂度分析: 1:最好情况时间复杂度(best case time complexity) 2:最坏情况时间复杂度(worst case time complexity) 3:平均情况时间复杂度(average case time complexity) 4:均摊时间复杂度(amortized time complexity) for (; i < n; ++i) { if (array[i] == x) { pos = i; break; } } 分析:1:最好情况时间复杂度:O(1) 2

分析下实施ERP最佳的实践方案

无论您属于那种类型的业务,实施ERP解决方案是必须认真对待的关键项目.从关心成本和证明投资回报率到谁将管理您公司业务的实际考虑,ERP实施需要项目团队的投入和企业领导的支持,以便确保能够取得成功. ERP部署是一项重要任务,通过适当的规划和执行,实施ERP系统应该是一个顺利的过程,它可以快速提高整个业务的效率. 这里有一些关键的ERP实施最佳做法,在选择和部署解决方案时应考虑它们. 为企业选择最佳的ERP实施 第一,考虑到ERP包含多种功能,存在多种类型的ERP实施也就不足为奇了.您可以选择构建