深读源码-java集合之TreeMap源码分析(四)

本文深入探讨了红黑树的特性及其在TreeMap中的应用,详细讲解了二叉树的遍历方法,包括前序、中序和后序遍历,并对比了递归与非递归遍历方式,最后分析了红黑树遍历的时间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

二叉树的遍历

我们知道二叉查找树的遍历有前序遍历、中序遍历、后序遍历。

(1)前序遍历,先遍历我,再遍历我的左子节点,最后遍历我的右子节点;

(2)中序遍历,先遍历我的左子节点,再遍历我,最后遍历我的右子节点;

(3)后序遍历,先遍历我的左子节点,再遍历我的右子节点,最后遍历我;

这里的前中后都是以“我”的顺序为准的,我在前就是前序遍历,我在中就是中序遍历,我在后就是后序遍历。

下面让我们看看经典的中序遍历是怎么实现的:

public class TreeMapTest {

    public static void main(String[] args) {
        // 构建一颗10个元素的树
        TreeNode<Integer> node = new TreeNode<>(1, null).insert(2)
                .insert(6).insert(3).insert(5).insert(9)
                .insert(7).insert(8).insert(4).insert(10);

        // 中序遍历,打印结果为1到10的顺序
        node.root().inOrderTraverse();
    }
}

/**
 * 树节点,假设不存在重复元素
 * @param <T>
 */
class TreeNode<T extends Comparable<T>> {
    T value;
    TreeNode<T> parent;
    TreeNode<T> left, right;

    public TreeNode(T value, TreeNode<T> parent) {
        this.value = value;
        this.parent = parent;
    }

    /**
     * 获取根节点
     */
    TreeNode<T> root() {
        TreeNode<T> cur = this;
        while (cur.parent != null) {
            cur = cur.parent;
        }
        return cur;
    }

    /**
     * 中序遍历
     */
    void inOrderTraverse() {
        if(this.left != null) this.left.inOrderTraverse();
        System.out.println(this.value);
        if(this.right != null) this.right.inOrderTraverse();
    }

    /**
     * 经典的二叉树插入元素的方法
     */
    TreeNode<T> insert(T value) {
        // 先找根元素
        TreeNode<T> cur = root();

        TreeNode<T> p;
        int dir;

        // 寻找元素应该插入的位置
        do {
            p = cur;
            if ((dir=value.compareTo(p.value)) < 0) {
                cur = cur.left;
            } else {
                cur = cur.right;
            }
        } while (cur != null);

        // 把元素放到找到的位置
        if (dir < 0) {
            p.left = new TreeNode<>(value, p);
            return p.left;
        } else {
            p.right = new TreeNode<>(value, p);
            return p.right;
        }
    }
}

TreeMap的遍历

从上面二叉树的遍历我们很明显地看到,它是通过递归的方式实现的,但是递归会占用额外的空间,直接到线程栈整个释放掉才会把方法中申请的变量销毁掉,所以当元素特别多的时候是一件很危险的事。

(上面的例子中,没有申请额外的空间,如果有声明变量,则可以理解为直到方法完成才会销毁变量)

那么,有没有什么方法不用递归呢?

让我们来看看java中的实现:

@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
    Objects.requireNonNull(action);
    // 遍历前的修改次数
    int expectedModCount = modCount;
    // 执行遍历,先获取第一个元素的位置,再循环遍历后继节点
    for (Entry<K, V> e = getFirstEntry(); e != null; e = successor(e)) {
        // 执行动作
        action.accept(e.key, e.value);

        // 如果发现修改次数变了,则抛出异常
        if (expectedModCount != modCount) {
            throw new ConcurrentModificationException();
        }
    }
}

是不是很简单?!

(1)寻找第一个节点;

从根节点开始找最左边的节点,即最小的元素。

    final Entry<K,V> getFirstEntry() {
        Entry<K,V> p = root;
        // 从根节点开始找最左边的节点,即最小的元素
        if (p != null)
            while (p.left != null)
                p = p.left;
        return p;
    }

(2)循环遍历后继节点;

寻找后继节点这个方法我们在删除元素的时候也用到过,当时的场景是有右子树,则从其右子树中寻找最小的节点。

static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
    if (t == null)
        // 如果当前节点为空,返回空
        return null;
    else if (t.right != null) {
        // 如果当前节点有右子树,取右子树中最小的节点
        Entry<K,V> p = t.right;
        while (p.left != null)
            p = p.left;
        return p;
    } else {
        // 如果当前节点没有右子树
        // 如果当前节点是父节点的左子节点,直接返回父节点
        // 如果当前节点是父节点的右子节点,一直往上找,直到找到一个祖先节点是其父节点的左子节点为止,返回这个祖先节点的父节点
        Entry<K,V> p = t.parent;
        Entry<K,V> ch = t;
        while (p != null && ch == p.right) {
            ch = p;
            p = p.parent;
        }
        return p;
    }
}

让我们一起来分析下这种方式的时间复杂度吧。

