返回顶部
首页 > 资讯 > 后端开发 > JAVA >一文带你全面深入了解TreeMap
  • 679
分享到

一文带你全面深入了解TreeMap

算法java数据结构 2023-09-05 11:09:23 679人浏览 八月长安
摘要

概述 TreeMap是Map家族中的一员,也是用来存放key-value键值对的。平时在工作中使用的可能并不多,它最大的特点是遍历时是有顺序的,根据key的排序规则来,那么它具体是如何使用,又是怎么实现的呢?本文基于jdk8做一个讲解。 T

概述

TreeMap是Map家族中的一员,也是用来存放key-value键值对的。平时在工作中使用的可能并不多,它最大的特点是遍历时是有顺序的,根据key的排序规则来,那么它具体是如何使用,又是怎么实现的呢?本文基于jdk8做一个讲解。

TreeMap介绍

TreeMap是一个基于key有序的key value散列表。

  • map根据其键的自然顺序排序,或者根据map创建时提供的Comparator排序
  • 不是线程安全
  • key 不可以存入null
  • 底层是基于红黑树实现的

以上是TreeMap的类结构图:

  1. 实现了NavigableMap接口,NavigableMap又实现了Map接口,提供了导航相关的方法。
  2. 继承了AbstractMap,该方法实现Map操作的骨干逻辑。
  3. 实现了Cloneable接口,标记该类支持clone方法复制
  4. 实现了Serializable接口,标记该类支持序列化

构造方法

  • TreeMap()

说明:使用键的自然排序构造一个新的空树映射。

  • TreeMap(Comparator comparator)

说明:构造一个新的空树映射,根据给定的比较器排序。

  • TreeMap(Map m)

说明:构造一个新的树映射,包含与给定映射相同的映射,按照键的自然顺序排序。

  • TreeMap(SortedMap m)

说明:构造一个新的树映射,包含相同的映射,并使用与指定排序映射相同的顺序。

关键方法

这边主要讲解下NavigableMap和SortedMap提供的一些方法,Map相关的方法大家应该都很熟悉了。

SortedMap接口:

  • Comparator comparator()

返回用于排序此映射中的键的比较器,如果此映射使用其键的自然排序,则返回null。

  • Set> entrySet()

返回此映射中包含的映射的Set视图。

返回当前映射中的第一个(最低)键。

  • K lastKey()

返回当前映射中的最后(最高)键。

NavigableMap接口:

  • Map.Entry ceilingEntry(K key)

返回与大于或等于给定键的最小键相关联的键值映射,如果没有这样的键则返回null。

  • K ceilingKey(K key)

返回大于或等于给定键的最小键,如果没有这样的键,则返回null。

  • NavigableMap descendingMap()

返回此映射中包含的映射的倒序视图。

  • Map.Entry firstEntry()

返回与该映射中最小的键关联的键值映射,如果映射为空,则返回null。

  • Map.Entry floorEntry(K key)

返回与小于或等于给定键的最大键相关联的键值映射,如果没有这样的键则返回null。

  • SortedMap headMap(K toKey)

返回该映射中键严格小于toKey的部分的视图。

  • Map.Entry higherEntry(K key)

返回与严格大于给定键的最小键关联的键值映射,如果没有这样的键,则返回null。

  • Map.Entry lastEntry()

返回与此映射中最大键关联的键值映射,如果映射为空,则返回null。

  • Map.Entry lowerEntry(K key)

返回与严格小于给定键的最大键关联的键值映射,如果没有这样的键,则返回null。

  • Map.Entry pollFirstEntry()

删除并返回与该映射中最小的键关联的键值映射,如果映射为空,则返回null。

  • Map.Entry pollLastEntry()

删除并返回与此映射中最大键关联的键值映射,如果映射为空,则返回null。

  • SortedMap subMap(K fromKey, K toKey)

返回该映射中键范围从fromKey(包含)到toKey(独占)的部分的视图。

  • SortedMap tailMap(K fromKey)

返回该映射中键大于或等于fromKey的部分的视图。

使用案例

  • 验证顺序性
