当前位置: 首页 > news >正文

TreeMap源码分析 红黑树

今天尝试刨一下TreeMap的祖坟。

底层结构对比

先来看一下与HashMap、LinkedHashMap和TreeMap的对比,同时就当是复习一下:

  1. HashMap使用数组存储数据,并使用单向链表结构存储hash冲突数据,同一个冲突桶中数据量大的时候(默认超过8)则使用红黑树存储冲突数据。
  2. LinkedHashMap使用数组+双向链表存储数据,冲突数据存储方式同HashMap。
  3. TreeMap使用红黑树存储数据,注意是直接使用红黑树,不使用table数组。

关于排序特性

  1. HashMap无顺序,不能保持顺序。
  2. LinkedHashMap能保持写入的顺序,遍历的时候可以按照写入顺序获取数据。
  3. TreeMap是有序的Map,自动按照key值排序存储,遍历时获取到的是有序数据。

需要注意LinkedHashMap和TreeMap在顺序方面的区别,LinkedHashMap只能保持写入顺序,从“排序”的角度讲,他实际是无序的。

只有TreeMap是可以实现自动排序的。

TreeMap按照什么排序?

TreeMap底层支持两种排序方式:

  1. TreeMap对象实例化时传入comparator对象。
  2. key值对象实现Comparable接口。

如果以上两点都不能满足的话,向TreeMap对象put数据的时候会抛出运行时异常。

比如TreeMap<String,Object>,由于String实现了Comparable接口,所以是没有问题的。

但是如果自定义的对象,没有实现Comparable接口,同时在TreeMap实例化的时候没有设置comparator对象,则该TreeMap对象实际是不可用的。

TreeMap是否可以存储null?

指的是,是否可以存储key为空的数据?我们知道HashMap是可以支持唯一一个null对象的。

很多人都说不可以,但是我觉得有条件可以,但是没做测试(因为感觉则个问题其实有点扯)。

条件是实例化TreeMap对象的时候指定comparator对象,同时,该comparator对象的compare方法可以支持null。

研究TreeMap的put源码,也可以发现对以上说法的支持:

Comparator<? super K> cpr = comparator;if (cpr != null) {do {parent = t;cmp = cpr.compare(key, t.key);if (cmp < 0)t = t.left;else if (cmp > 0)t = t.right;elsereturn t.setValue(value);} while (t != null);}else {if (key == null)throw new NullPointerException();...省略若干代码

可以发现如果有comparator的话,put方法不会立即抛出异常。但是如果comparator对象的compare方法不能支持null的话,一样会抛出异常。

put方法

由于TreeMap支持自动排序,所以put方法会检查是否满足规则。

不满足排序规则,抛出异常。

否则,按照红黑树算法规则要求,创建红黑树,存储数据。

所以这里就涉及到一个重要的数据结构:红黑树。

二叉树BST & 平衡二叉树AVL

红黑树是树结构的一种,是比二叉树和平衡二叉树更加复杂的一种数据结构。所以我们先从简单的入手,了解一下二叉树。

树结构其实是实现了子排序的一种数据结构,我们说到的树结构一般指的是二叉树、也叫二叉搜索树(BST - Binary Search Tree),定义:它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。

二叉搜索树的插入、搜索操作所花的时间都和树的高度成正比。因此,如果共有n个元素,那么平均每次操作需要O(logn)的时间。

二叉搜索树的特性并不能保证他是平衡的,也就是说,极端情况下,一颗二叉搜索树从根节点开始,只有左节点、没有右节点(比如一直按照从大到小或者相反顺序插入二叉树),这种情况下,二叉树就蜕变成了链表,查询时间复杂度就会下降为O(n)。

改善二叉树这一缺点的经典数据结构是平衡二叉树AVL(Adelson-Velsky and Landis Tree,以两位发明者命名)。平衡二叉树的特性:

1.本身首先是一棵二叉搜索树。
2.带有平衡条件:每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1。

一颗平衡二叉树在新节点插入之后可能会失衡,包括以下四种场景:

左左失衡LL

插入的数据在左侧不平衡树的左侧,这种情况下需要进行右旋操作再次平衡:
在这里插入图片描述
我们需要记住一个概念:平衡二叉树失衡之后通过旋转动作使得它再次平衡的处理,只是针对失衡的子树、不需要对整个树进行操作。比如上图新的节点1加入之后,导致pivot节点7一侧的树失衡,我们需要处理的是包含pivot节点的root节点的左侧树这一部分子树,右侧节点18下即使有再多的节点,也不需要处理。

右旋操作的实质是:以pivot节点7为支点做顺时针旋转,旋转之后7变为自己原来父节点的父节点、7的左节点不变,7的右节点变为7原来的父节点的左节点。

右右失衡RR

插入的节点在右侧不平衡树的右侧,这种情况下需要左旋:
在这里插入图片描述
左旋操作的实质是:以pivot节点18为支点做逆时针旋转,旋转之后18变为自己原来父节点的父节点、18的右节点不变,18的左节点变为18原来的父节点的右节点。

左右失衡LR

插入的新节点在左侧不平衡树的右侧。先左旋再右旋
在这里插入图片描述

右左失衡LR

插入的节点在右侧不平衡树的左侧,先右旋再左旋:
在这里插入图片描述
平衡二叉树具有:每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1 这一特性,这一严格条件会导致每次插入新节点之后总会大概率破坏平衡、从而必须要通过上述的旋转操作使其再次达到平衡,旋转操作会影响数据插入的效率。

红黑树可以解决这一问题。

红黑树

红黑树是一种带颜色(节点是红色或者黑色)的平衡二叉树,具有以下特性:
性质1. 结点是红色或黑色。
性质2. 根结点是黑色。
性质3. 所有叶子都是黑色。(叶子是NIL结点)
性质4. 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
性质5. 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。

红黑树新加入节点的颜色默认为黑色,因为根据红黑树性质4,每个红色节点的两个子节点都是黑色。所以,新加入节点如果是黑色的话,可以检查其父节点如果是红色的话,可以自动满足性质4从而减少再平衡操作、提高数据加入的效率。这一点可以通过TreeMap的put方法源码得到验证:

public V put(K key, V value) {Entry<K,V> t = root;if (t == null) {compare(key, key); // type (and possibly null) checkroot = new Entry<>(key, value, null);size = 1;modCount++;return null;}int cmp;Entry<K,V> parent;// split comparator and comparable pathsComparator<? super K> cpr = comparator;if (cpr != null) {do {parent = t;cmp = cpr.compare(key, t.key);if (cmp < 0)t = t.left;else if (cmp > 0)t = t.right;elsereturn t.setValue(value);} while (t != null);}else {if (key == null)throw new NullPointerException();@SuppressWarnings("unchecked")Comparable<? super K> k = (Comparable<? super K>) key;do {parent = t;cmp = k.compareTo(t.key);if (cmp < 0)t = t.left;else if (cmp > 0)t = t.right;elsereturn t.setValue(value);} while (t != null);}Entry<K,V> e = new Entry<>(key, value, parent);if (cmp < 0)parent.left = e;elseparent.right = e;fixAfterInsertion(e);size++;modCount++;return null;}

put方法前半部分逻辑比较简单,为新节点找到合适的位置加入,之后会调用fixAfterInsertion检查是否破坏了红黑树的规则从而需要执行再平衡操作:

private void fixAfterInsertion(Entry<K,V> x) {x.color = RED;while (x != null && x != root && x.parent.color == RED) {if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {Entry<K,V> y = rightOf(parentOf(parentOf(x)));if (colorOf(y) == RED) {setColor(parentOf(x), BLACK);setColor(y, BLACK);setColor(parentOf(parentOf(x)), RED);x = parentOf(parentOf(x));} else {if (x == rightOf(parentOf(x))) {x = parentOf(x);rotateLeft(x);}setColor(parentOf(x), BLACK);setColor(parentOf(parentOf(x)), RED);rotateRight(parentOf(parentOf(x)));}} else {Entry<K,V> y = leftOf(parentOf(parentOf(x)));if (colorOf(y) == RED) {setColor(parentOf(x), BLACK);setColor(y, BLACK);setColor(parentOf(parentOf(x)), RED);x = parentOf(parentOf(x));} else {if (x == leftOf(parentOf(x))) {x = parentOf(x);rotateRight(x);}setColor(parentOf(x), BLACK);setColor(parentOf(parentOf(x)), RED);rotateLeft(parentOf(parentOf(x)));}}}root.color = BLACK;}

fixAfterInsertion方法首先判断其父节点是红色的话,则不做任何操作。

get方法

根据红黑树查找算法查找并返回数据,红黑树是平衡二叉树,查询时间复杂度为O(log(n))。

key遍历

比如调用TreeMap.keySet方法,采用遍历二叉树算法,按照从小到大的顺序返回所有key值组成的循环器。

我该使用哪一个?

需要用到Map的时候,到底该使用哪一个的问题:

  1. 我只需要一个存储数据的容器,没有具体要求的话,用HashMap。
  2. 存储数据后,有按照存储顺序获取数据的需求,采用LinkedHashMap。
  3. 希望存储数据的同时,帮助实现自动排序,采用TreeMap。

性能的问题,其实几乎不需要考虑,不过我们还是需要知道:

  1. HashMap和LinkedHashMap查询速度快,理想情况下时间复杂度几乎是O(1)。
  2. HashMap写入速度最快,LinkedHashMap写入速度与HashMap几乎相同,TreeMap写入速度最慢(理论上,实际数据量小的情况下未必慢)。
  3. 遍历速度相差无几,理论上HashMap会慢一点,因为需要遍历空桶。

并发问题尚待研究,但是我们清楚地知道,以上三种均不具备线程安全性。

好梦!

相关文章:

  • mac系统快捷键及命令安装
  • LSNet: 基于侧向抑制的神经网络
  • 预测性 SRE 与自动化修复
  • fvcom 网格文件grd制作
  • yolov11安装,训练模型,tensorrtx加速,Qt预测图像
  • mac触摸板设置右键
  • python pyecharts 数据分析及可视化(2)
  • 八股文——JAVA基础:hashCode()方法的作用与意义以及与equals方法的联动
  • 通过阿里云部署n8n工作流自动备份GitHub
  • Gartner《Everything Technical Professionals Need to KnowAbout DA Strategy》学习心得
  • RuoYi-Vue学习环境搭建
  • docker compose基本使用以及示例
  • 云端可视化耦合电磁场:麦克斯韦方程组的应用-AI云计算数值分析和代码验证
  • 学习使用Visual Studio分析.net内存转储文件的基本用法
  • MybatisPlus-03.快速入门-常用注解
  • 横向移动01
  • leetcode437-路径总和III
  • FLOPS、FLOP/s、TOPS概念
  • 手机流量监控App(GlassWire)使用指南
  • 自学嵌入式 day27 进程