62. TreeMap 及排序规则

一、TreeMap 概述

TreeMap 的定义

TreeMap 是 Java 集合框架中基于红黑树(Red-Black Tree)实现的有序键值对映射(Map)。它继承自 AbstractMap,实现了 NavigableMap 接口,默认按照键的自然顺序Comparable 接口)或通过自定义比较器Comparator)排序。

TreeMap 的核心特点

  1. 有序性

    • 键值对按照键的顺序存储(升序或降序),迭代时直接输出排序后的结果。
    • 支持按范围查询(如 subMap()headMap()tailMap())。
  2. 基于红黑树

    • 红黑树是自平衡二叉查找树,保证插入、删除、查找的时间复杂度为 O(log n)
    • 相比 HashMap(O(1)),TreeMap 牺牲部分性能换取有序性。
  3. 键的唯一性与排序约束

    • 键不允许为 null(若使用自然排序,值可为 null)。
    • 键必须实现 Comparable 接口,或通过构造器传入 Comparator
  4. 线程不安全

    • 需手动同步(如 Collections.synchronizedSortedMap)。

示例代码(自然排序)

TreeMap<Integer, String> map = new TreeMap<>();
map.put(3, "Apple");
map.put(1, "Banana");
map.put(2, "Cherry");
System.out.println(map); // 输出:{1=Banana, 2=Cherry, 3=Apple}(自动按键升序排列)

适用场景

  • 需要键值对按顺序遍历或范围查询时(如按分数段查询学生成绩)。
  • 键的排序逻辑明确且稳定(如日期、数值等)。

注意事项

  • 性能权衡:频繁插入/删除时,红黑树的平衡操作可能比 HashMap 更耗时。
  • 排序一致性:若键为自定义对象,需确保 compareTo()Comparator 逻辑与 equals() 一致(避免违反 Set 契约)。

TreeMap 的继承结构

1. 继承关系图
java.lang.Object
  ↳ java.util.AbstractMap<K,V>
      ↳ java.util.TreeMap<K,V>
2. 实现接口
  • java.util.NavigableMap<K,V>
    (提供导航方法如 lowerEntry(), floorKey() 等)
  • java.util.SortedMap<K,V>
    (保证键的有序性)
  • java.lang.Cloneable
    (支持 clone() 方法)
  • java.io.Serializable
    (支持序列化)
3. 关键特点
  • 基于红黑树(自平衡二叉查找树)实现
  • 默认按键的自然顺序排序(依赖 Comparable 接口)
  • 可通过 Comparator 自定义排序规则
  • 非线程安全(需手动同步)
4. 与 HashMap 对比
特性 TreeMap HashMap
数据结构 红黑树 数组+链表/红黑树
排序 按键有序 无序
时间复杂度 增删查 O(log n) 平均 O(1)
允许 null 键 取决于 Comparator 允许

TreeMap 与 HashMap 的区别

数据结构
  • TreeMap:基于红黑树(一种自平衡二叉查找树)实现,保证元素有序存储(按键的自然顺序或自定义比较器排序)。
  • HashMap:基于哈希表(数组+链表/红黑树)实现,元素存储无序。
性能对比
  • 插入/删除/查找
    • TreeMap:时间复杂度为 O(log n),因红黑树的平衡操作。
    • HashMap:平均 O(1)(理想哈希冲突下),最坏 O(n)(哈希冲突严重时退化为链表)。
  • 内存占用
    • TreeMap:需额外存储树结构信息(左/右子节点、颜色等),内存占用较高。
    • HashMap:仅需存储哈希桶和链表/树节点,通常更节省内存。
排序特性
  • TreeMap
    • 默认按键的自然顺序(如 String 按字典序,Integer 按数值)排序。
    • 可通过 Comparator 自定义排序规则。
    • 支持 firstKey()lastKey() 等有序操作。
  • HashMap
    • 无序存储,迭代顺序不固定(Java 8 后按桶顺序,但不保证逻辑顺序)。
使用场景
  • TreeMap
    • 需要有序遍历键值对的场景(如按范围查询、排行榜)。
    • 键需实现 Comparable 或提供 Comparator
  • HashMap
    • 高频增删查操作,且无需排序的场景(如缓存、快速查找)。
示例代码
// TreeMap 示例(自然排序)
TreeMap<Integer, String> treeMap = new TreeMap<>();
treeMap.put(3, "Three");
treeMap.put(1, "One");
treeMap.put(2, "Two");
System.out.println(treeMap); // 输出:{1=One, 2=Two, 3=Three}

