通过上篇文章,大家已经能够理解红黑树的基础数据结构,那么这篇文章就来分析下,在红黑树中插入一个结点后,内部数据结构发生了哪些变化。

TreeMap插入某个结点的源码分析

 1     /**
2 * 插入节点,并平衡红黑树的操作
3 * 如果原先map中已经有该key对应的键值对,则替换原先该key对应的value为新的value
4 * 如果原先map中没有该key对应的键值对,则在map中新插入一个该key和value对应的键值对
5 *
6 * @param key 键
7 * @param value 新值
8 *
9 * @return 返回原先的值 或者 null
10 *
11 * @throws ClassCastException 如果用户传入了一个不可和其他key比较的key(违反泛型约定),则抛出该异常
12 * @throws NullPointerException key传入了null,则报此异常
13 */
14 public V put(K key, V value) {
15 Entry<K,V> t = root; //获取根节点
16 if (t == null) { //如果根节点为空,则当前的树还没有初始化
17 compare(key, key); // 检查key的类型以及是否为null
18
19 root = new Entry<>(key, value, null); //根据key和value创建一个黑色新节点作为树的根节点
20 size = 1; //结点总数+1
21 modCount++;
22 return null; //直接返回
23 }
24 //根节点不为空
25 int cmp; //结点的比较结果 >0 OR <0 OR =0
26 Entry<K,V> parent;
27 // 区分是使用key的自然排序进行结点比较 还是 使用用户传入的比较器进行结点的比较
28 Comparator<? super K> cpr = comparator;
29 if (cpr != null) { //如果用户传入了自定义比较器
30 do {
31 parent = t; //每次进行比较的节点,一开始t变量保存的是树的根节点
32 cmp = cpr.compare(key, t.key); //使用自定义比较器的compare方法,对传入的结点和当前遍历到结点的key进行比较
33 if (cmp < 0) //如果传入节点的key比当前遍历到节点的key小
34 t = t.left; //把下次进行比较的节点设置为当前遍历到的节点的左子节点
35 else if (cmp > 0) //如果传入节点的key比当前遍历到节点的key大
36 t = t.right; //把下次进行比较的节点设置为当前遍历到的节点的右子节点
37 else //如果传入节点的key和当前遍历到节点的key一样大
38 return t.setValue(value); //说明这是一个替换原有key对应的value的操作,替换完成后直接返回(不需要再进行下面的插入操作)
39 } while (t != null); //直到遍历到某一个叶子结点才结束,最终t变量保存的是当前遍历到的叶子节点
40 }
41 else { //没有自定义比较器,使用key的自然排序进行比较
42 if (key == null)
43 throw new NullPointerException(); //如果传入key为null,则报异常
44 Comparable<? super K> k = (Comparable<? super K>) key;
45 do {
46 parent = t; //每次进行比较的节点,一开始t变量保存的是树的根节点
47 cmp = k.compareTo(t.key); //使用Comparable接口的compareTo方法,对传入的结点和当前遍历到结点的key进行比较
48 if (cmp < 0) //以下步骤和上面if的步骤完全相同,不再赘述
49 t = t.left;
50 else if (cmp > 0)
51 t = t.right;
52 else
53 return t.setValue(value);
54 } while (t != null);
55 }
56 //执行到这里说明遍历了整个树后没有发现存在与传入key相同的键值对,则需要将传入的键值对插入到树中
57 Entry<K,V> e = new Entry<>(key, value, parent); //根据当前传入的key和value新建一个黑色节点
58 if (cmp < 0) //如果之前最后一次比较的结果是传入的key比当时叶子结点的key小
59 parent.left = e; //那么就将当时叶子结点的左子结点设置为当前传入的结点,当前传入的结点变为新的叶子节点
60 else //如果之前最后一次比较的结果是传入的key比当时叶子结点的key大
61 parent.right = e; //那么就将当时叶子结点的右子结点设置为当前传入的结点,当前传入的结点变为新的叶子节点
62
63 fixAfterInsertion(e); //红黑树的核心方法:在插入后通过左旋、右旋、变色将当前树变成符合红黑树规定的树
64
65 size++; //节点总数+1
66 modCount++;
67 return null; //返回null
68 }