@Test    public void test1() {        Map treeMap = new TreeMap<>();        treeMap.put(16, "a");        treeMap.put(1, "b");        treeMap.put(4, "c");        treeMap.put(3, "d");        treeMap.put(8, "e");        // 遍历        System.out.println("默认排序:");        treeMap.forEach((key, value) -> {            System.out.println("key: " + key + ", value: " + value);        });        // 构造方法传入比较器        Map tree2Map = new TreeMap<>((o1, o2) -> o2 - o1);        tree2Map.put(16, "a");        tree2Map.put(1, "b");        tree2Map.put(4, "c");        tree2Map.put(3, "d");        tree2Map.put(8, "e");        // 遍历        System.out.println("倒序排序:");        tree2Map.forEach((key, value) -> {            System.out.println("key: " + key + ", value: " + value);        });    }

运行结果:

  • 验证不能存储null
@Test    public void test2() {        Map treeMap = new TreeMap<>();        treeMap.put(null, "a");    }

运行结果:

  • 验证NavigableMap相关方法
@Test    public void test3() {        NavigableMap treeMap = new TreeMap<>();        treeMap.put(16, "a");        treeMap.put(1, "b");        treeMap.put(4, "c");        treeMap.put(3, "d");        treeMap.put(8, "e");        // 获取大于等于5的key        Integer ceilingKey = treeMap.ceilingKey(5);        System.out.println("ceilingKey 5 is " + ceilingKey);        // 获取最大的key        Integer lastKey = treeMap.lastKey();        System.out.println("lastKey is " + lastKey);    }

运行结果:

核心机制

实现原理

大家有想过TreeMap的底层是怎么实现的吗,是如何维护key的顺序呢?答案就是基于红黑树实现的。

那什么是红黑树呢?我们在这里简单的认识一下,了解一下红黑树的特点:红黑树是一颗自平衡的排序二叉树。我们就先从二叉树开始说起。

  • 二叉树

二叉树很容易理解,就是一棵树分俩叉。

上面这颗就是一颗最普通的二叉树。但是你会发现看起来不那么美观,因为你以H为根节点,发现左右两边高低不平衡,高度相差达到了2。于是出现了平衡二叉树,使得左右两边高低差不多。

  • 平衡二叉树

这下子应该能看到,不管是从任何一个字母为根节点,左右两边的深度差不了2,最多是1。这就是平衡二叉树。不过好景不长,有一天,突然要把字母变成数字,还要保持这种特性怎么办呢?于是又出现了平衡二叉排序树。

  • 平衡二叉排序树

不管是从长相(平衡),还是从规律(排序)感觉这棵树超级完美。但是有一个问题,那就是在增加删除节点的时候,你要时刻去让这棵树保持平衡,需要做太多的工作了,旋转的次数超级多,于是乎出现了红黑树。

  • 红黑树

这就是传说中的红黑树,和平衡二叉排序树的区别就是每个节点涂上了颜色,他有下列五条性质:

  1. 每个节点都只能是红色或者黑色
  2. 根节点是黑色
  3. 每个叶节点(NIL节点,空节点)是黑色的。
  4. 如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

这些性质有什么优点呢?就是插入效率超级高。因为在插入一个元素的时候,最多只需三次旋转,O(1)的复杂度,但是有一点需要说明他的查询效率略微逊色于平衡二叉树,因为他比平衡二叉树会稍微不平衡最多一层,也就是说红黑树的查询性能只比相同内容的avl树最多多一次比较。如何去旋转呢?如下图所示。

首先是左旋:

然后是右旋:

红黑树更详细的内容可以参考这篇文章:segmentfault.com/a/119000001…

源码解析

成员变量

//这是一个比较器,方便插入查找元素等操作private final Comparator comparator;//红黑树的根节点:每个节点是一个Entryprivate transient Entry root;//集合元素数量private transient int size = 0;//集合修改的记录private transient int modCount = 0;
  • comparator是一个排序器,作为key的排序规则
  • root是红黑树的根节点,说明的确底层用的红黑树作为数据结构
static final class Entry implements Map.Entry {        K key;        V value;     //左子树        Entry left;     //右子树        Entry right;     //父节点        Entry parent;     //每个节点的颜色:红黑树属性。        boolean color = BLACK;        Entry(K key, V value, Entry parent) {            this.key = key;            this.value = value;            this.parent = parent;        }        public K getKey() {            return key;        }        public V getValue() {            return value;        }        public V setValue(V value) {            V oldValue = this.value;            this.value = value;            return oldValue;        }        public boolean equals(Object o) {            if (!(o instanceof Map.Entry))                return false;            Map.Entry e = (Map.Entry)o;            return valEquals(key,e.getKey()) && valEquals(value,e.getValue());        }        public int hashCode() {            int keyHash = (key==null ? 0 : key.hashCode());            int valueHash = (value==null ? 0 : value.hashCode());            return keyHash ^ valueHash;        }        public String toString() {            return key + "=" + value;        }    }

查找get方法

TreeMap基于红黑树实现,而红黑树是一种自平衡二叉查找树,所以 TreeMap 的查找操作流程和二叉查找树一致。二叉树的查找流程是这样的,先将目标值和根节点的值进行比较,如果目标值小于根节点的值,则再和根节点的左孩子进行比较。如果目标值大于根节点的值,则继续和根节点的右孩子比较。在查找过程中,如果目标值和二叉树中的某个节点值相等,则返回 true,否则返回 false。TreeMap 查找和此类似,只不过在 TreeMap 中,节点(Entry)存储的是键值对。在查找过程中,比较的是键的大小,返回的是值,如果没找到,则返回null。TreeMap 中的查找方法是get。

public V get(Object key) {        //调用 getEntry方法查找        Entry p = getEntry(key);        return (p==null ? null : p. value);}final Entry getEntry(Object key) {    / 如果比较器为空,只是用key作为比较器查询    if (comparator != null)         return getEntryUsinGComparator(key);    if (key == null)        throw new NullPointerException();    Comparable k = (Comparable) key;    // 取得root节点    Entry p = root;   //核心来了:从root节点开始查找,根据比较器判断是在左子树还是右子树    while (p != null) {        int cmp = k.compareTo(p.key );        if (cmp < 0)            p = p. left;        else if (cmp > 0)            p = p. right;        else           return p;    }   

插入put方法

我们来看下关键的插入方法,在插入时候是如何维护key的。

public V put(K key, V value) {        Entry t = root;       // 1.如果根节点为 null,将新节点设为根节点        if (t == null) {            compare(key, key); // type (and possibly null) check            root = new Entry<>(key, value, null);            size = 1;            modCount++;            return null;        }      //如果root不为null,说明已存在元素         int cmp;        Entry parent;        // split comparator and comparable paths        Comparator cpr = comparator;    //如果比较器不为null 则使用比较器        if (cpr != null) {            //找到元素的插入位置            do {                parent = t;                cmp = cpr.compare(key, t.key);                 //当前key小于节点key 向左子树查找                if (cmp < 0)                    t = t.left;                    //当前key大于节点key 向右子树查找                else if (cmp > 0)                    t = t.right;                else                    //相等的情况下 直接更新节点值                    return t.setValue(value);            } while (t != null);        }            //如果比较器为null 则使用默认比较器        else {            //如果key为null  则抛出异常            if (key == null)                throw new NullPointerException();            @SuppressWarnings("unchecked")                Comparable k = (Comparable) key;             //找到元素的插入位置            do {                parent = t;                cmp = k.compareTo(t.key);                if (cmp < 0)                    t = t.left;                else if (cmp > 0)                    t = t.right;                else                    return t.setValue(value);            } while (t != null);        }        Entry e = new Entry<>(key, value, parent);      //根据比较结果决定插入到左子树还是右子树        if (cmp < 0)            parent.left = e;        else            parent.right = e;    //保持红黑树性质,进行红黑树的旋转等操作        fixAfterInsertion(e);        size++;        modCount++;        return null;    }

比较关键的就是fixAfterInsertion方法, 看懂这个方法需要你对红黑树的机制比较了解。

private void fixAfterInsertion(Entry x) {    // 将新插入节点的颜色设置为红色    x. color = RED;    // while循环,保证新插入节点x不是根节点或者新插入节点x的父节点不是红色(这两种情况不需要调整)    while (x != null && x != root && x. parent.color == RED) {        // 如果新插入节点x的父节点是祖父节点的左孩子        if (parentOf(x) == leftOf(parentOf (parentOf(x)))) {            // 取得新插入节点x的叔叔节点            Entry y = rightOf(parentOf (parentOf(x)));            // 如果新插入x的父节点是红色            if (colorOf(y) == RED) {                // 将x的父节点设置为黑色                setColor(parentOf (x), BLACK);                // 将x的叔叔节点设置为黑色                setColor(y, BLACK);                // 将x的祖父节点设置为红色                setColor(parentOf (parentOf(x)), RED);                // 将x指向祖父节点,如果x的祖父节点的父节点是红色,按照上面的步奏继续循环                x = parentOf(parentOf (x));            } else {                // 如果新插入x的叔叔节点是黑色或缺少,且x的父节点是祖父节点的右孩子                if (x == rightOf( parentOf(x))) {                    // 左旋父节点                    x = parentOf(x);                    rotateLeft(x);                }                // 如果新插入x的叔叔节点是黑色或缺少,且x的父节点是祖父节点的左孩子                // 将x的父节点设置为黑色                setColor(parentOf (x), BLACK);                // 将x的祖父节点设置为红色                setColor(parentOf (parentOf(x)), RED);                // 右旋x的祖父节点                rotateRight( parentOf(parentOf (x)));            }        } else { // 如果新插入节点x的父节点是祖父节点的右孩子和上面的相似            Entry 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;}

删除remove方法

删除remove是最复杂的方法。

public V remove(Object key) {        // 根据key查找到对应的节点对象        Entry p = getEntry(key);        if (p == null)            return null;        // 记录key对应的value,供返回使用        V oldValue = p. value;        // 删除节点        deleteEntry(p);        return oldValue;}
private void deleteEntry(Entry p) {        modCount++;        //元素个数减一        size--;        // 如果被删除的节点p的左孩子和右孩子都不为空,则查找其替代节        if (p.left != null && p. right != null) {            // 查找p的替代节点            Entry s = successor (p);            p. key = s.key ;            p. value = s.value ;            p = s;        }        Entry replacement = (p. left != null ? p.left : p. right);        if (replacement != null) {             // 将p的父节点拷贝给替代节点            replacement. parent = p.parent ;            // 如果替代节点p的父节点为空,也就是p为跟节点,则将replacement设置为根节点            if (p.parent == null)                root = replacement;            // 如果替代节点p是其父节点的左孩子,则将replacement设置为其父节点的左孩子            else if (p == p.parent. left)                p. parent.left   = replacement;            // 如果替代节点p是其父节点的左孩子,则将replacement设置为其父节点的右孩子            else                p. parent.right = replacement;            // 将替代节点p的left、right、parent的指针都指向空            p. left = p.right = p.parent = null;            // 如果替代节点p的颜色是黑色,则需要调整红黑树以保持其平衡            if (p.color == BLACK)                fixAfterDeletion(replacement);        } else if (p.parent == null) { // return if we are the only node.            // 如果要替代节点p没有父节点,代表p为根节点,直接删除即可            root = null;        } else {            // 如果p的颜色是黑色,则调整红黑树            if (p.color == BLACK)                fixAfterDeletion(p);            // 下面删除替代节点p            if (p.parent != null) {                // 解除p的父节点对p的引用                if (p == p.parent .left)                    p. parent.left = null;                else if (p == p.parent. right)                    p. parent.right = null;                // 解除p对p父节点的引用                p. parent = null;            }        }    }

最终还是落到了对红黑树节点的删除上,需要维持红黑树的特性,做一系列的工作。

来源地址:https://blog.csdn.net/weixin_49307478/article/details/126835483

--结束END--

本文标题: 一文带你全面深入了解TreeMap

本文链接: https://lsjlt.com/news/395176.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 一文带你全面深入了解TreeMap
    概述 TreeMap是Map家族中的一员,也是用来存放key-value键值对的。平时在工作中使用的可能并不多,它最大的特点是遍历时是有顺序的,根据key的排序规则来,那么它具体是如何使用,又是怎么实现的呢?本文基于jdk8做一个讲解。 T...
    99+
    2023-09-05
    算法 java 数据结构
  • 一文带你深入了解Java TreeMap
    目录概述TreeMap介绍构造方法关键方法使用案例核心机制实现原理源码解析成员变量查找get方法插入put方法删除remove方法概述 TreeMap是Map家族中的一员,也是用来存...
    99+
    2024-04-02
  • 一文带你全面了解Java Hashtable
    目录概述介绍和使用核心机制实现机制扩容机制源码解析成员变量构造函数put方法get方法remove方法总结概述 HashTable是jdk 1.0中引入的产物,基本上现在很少使用了,...
    99+
    2024-04-02
  • 一文带你深入了解Node.js(图文详解)
    本篇文章通过超多代码和图解来带大家深入解析Node.js,主要内容包括模块化处理、包的基本应用、Express、跨域、操作Mysql数据库等,希望对大家有所帮助!一、Node.js简介1.1什么是Node.jsNode.js是一个调用内置A...
    99+
    2023-05-14
    nodejs
  • 一文带你全面了解Java Properties类
    目录概述介绍构造方法关键方法使用案例源码解析总结概述 Properties是JDK1.0中引入的java类,目前也在项目中大量使用,主要用来读取外部的配置,那除了这个,你对它其他的一...
    99+
    2024-04-02
  • 一篇文章带你深入了解Java封装
    目录如何实现封装代码展示构造方法注意点:代码展示总结如何实现封装 可以分为两步: 第一步:将类的变量声明为private。 第二步:提供公共set和get方法来修改和获取变量的值。 ...
    99+
    2024-04-02
  • 一篇文章带你深入了解javaIO基础
    目录一.认识IO1.IO的分类2.IO的方式3.IO读写的方式4.IO的特性二.文件操作1.文件的构成2.文件的创建3.文件操作的API使用三.IO流1.流的分类2.流的创建3.流的...
    99+
    2024-04-02
  • 一篇文章带你深入了解Java异常
    目录一.初识异常1.常见的异常类型<1>除以0<2>数组下标越界<3>访问null对象2.防御式编程<1>LBYL<2>E...
    99+
    2024-04-02
  • 一篇文章带你深入了解Java基础
    目录1、String类1.1两种对象实例化方式1.2字符串比较1.3字符串常量是String的匿名对象1.4String两种实例化方式区别1、分析直接赋值方式2、构造方法赋值1.5字...
    99+
    2024-04-02
  • 一文带你深入了解Node中的Buffer类
    简单来说所谓的Buffer就是Node在V8堆内存之外分配的一块固定大小的内存空间。当Buffer被用console.log打印出来时,会以字节为单位,打印出一串以十六进制表示的值。创建Buffer了解完Buffer的基本概念后,我们再来创...
    99+
    2023-05-14
    前端 Node.js JavaScript
  • 一文带你深入了解Java8Stream流式编程
    目录一、Stream中间操作1.1、filter:过滤出符合条件的元素1.2、map:映射转换元素1.3、flatMap:将多个流合并为一个流1.4、distinct:去除重复的元素...
    99+
    2023-05-15
    Java8 Stream流式编程 Java8 Stream流 Java8 Stream
  • 一篇文章带你深入了解Mysql触发器
    目录1.对SC表进行插入或修改时,如果考试成绩不在0-100范围内时,则撤销插入或修改操作。2.对SC表进行插入时,如果学生的选课总学分超过30,则报错并撤销插入。3.对SC表进行修...
    99+
    2024-04-02
  • 一篇文章带你深入了解Java类加载
    目录1.类加载<1>.父子类执行的顺序<2>类加载的时机<3>类的生命周期<4>类加载的过程<5>类加载器1.启动类加载器...
    99+
    2024-04-02
  • 一篇文章带你深入了解Java线程池
    目录线程池模型常用线程池ThreadPoolExecutor构造函数参数说明 线程池默认工作行为ForkJoinPoolFutureTask线程数量分析CPU密集型IO密集...
    99+
    2024-04-02
  • 一篇文章带你深入了解Java基础(2)
    目录1、Java主要特点2、计算机的高级汇编语言类型:3、JVM(Java Visual Machine)4、编写第一个Java程序并运行5、CLASSPATH指的是类加载路径6、程...
    99+
    2024-04-02
  • 一篇文章带你深入了解Java基础(3)
    目录1、方法的基本定义2、方法重载3、方法的递归调用4、面向对象的前身是面向过程5、类与对象总结1、方法的基本定义 限制条件:本次所讲解的方法指的是在主类中定义,并且由主方法由主方法...
    99+
    2024-04-02
  • 一篇文章带你深入了解Java基础(4)
    目录1、private实现封装处理2、构造方法与匿名对象3、简单java类4、数组总结1、private实现封装处理 如果像想要知道封装,首先必须清楚如果没有封装会怎么样? 没有封装...
    99+
    2024-04-02
  • 一篇文章带你深入了解Java基础(5)
    目录1、数组Java对数组的支持1、数组的排序:java.util.Arrays.sort(数组名称)2、数组的拷贝:指的是将一个数组的部分内容替换掉另一个数组的部分内容总结1、数组...
    99+
    2024-04-02
  • 一文带你深入了解Java的数据结构
    目录枚举接口(Enumeration)位集合(BitSet)向量(Vector)栈(Stack)字典(Dictionary)哈希表(Hashtable)属性(Properties)J...
    99+
    2023-05-18
    Java数据结构 接口 Java数据结构 类 Java数据结构
  • 一文带你深入理解GolangContext包
    目录1. 基本原理1.1 Context 包的介绍1.2 Context 的创建1.2.1 WithCancel1.2.2 WithDeadline1.2.3 WithTimeout...
    99+
    2023-05-18
    Golang Context包 Golang Context
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作