// HashMap 示例(无序)
HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(3, "Three");
hashMap.put(1, "One");
hashMap.put(2, "Two");
System.out.println(hashMap); // 输出可能为:{1=One, 2=Two, 3=Three}(但不保证)
注意事项
  • 键的要求
    • TreeMap 的键必须实现 Comparable 或传入 Comparator,否则抛 ClassCastException
    • HashMap 的键需正确实现 hashCode()equals()
  • 线程安全
    • 两者均非线程安全,需用 Collections.synchronizedMapConcurrentHashMap 包装。

TreeMap 的线程安全性

概念定义

TreeMap 是 Java 集合框架中的一个基于红黑树(Red-Black Tree)实现的有序映射(Map)。它默认按照键的自然顺序排序,或者通过传入的 Comparator 进行自定义排序。TreeMap 本身不是线程安全的类,这意味着在多线程环境下直接使用可能会导致数据不一致或并发修改异常(ConcurrentModificationException)。

线程不安全的表现
  1. 并发修改问题:多个线程同时对 TreeMap 进行增删改操作时,可能导致内部红黑树结构被破坏,进而引发数据不一致或程序崩溃。
  2. 迭代器失效:在遍历 TreeMap 时(如使用 iterator()),如果其他线程修改了映射,会抛出 ConcurrentModificationException
如何保证线程安全
  1. 使用 Collections.synchronizedSortedMap
    可以通过 Collections.synchronizedSortedMap 方法将 TreeMap 包装成线程安全的 SortedMap:

    SortedMap<String, Integer> syncTreeMap = 
        Collections.synchronizedSortedMap(new TreeMap<>());
    
    • 所有操作(如 putgetremove)会通过同步锁(synchronized)保证线程安全。
    • 缺点:高并发场景下性能较差,因为所有操作会竞争同一把锁。
  2. 使用 ConcurrentSkipListMap
    Java 提供的 ConcurrentSkipListMap 是一个线程安全且有序的映射实现,基于跳表(Skip List)数据结构:

    ConcurrentSkipListMap<String, Integer> concurrentMap = 
        new ConcurrentSkipListMap<>();
    
    • 支持高并发读写,性能优于 synchronizedSortedMap
    • 提供原子性操作(如 putIfAbsentreplace)。
注意事项
  1. 手动同步
    如果需要对多个操作(如“检查-修改”)保证原子性,即使使用 synchronizedSortedMap 也需要额外同步:

    synchronized (syncTreeMap) {
         
         
        if (!syncTreeMap.containsKey("key")) {
         
         
            syncTreeMap.put("key", 1);
        }
    }
    
  2. 避免迭代器并发修改
    遍历时需显式加锁,否则可能抛出异常:

    synchronized (syncTreeMap) {
         
         
        for (Map.Entry<String, Integer> entry : syncTreeMap.entrySet()) {
         
         
            // 操作 entry
        }
    }
    
  3. 性能权衡

    • synchronizedSortedMap 适合低并发场景。
    • ConcurrentSkipListMap 适合高并发且需要排序的场景,但内存占用略高。

TreeMap 的性能分析

基本性能特征
  1. 时间复杂度

    • 插入(put):O(log n) —— 基于红黑树的平衡特性。
    • 删除(remove):O(log n) —— 需要重新平衡树结构。
    • 查询(get/containsKey):O(log n) —— 二分查找树的路径。
    • 遍历(迭代):O(n) —— 中序遍历所有节点。
  2. 空间复杂度

    • 每个节点存储键值对及额外的树结构信息(父/子节点指针、颜色标记等),空间占用略高于 HashMap
与 HashMap 的对比
操作 TreeMap (红黑树) HashMap (哈希表)
插入/删除 O(log n) O(1) 平均
查询 O(log n) O(1) 平均
有序性 支持(自然序/自定义) 无序
适用场景
  1. 需要有序键的场景

    • 按键排序遍历(如输出排行榜、范围查询)。
    • 使用 subMap()headMap()tailMap() 等范围操作时高效。
  2. 内存敏感但需排序

    • 相比 LinkedHashMap(维护插入顺序),TreeMap 在动态排序场景更节省内存。
性能优化注意事项
  1. 自定义比较器的代价

    • 频繁比较的 Comparator 应尽量简单(如避免字符串拼接等耗时操作)。
    // 低效示例:每次比较拼接字符串
    TreeMap<String, String> map = new TreeMap<>((a, b) -> 
        (a + b).compareTo(b + a)); // 避免此写法
    
  2. 初始容量无关

    • HashMap 不同,TreeMap 无扩容机制,无需关注初始容量参数。
  3. 并发场景的替代方案

    • 多线程环境下需用 Collections.synchronizedSortedMap 包装,或改用 ConcurrentSkipListMap(跳表实现,并发安全且有序)。