逻辑还是比较简单的,只要区分两点即可:

1.比较规则是使用key的自然排序进行比较还是使用用户自定义的排序规则进行比较。

2.原来的红黑树中是否已经存在这次插入结点key对应的结点?如果是,则此次操作其实是更新旧值的操作;否则就是新增一个结点的操作。

这里有个需要着重理解的方法,在第 63 行的  fixAfterInsertion(e)  方法。这个方法是红黑树新增一个结点的核心方法。

要理解这个方法,首先需要先了解一些预备知识。

关于树的旋转

之前已经说过,因为红黑树自身的特性,其在插入和删除结点后还要通过旋转、着色的操作来使整个树的定义符合红黑树的定义。

着色很好理解,无非就是更改结点为红色或黑色,那么关于树的旋转操作,大家是否还记得上大学时老师在黑板上画了好多树的左旋、右旋图呢?

我在这儿就稍微介绍下树的旋转相关的知识吧。

以树的右旋转为例,先看一张图:

图中描述的是针对a结点的一次右旋操作。

然后是右旋操作的源码:

 1     /** 树的右旋操作 */
2 private void rotateRight(Entry<K,V> p) {
3 if (p != null) { //如果待右旋节点p不为空
4 Entry<K,V> l = p.left; //获取待右旋节点p的左子节点l
5 p.left = l.right; //将p的左子节点指向l的右子节点
6 //l的右子节点不为空
7 if (l.right != null) l.right.parent = p; //l的右子节点的父节点指向p(p与l的右子节点建立父子节点关系)
8 l.parent = p.parent; //l的父节点指向p的父节点(相当于l取代p的位置)
9 if (p.parent == null) //p父节点是否为空?
10 root = l; //p就是根节点,则将当前根节点变更为l
11 else if (p.parent.right == p) //p是否为其父节点的右子节点?
12 p.parent.right = l; //p的父节点的右子节点指向l
13 else p.parent.left = l; //p的父节点的左子节点指向l
14 l.right = p; //l的右子节点指向p
15 p.parent = l; //p的父节点指向l
16 }
17 }

源码中的P变量就是上图的a结点,l变量就是上图的b结点,大家可以对照源码和图,在纸上将图中的节点一步一步按照源码的操作画出来,这样比较容易理解。

左旋的操作和右旋是相似的,就不再啰嗦了,大家可以把上图中的右旋后的树作为基础,左旋后得到的就是原先的树。左旋源码如下:

 1     /** 树的左旋操作 */
2 private void rotateLeft(Entry<K,V> p) {
3 if (p != null) {
4 Entry<K,V> r = p.right;
5 p.right = r.left;
6 if (r.left != null)
7 r.left.parent = p;
8 r.parent = p.parent;
9 if (p.parent == null)
10 root = r;
11 else if (p.parent.left == p)
12 p.parent.left = r;
13 else
14 p.parent.right = r;
15 r.left = p;
16 p.parent = r;
17 }
18 }

了解树的旋转操作以后,还需要了解一个小知识。因为红黑树本身是一棵二叉树,二叉树的每个结点最多有两个子结点(左子结点和右子结点)

那么在插入一个结点时,可能插入的结点是左子结点,也可能插入的是右子结点,如下图:

因为结点3和结点8都是在根结点10的左子结点下插入的,所以这种插入方式称之为左子树插入。

上图中左边插入的结点3是结点6的左子结点,这种情况我们称为左子树外侧插入;

而右边的结点8是结点6的右子结点,这种情况我们称为左子树内侧插入。

当然,右子树插入,右子树外侧、内侧插入,聪明如大家,应该不难自行推导出来吧~~

OK,到这里关于红黑树插入结点的前置知识已经介绍完毕,接下来就要分析插入结点的核心源码了 : )

红黑树插入结点核心方法源码分析

 1     /**  平衡树的相关操作  */