首先,寻找第一个元素,因为红黑树是接近平衡的二叉树,所以找最小的节点,相当于是从顶到底了,时间复杂度为O(log n);

其次,寻找后继节点,因为红黑树插入元素的时候会自动平衡,最坏的情况就是寻找右子树中最小的节点,时间复杂度为O(log k),k为右子树元素个数;

最后,需要遍历所有元素,时间复杂度为O(n);

所以,总的时间复杂度为 O(log n) + O(n * log k) ≈ O(n)。

虽然遍历红黑树的时间复杂度是O(n),但是它实际是要比跳表要慢一点的,啥?跳表是啥?安心,后面会讲到跳表的。

总结

到这里红黑树就整个讲完了,让我们再回顾下红黑树的特性:

(1)节点是红色或者黑色。

(2)根节点是黑色。

(3)每个叶子节点(NIL)是黑色。(注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!)

(4)如果一个节点是红色的,则它的子节点必须是黑色的。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)

(5)从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

除了上述这些标准的红黑树的特性,你还能讲出来哪些TreeMap的特性呢?

(1)TreeMap的存储结构只有一颗红黑树;

(2)TreeMap中的元素是有序的,按key的顺序排列;

(3)TreeMap比HashMap要慢一些,因为HashMap前面还做了一层桶,寻找元素要快很多;

(4)TreeMap没有扩容的概念;

(5)TreeMap的遍历不是采用传统的递归式遍历;

(6)TreeMap可以按范围查找元素,查找最近的元素;

(7)欢迎补充...


原文链接:https://www.cnblogs.com/tong-yuan/p/10657637.html