示例:性能测试对比
// 插入 100,000 个随机键值对的耗时对比
public static void main(String[] args) {
   
   
    Map<Integer, String> hashMap = new HashMap<>();
    Map<Integer, String> treeMap = new TreeMap<>();

    long start = System.currentTimeMillis();
    for (int i = 0; i < 100_000; i++) {
   
   
        hashMap.put(ThreadLocalRandom.current().nextInt(), "value");
    }
    System.out.println("HashMap 插入耗时: " + (System.currentTimeMillis() - start) + "ms");

    start = System.currentTimeMillis();
    for (int i = 0; i < 100_000; i++) {
   
   
        treeMap.put(ThreadLocalRandom.current().nextInt(), "value");
    }
    System.out.println("TreeMap 插入耗时: " + (System.currentTimeMillis() - start) + "ms");
}

典型输出

HashMap 插入耗时: 15ms
TreeMap 插入耗时: 50ms

二、TreeMap 的核心实现

红黑树数据结构基础

红黑树定义

红黑树是一种自平衡二叉查找树,通过颜色标记和旋转操作维持平衡,保证最坏情况下基本操作(插入、删除、查找)的时间复杂度为 O(log n)

核心性质
  1. 颜色属性:每个节点非红即黑。
  2. 根节点:根必须是黑色。
  3. 叶子节点:所有空节点(NIL)视为黑色。
  4. 红色限制:红色节点的子节点必须为黑色(即不能有连续红色节点)。
  5. 黑高一致:从任意节点到其子孙叶子节点的路径包含相同数量的黑色节点。
平衡机制

通过旋转(左旋/右旋)和颜色调整修复插入/删除后的性质破坏:

  • 左旋:将右子节点提升为父节点。
  • 右旋:将左子节点提升为父节点。
与普通二叉树的区别
特性 普通二叉树 红黑树
平衡性 可能退化为链表 自动平衡
查找时间复杂度 O(n) 最坏 O(log n) 稳定
插入/删除成本 低(无平衡操作) 需额外平衡操作
代码示例(节点结构)
class RBNode<K extends Comparable<K>, V> {
   
   
    K key;
    V value;
    RBNode<K, V> left, right, parent;
    boolean color; // 通常 true=红,false=黑

    public RBNode(K key, V value) {
   
   
        this.key = key;
        this.value = value;
        this.color = true; // 新节点默认红色
    }
}
使用场景
  1. 需要有序且高频更新的数据:如Java的TreeMap、Linux进程调度。
  2. 避免最坏性能:当普通二叉搜索树可能退化为链表时。
注意事项
  1. 实现复杂度:需处理多种旋转和颜色调整情况。
  2. 内存开销:每个节点需额外存储颜色和父节点指针。

TreeMap 的内部结构

红黑树(Red-Black Tree)

TreeMap 基于 红黑树(一种自平衡的二叉查找树)实现。红黑树通过以下规则保证平衡性:

  1. 节点颜色:每个节点是红色或黑色。
  2. 根节点:根节点必须是黑色。
  3. 叶子节点(NIL):所有叶子节点(空节点)视为黑色。
  4. 红色节点限制:红色节点的子节点必须是黑色(即不能有连续的红色节点)。
  5. 黑高平衡:从任意节点到其叶子节点的路径上,黑色节点数量相同。
节点结构(Entry)

TreeMap 的节点通过静态内部类 Entry 实现,包含以下字段:

static final class Entry<K,V> implements Map.Entry<K,V> {
   
   
    K key;          // 键
    V value;        // 值
    Entry<K,V> left;  // 左子节点
    Entry<K,V> right; // 右子节点
    Entry<K,V> parent; // 父节点
    boolean color = BLACK; // 节点颜色(默认黑色)
}
排序规则

TreeMap 通过以下两种方式维护键的有序性:

  1. 自然排序:键需实现 Comparable 接口,通过 compareTo() 方法排序。
  2. 定制排序:构造时传入 Comparator 对象,自定义比较逻辑。
操作复杂度
  • 插入/删除/查找:时间复杂度为 O(log n),因红黑树的自平衡特性避免退化为链表。