2
3 /** 返回当前节点的颜色,如果节点为空则默认返回黑色 */
4 private static <K,V> boolean colorOf(Entry<K,V> p) {
5 return (p == null ? BLACK : p.color);
6 }
7
8 /** 返回当前节点的父节点,没有父节点则返回空 */
9 private static <K,V> Entry<K,V> parentOf(Entry<K,V> p) {
10 return (p == null ? null: p.parent);
11 }
12
13 /** 给当前结点设置颜色 */
14 private static <K,V> void setColor(Entry<K,V> p, boolean c) {
15 if (p != null)
16 p.color = c;
17 }
18
19 /** 返回当前节点的左子节点,没有左子节点则返回空 */
20 private static <K,V> Entry<K,V> leftOf(Entry<K,V> p) {
21 return (p == null) ? null: p.left;
22 }
23
24 /** 返回当前节点的右子节点,没有右子节点则返回空 */
25 private static <K,V> Entry<K,V> rightOf(Entry<K,V> p) {
26 return (p == null) ? null: p.right;
27 }
28
29
30 /**
31 * 树插入一个新结点后,将其根据红黑树的规则进行修正
32 * @param x 当前插入树的节点
33 */
34 private void fixAfterInsertion(Entry<K,V> x) {
35 //默认将当前插入树的节点颜色设置为红色,为什么???
36 //因为红黑树有一个特性: "从根节点到所有叶子节点上的黑色节点数量是相同的"
37 //如果当前插入的节点是黑色的,那么必然会违反这个特性,所以必须将插入节点的颜色先设置为红色
38 x.color = RED;
39 //第一次遍历时,x变量保存的是当前新插入的节点
40 //为什么要用while循环?
41 //因为在旋转的过程中可能还会出现父子节点均为红色的情况,所以要不断往上遍历直至整个树都符合红黑树的规则
42 while (x != null && x != root && x.parent.color == RED) { //如果当前节点不为空且不是根节点,并且当前节点的父节点颜色为红色
43 if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { //如果当前节点的父节点等于当前节点父节点的父节点的左子节点(即当前节点为左子树插入)
44
45 Entry<K,V> y = rightOf(parentOf(parentOf(x))); //获取当前节点的叔父节点(和当前插入节点的父节点同辈的另外那个节点)
46 if (colorOf(y) == RED) { //如果叔父节点的颜色为红色
47 //以下4步用来保证不会连续出现两个红色节点
48 setColor(parentOf(x), BLACK); //将当前节点的父节点设置为黑色
49 setColor(y, BLACK); //将当前节点的叔父节点设置为黑色
50 setColor(parentOf(parentOf(x)), RED); //将当前节点的祖父节点设置为红色
51 x = parentOf(parentOf(x)); //当前遍历节点变更为当前节点的祖父节点
52 } else { //如果叔父节点的颜色为黑色,或没有叔父节点
53 if (x == rightOf(parentOf(x))) { //如果当前节点为左子树内侧插入
54 x = parentOf(x); //将x变更为当前节点的父节点
55 rotateLeft(x); //对当前节点的父节点进行一次左旋操作(旋转完毕后x对应的就是最左边的叶子节点)
56 }
57 //如果当前节点为左子树外侧插入
58 setColor(parentOf(x), BLACK); //将当前节点的父节点设置为黑色
59 setColor(parentOf(parentOf(x)), RED); //将当前节点的祖父节点设置为红色
60 rotateRight(parentOf(parentOf(x))); //对当前节点的祖父节点进行一次右旋
61 }
62 } else { //当前节点为右子树插入
63 Entry<K,V> y = leftOf(parentOf(parentOf(x))); //以下步骤与上面基本相似,只是旋转方向相反,不再赘述
64 if (colorOf(y) == RED) {
65 setColor(parentOf(x), BLACK);
66 setColor(y, BLACK);
67 setColor(parentOf(parentOf(x)), RED);
68 x = parentOf(parentOf(x));
69 } else {
70 if (x == leftOf(parentOf(x))) {
71 x = parentOf(x);
72 rotateRight(x);
73 }
74 setColor(parentOf(x), BLACK);
75 setColor(parentOf(parentOf(x)), RED);
76 rotateLeft(parentOf(parentOf(x)));
77 }
78 }
79 }
80 root.color = BLACK;//注意在旋转的过程中可能将根节点变更为红色的,但红黑树的特性要求根节点必须为黑色,所以无论如何最后总要执行这行代码,将根节点设置为黑色
81 }