<think>嗯,用户让我介绍一下Java集合中的TreeMap,结合源码详细分析。首先,我需要回忆一下TreeMap的基本概念和它在Java集合框架中的位置。TreeMap是基于红黑树实现的NavigableMap,提供有序的键值对管理。用户可能已经了解过HashMap,所以对比一下两者的不同可能有助于理解。 接下来,我得确定要覆盖的关键点。首先是TreeMap的结构,红黑树的特点,比如自平衡的二叉树,保证基本操作的时间复杂度是O(log n)。然后是关键的源码分析,比如put方法、get方法、删除操作,以及红黑树的旋转和颜色调整。 用户可能希望了解TreeMap如何保持有序,所以需要提到Comparator或者自然顺序。TreeMap的构造函数允许传入Comparator,如果没有,则使用键的自然顺序。这部分在源码中如何体现呢?比如,比较键的时候会检查comparator是否为null,决定使用哪种方式比较。 然后,put方法的实现。插入新节点时,TreeMap会找到合适的位置,然后调整红黑树的结构。这里需要详细分析fixAfterInsertion方法,包括左旋、右旋、颜色变换等操作。可能需要解释红黑树的五个性质,以及插入后如何通过旋转和变色来维持这些性质。 删除操作也是重点,remove方法中的fixAfterDeletion方法,处理的情况可能更复杂,比如删除节点有两个子节点时如何找到后继节点,调整颜色和结构。这部分源码比较复杂,可能需要逐步解释。 另外,TreeMap的导航方法,如firstKey(), lastKey(), higherKey(), lowerKey()等,这些方法利用了红黑树的有序性,可以高效地找到边界节点或者邻近节点。这部分在源码中是如何实现的?通常是通过遍历左子树或右子树找到最左或最右节点。 还需要考虑线程安全的问题。TreeMap不是线程安全的,如果在多线程环境下使用,需要外部同步。用户可能想知道这一点,所以应该提到。 可能用户的实际应用场景是什么?比如需要有序的键值对,或者范围查询,比如subMap方法的应用。这时候可以简要举例说明使用场景。 在分析源码时,需要注意代码的版本,不同JDK版本可能有细微差别。假设用户使用的是Java 8的源码,但需要确认。不过大部分核心逻辑在版本间变化不大,红黑树的基本实现应该一致。 现在,需要组织这些内容,结构清晰。首先介绍TreeMap的基本概念和特性,然后结构分析,接着详细讲解put、get、remove等方法的源码,包括红黑树的调整。最后可以总结使用场景和注意事项。 可能还需要指出TreeMap的优缺点,比如有序性带来的log(n)时间性能,相对于HashMap的O(1)性能,但需要排序时更高效。同时,键是否实现Comparable接口或者提供Comparator的重要性,避免ClassCastException。 在分析源码时,可能需要贴出关键代码片段,并逐行解释。例如,在put方法中,如何遍历树找到插入位置,插入后调用fixAfterInsertion进行平衡。在fixAfterInsertion中,如何处理不同情况下的颜色变化和旋转操作。 需要注意的是,源码中的一些辅助方法,比如左旋rotateLeft和右旋rotateRight,它们的实现逻辑是怎样的,参数是什么,如何改变节点的父子关系和左右子节点。 总结部分要强调TreeMap的适用场景,以及在使用时需要注意的地方,比如线程安全和Comparator的使用。可能用户在使用TreeMap时遇到的常见问题,比如顺序不符合预期,或者性能问题,可以在这里提醒。 最后检查是否有遗漏的重要点,比如Entry节点的结构,TreeMap中包含的根节点root,以及size、modCount等变量的作用。modCount用于迭代时的快速失败机制,这点也值得提及。 总之,整个思考过程需要涵盖TreeMap的核心实现,源码中的关键方法,红黑树的维护机制,以及使用时的注意事项,确保用户能够全面理解TreeMap的工作原理和内部结构。</think> Java集合TreeMap源码解析与实现分析 -- #### **一、TreeMap概述** `TreeMap` 是Java集合框架中基于 **红黑树(Red-Black Tree)** 实现的 `NavigableMap`,其核心特性是 **有序存储键值对**。与 `HashMap` 不同,`TreeMap` 的键按照自然顺序(`Comparable`)或自定义 `Comparator` 排序,保证所有操作(增删改查)的时间复杂度为 **O(log n)**。 -- #### **二、核心数据结构红黑树** 红黑树是一种自平衡二叉查找树,满足以下性质: 1. 节点是红色或黑色。 2. 根节点是黑色。 3. 叶子节点(NIL)是黑色。 4. 红色节点的子节点必须是黑色。 5. 从任一节点到其叶子节点的所有路径包含相同数量的黑色节点。 这些规则确保树的高度近似平衡,保证操作效率。 --- #### **三、源码核心分析** ##### **1. 内部类:Entry** `TreeMap` 的节点通过内部类 `Entry` 表示: ```java 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. 构造方法** - **自然排序构造**:要求键实现 `Comparable`。 - **自定义Comparator构造**:允许外部传入排序逻辑。 ```java public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; // 决定节点顺序的核心 } ``` ##### **3. **put() 方法源码解析** 插入键值对的核心流程: ```java public V put(K key, V value) { Entry<K,V> t = root; if (t == null) { // 树为空时直接创建根节点 compare(key, key); // 检查key是否可比较 root = new Entry<>(key, value, null); size = 1; modCount++; return null; } int cmp; Entry<K,V> parent; Comparator<? super K> cpr = comparator; if (cpr != null) { // 使用Comparator比较 do { parent = t; cmp = cpr.compare(key, t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); // 替换旧值 } while (t != null); } else { // 自然排序比较 if (key == null) throw new NullPointerException(); 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; else return t.setValue(value); } while (t != null); } Entry<K,V> e = new Entry<>(key, value, parent); if (cmp < 0) parent.left = e; else parent.right = e; fixAfterInsertion(e); // 红黑树平衡调整 size++; modCount++; return null; } ``` **关键点**: - 通过循环找到插入位置。 - 插入后调用 `fixAfterInsertion(e)` 调整树结构。 --- ##### **4. fixAfterInsertion()红黑树平衡调整** 插入新节点(默认红色)后,可能破坏红黑树性质,需调整颜色或旋转。 ```java 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) { // Case 1:叔叔节点为红色 setColor(parentOf(x), BLACK); setColor(y, BLACK); setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } else { if (x == rightOf(parentOf(x))) { // Case 2:x是右子节点 x = parentOf(x); rotateLeft(x); // 左旋 } // Case 3:x是左子节点 setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); rotateRight(parentOf(parentOf(x))); // 右旋 } } else { // 对称处理父节点在右侧的情况 // ... 类似逻辑,方向相反 ... } } root.color = BLACK; // 确保根节点为黑色 } ``` **调整策略**: - **Case 1**:叔叔节点为红色 → 父、叔变黑,祖父变红。 - **Case 2**:插入节点是右子节点 → 左旋父节点,转为Case 3。 - **Case 3**:插入节点是左子节点 → 右旋祖父节点并调整颜色。 --- ##### **5. get() 方法源码解析** 查找逻辑基于二叉搜索树: ```java public V get(Object key) { Entry<K,V> p = getEntry(key); return (p == null ? null : p.value); } final Entry<K,V> getEntry(Object key) { if (comparator != null) return getEntryUsingComparator(key); if (key == null) throw new NullPointerException(); Comparable<? super K> k = (Comparable<? super K>) key; Entry<K,V> p = 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; } return null; } ``` --- ##### **6. remove() 方法解析** 删除节点后需调用 `fixAfterDeletion()` 调整红黑树平衡,逻辑比插入更复杂,涉及多种情况处理(如兄弟节点颜色、侄子节点颜色等)。 --- #### **TreeMap特性总结** 1. **有序性**:键按自然顺序或 `Comparator` 排序。 2. **时间复杂度**:增删改查均为 O(log n)。 3. **线程不安全**:需外部同步(如 `Collections.synchronizedSortedMap`)。 4. **导航方法**:支持 `firstKey()`, `lastKey()`, `ceilingKey()`, `floorKey()` 等高效范围查询。 --- #### **五、使用场景** - 需要有序键值对的场景。 - 频繁范围查询(如数据库索引)。 - 需自定义排序逻辑时(通过 `Comparator`)。 通过源码分析,可以更高效地利用 `TreeMap` 的特性,避免因误用导致性能问题或逻辑错误。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值