关键方法实现
  1. 插入(put)

    • 按二叉查找树规则找到插入位置。
    • 新节点初始为红色,通过旋转和变色恢复平衡。
  2. 删除(remove)

    • 找到待删除节点后,分情况处理(无子节点、单子节点、双子节点)。
    • 通过调整保证删除后仍满足红黑树性质。
示例代码(自然排序)
TreeMap<String, Integer> map = new TreeMap<>();
map.put("apple", 1);
map.put("banana", 2);
System.out.println(map); // 输出:{apple=1, banana=2}(按键字母顺序排序)

Entry 节点的组成

TreeMap 中,Entry 是存储键值对的基本单元,同时也是红黑树的节点结构。其核心组成如下:

1. 基本字段
static final class Entry<K,V> implements Map.Entry<K,V> {
   
   
    K key;          // 存储键
    V value;        // 存储值
    Entry<K,V> left;  // 左子节点
    Entry<K,V> right; // 右子节点
    Entry<K,V> parent; // 父节点
    boolean color = BLACK; // 节点颜色(红黑树特性)
}
2. 关键特性
  • 键值对存储keyvalue 直接存储数据。
  • 树形结构:通过 leftrightparent 维护红黑树的父子关系。
  • 颜色标记color 字段(RED/BLACK)用于红黑树的平衡规则。
3. 方法实现
  • getKey()getValue():直接返回键或值。
  • setValue(V value):修改当前节点的值。
  • equals()hashCode():基于 Map.Entry 的规范实现键值对的比较。
4. 示例代码(遍历节点)
TreeMap<Integer, String> map = new TreeMap<>();
map.put(3, "Three");
map.put(1, "One");

// 获取第一个Entry节点
Entry<Integer, String> firstEntry = map.firstEntry();
System.out.println("Key: " + firstEntry.getKey() + ", Value: " + firstEntry.getValue());
5. 注意事项
  • 不可变性key 不可为 null(依赖比较器或自然排序)。
  • 结构依赖:修改 left/right/parent 需同步维护红黑树平衡。
  • 线程不安全:多线程操作需外部同步。

TreeMap 的构造方法

默认构造方法
TreeMap<K, V> map = new TreeMap<>();
  • 作用:创建一个空的 TreeMap,使用键的自然顺序进行排序。
  • 要求:键的类型 K 必须实现 Comparable 接口(如 StringInteger 等)。
  • 示例
    TreeMap<String, Integer> map = new TreeMap<>(); // 按键的字典序排序
    
自定义比较器的构造方法
TreeMap<K, V> map = new TreeMap<>(Comparator<? super K> comparator);
  • 作用:通过传入的 Comparator 自定义键的排序规则。
  • 适用场景:键未实现 Comparable,或需要覆盖自然顺序时。
  • 示例(按字符串长度排序):
    TreeMap<String, Integer> map = new TreeMap<>(
        (s1, s2) -> s1.length() - s2.length() // 比较字符串长度
    );
    
从 Map 初始化的构造方法
TreeMap<K, V> map = new TreeMap<>(Map<? extends K, ? extends V> m);
  • 作用:将已有 Map 的数据复制到 TreeMap 中,并按键的自然顺序排序。
  • 注意:若原 Map 的键未实现 Comparable,会抛出 ClassCastException
从 SortedMap 初始化的构造方法
TreeMap<K, V> map = new TreeMap<>(SortedMap<K, ? extends V> m);
  • 作用:复制 SortedMap 的数据及其排序规则到新 TreeMap
  • 特点:保留原 SortedMap 的比较器(自然顺序或自定义顺序)。

自定义比较器的实现方式

概念定义

自定义比较器是用于定义对象排序规则的接口实现,通常通过 Comparator 接口实现。它允许开发者灵活地指定对象的比较逻辑,而不依赖对象的自然排序(如 Comparable 接口)。

使用场景
  1. 对没有实现 Comparable 接口的类进行排序。
  2. 需要多种排序规则(如按年龄、姓名等不同字段排序)。
  3. 对集合(如 TreeMapTreeSet)或数组进行自定义排序。
实现方式
  1. 匿名内部类
    适用于临时、简单的比较逻辑。

    Comparator<String> lengthComparator = new Comparator<String>() {
         
         
        @Override
        public int compare(String s1, String s2) {
         
         
            return Integer.compare(s1.length(), s2.length());
        }
    };
    
  2. Lambda 表达式(Java 8+)
    简化代码,适用于函数式编程。

    Comparator<String> lengthComparator = (s1, s2) -> Integer.compare(s1.length(), s2.length());
    
  3. 方法引用(Java 8+)
    进一步简化,直接引用已有方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值