核心方法是   fixAfterInsertion(Entry<K,V> x),简单描述下该方法的逻辑:

从插入的结点开始,往上遍历父结点直到根结点,对每次遍历到的结点判断是左子树插入还是右子树插入

然后再判断当前遍历到结点的叔父结点的颜色为红色还是黑色,不同颜色有不同的操作来使红黑树符合其自身性质,最终遍历到根结点时,就平衡了一棵红黑树。

光看源码比较难理解,下面我举几个插入结点的例子,结合上方的源码,大家可以在纸上将每次插入结点的操作一步一步和上方的源码进行验证,这样可以更好的理解这一过程:

我们假设一开始只有一个结点10,其为根结点,且因红黑树性质决定了根结点必须为黑色。

然后,我们插入第一个结点85。因为85比10大,所以85必定为10的右子结点。

由源码第38行可知,默认插入结点的颜色为红色,而由第42行的while循环可知,当前插入的红色结点85它的父结点为10,10为根结点必定为黑色,所以不符合while的循环条件,直接退出方法。

此时红黑树的结构如下图所示:

然后我们插入第二个结点 15.因为15比10大,但又比85小,所以应该是右子树内侧插入。

因为默认插入结点为红色,所以此时红黑树结构如下图所示:

而因为红黑树自身特性要求不能有连续两个结点都是红色,所以要进行平衡树的相关操作

因为根结点10没有左子结点,所以不满足第64行的if判断,转而走第69行的else部分的代码。

而又因为15结点是85结点的左子结点,所以会走第70行的if语句,将新增结点的指针从15结点移动到85结点并做右旋操作,然后走下一行代码将15结点变更黑色

此时红黑树的结构如下图所示:

然后执行75、76两行代码,将10结点变为红色,再以10结点为基准做左旋操作,全部完成后此时红黑树如下图所示:

大家在纸上将每一步的操作好好思考后全部画下来,自然能够很清晰的看到每一步发生的变化。

接下来插入70结点,70比15大又比85小,所以依然是右子树内侧插入。

因为85结点是红色的,所以此时走第64行的if代码,通过if中的4行代码将10结点、85结点变为黑色,将15结点变为红色。

别忘了最后还会执行一句  root.color = BLACK 会将根结点15变为黑色。全部操作完成后,红黑树结构如下图:

接下来再插入结点就不再像上面一步步分析了,我会把每次插入结点关键的中间状态以及完成插入的状态图贴上,大家可以自行参考 : )

接下来插入20结点,中间状态图如下所示:

1. 

2.

接下来插入60结点:

接下来插入30结点:

1.

2.

最后插入50结点:

1.

2.

3.

注:本篇文章所有图片出处均为  博客园——五月的仓颉

到这里treeMap的插入结点操作已经全部讲解完成,下一篇文章会分析treeMap删除结点的过程。

转载于:https://www.cnblogs.com/smartchen/p/9077989.html

TreeMap分析(中)的更多相关文章

  1. Loadrunner结果分析中连接图没有数据的设置

    场景进行中,或者之后进行结果分析中,连接图表没有数据,取消选择标记选项.

  2. 关于Jaccard相似度在竞品分析中的一点思考

    上个月对一个小项目的效果进行改进,时间紧,只有不到一周的时间,所以思考了一下就用了最简单的方法来做,跟大家分享一下(项目场景用的类似的场景) 项目场景:分析一个产品的竞品,譬如app的竞品.网站的竞品 ...

  3. Jaccard相似度在竞品分析中的应用

    上个月对一个小项目的效果进行改进,时间紧,只有不到一周的时间,所以思考了一下就用了最简单的方法来做,效果针对上一版提升了5%左右,跟大家分享一下(项目场景用的类似的场景) 项目场景:分析一个产品的竞品 ...

  4. 小技巧——病毒分析中关闭ASLR

    原文来自:https://bbs.ichunqiu.com/thread-41359-1-1.html 病毒分析中关闭ASLR 分析病毒的时候,尽可能用自己比较熟悉的平台,这样可以大大地节省时间,像我 ...

  5. 解读人:陈秋实,SP2: Rapid and Automatable Contaminant Removal from Peptide Samples for Proteomic Analyses(标准操作流程2:如何在蛋白质组学分析中快速和自动的去除肽段样品中的污染物)

    发表时间:2019年4月 IF:3.950 单位: 威斯康星医学院生物化学系 威斯康星医学院生物医学质谱研究中心 物种:人(人体肾脏细胞和蛋白) 技术:肽段清理 一. 概述:(用精炼的语言描述文章的整 ...

  6. 数据包分析中Drop和iDrop的区别

    数据包分析中Drop和iDrop的区别   在数据包分析中,Drop表示因为过滤丢弃的包.为了区分发送和接受环节的过滤丢弃,把Drop又分为iDrop和Drop.其中,iDrop表示接受环节丢弃的包, ...

  7. CVPR2020:点云分析中三维图形卷积网络中可变形核的学习

    CVPR2020:点云分析中三维图形卷积网络中可变形核的学习 Convolution in the Cloud: Learning Deformable Kernels in 3D Graph Con ...

  8. idapython在样本分析中的使用-字符解密

    最近接手的一个样本,样本中使用了大量的xor加密,由于本身样本不全,无法运行(好吧我最稀饭的动态调试没了,样本很有意思,以后有时间做票大的分析),这个时候就只好拜托idapython大法了(当然用id ...

  9. Dynamics AX 2012 在BI分析中建立数据仓库的必要性

    AX系统已有的BI分析架构 对于AX 的BI分析架构,相信大家都了解,可以看Reinhard之前的译文[译]Dynamics AX 2012 R2 BI系列-分析的架构 . AX 的BI分析架构的优势 ...

随机推荐

  1. LeetCode | 169. 多数元素

    给定一个大小为 n 的数组,找到其中的多数元素.多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素. 你可以假设数组是非空的,并且给定的数组总是存在多数元素. 示例 1: 输入: [3,2,3] ...

  2. 配置附加权限和LDAP

     配置附加权限和LDAP 补充:调整root的权限为rwx(读,写,执行) 步骤:采用数值形式将目录/root的权限调整为rwx------ 1)查看原来的权限 [root@svr7~]#ls -ld ...

  3. MariaDB使用数据库查询《三》

                                                                 MariaDB使用数据库查询 案例5:使用数据库查询 5.1 问题 本例要求配 ...

  4. 用curl调用https接口

    今天在windows下用curl类获取微信token一直返回false,查阅资料后,发现是https证书的锅,在curl类中加上这两条,问题瞬间解决. curl_setopt($ch, CURLOPT ...

  5. 给Jekyll静态博客添加ScrollSpy博文大纲目录

    目录 内置TOC 添加ScrollSpy博文menu Scrollnav.js 使用方法❤ 最近又双叒把博客模板换成了Jekyll,Jekyll无论上手难度和修改难度都是目前所见流行模板中最低的(以无 ...

  6. C++语言实现链式栈

    在之前写的C语言实现链式栈篇博文中,我已经给大家大概介绍了关于链式栈的意义以及相关操作,我会在下面给大家分享百度百科对链式栈的定义,以及给大家介绍利用C++实现链式栈的基本操作. 百度百科链式栈 链式 ...

  7. hive常用函数六

    cast 函数: 类型转换函数,cast(kbcount as int); case when: 条件判断,case when kbcount is not null and cast(kbcount ...

  8. java nio消息半包、粘包解决方案

    问题背景 NIO是面向缓冲区进行通信的,不是面向流的.我们都知道,既然是缓冲区,那它一定存在一个固定大小.这样一来通常会遇到两个问题: 消息粘包:当缓冲区足够大,由于网络不稳定种种原因,可能会有多条消 ...

  9. work of 1/6/2016

    part 组员                今日工作              工作耗时/h 明日计划 工作耗时/h    UI 冯晓云 UI动态布局改进和攻克疑难     6 继续下滑条等增删补减 ...

  10. 格式化启动盘win10

    我这个(U盘)磁盘被分成了两个区,不能直接格式化 第一步: 第二步: 删除完了之后,选择格式化,ok. 说明:格式化时要选择系统. 常规NTFS  缺点:老设备,比如打印机,监控机识别不了. FAT系 ...