网站公告

本次更新:没啥更新的

“钱塘江上潮信起,今日方知我是我”

Skip to content

TreeMap 详解(附源码) ​

字数: 0 字 时长: 0 分钟

之前在 LinkedHashMap 中提到过:

text
HashMap
    ↓
不保证遍历顺序


LinkedHashMap
    ↓
通过双向链表
维护插入顺序或访问顺序

那 TreeMap 呢?

TreeMap 底层由:

text
红黑树

实现,可以按照 Key 的:

text
自然顺序

或者

Comparator 指定的自定义顺序

进行排列。

红黑树(Red-Black Tree)是一种自平衡的二叉查找树(Binary Search Tree),结构比较复杂,但拥有良好的性能,查找、插入和删除操作的时间复杂度通常为 O(log n)。

二叉查找树是一种常见的树形结构。

每个节点都包含一个键值对,并且:

text
左子树节点
    ↓
小于当前节点


右子树节点
    ↓
大于当前节点

这个特点使二叉查找树非常适合进行:

text
查找

排序

下面是一个简单的二叉查找树:

text
        8
      /   \
     3     10
    / \      \
   1   6     14
      / \    /
     4   7  13

在这棵二叉查找树中:

text
根节点
    ↓
8


左子树
    ↓
3、1、6、4、7


右子树
    ↓
10、14、13

满足:

text
3 < 8 < 10

1 < 3 < 6

4 < 6 < 7

10 < 14

13 < 14

这是一棵典型的二叉查找树:

  1. 左子树上的节点值小于或等于根节点的值。
  2. 右子树上的节点值大于或等于根节点的值。
  3. 左、右子树本身也分别是二叉查找树。

二叉查找树进行查找时:

text
从根节点开始
    ↓
与当前节点比较

如果:

text
目标值 == 当前节点
    ↓
查找成功


目标值 < 当前节点
    ↓
继续查找左子树


目标值 > 当前节点
    ↓
继续查找右子树

如果一直遍历到叶子节点仍然没有找到:

text
查找失败

插入操作也类似。

从根节点开始比较:

text
新 Key < 当前 Key
    ↓
进入左子树


新 Key > 当前 Key
    ↓
进入右子树


Key 已经存在
    ↓
更新对应的 Value

删除操作稍微复杂一些,需要考虑:

text
删除叶子节点

删除只有一个子节点的节点

删除拥有两个子节点的节点

总的来说,二叉查找树非常适合:

text
查找

排序

删除

但普通二叉查找树有一个明显的问题:

可能变得不平衡。

例如:

text
        6
      /   \
     4     8
    /     / \
   3     7   9
  /
 1

这棵树中:

text
根节点
    ↓
6


左子树
    ↓
4、3、1


右子树
    ↓
8、7、9

左子树明显比右子树更高。

树一旦越来越不平衡:

text
查找

插入

删除

的效率都会下降。

再看一个更加极端的情况:

text
1
 \
  2
   \
    3
     \
      4
       \
        5
         \
          6

这棵树中,所有节点都只有右子节点。

此时虽然结构上还是一棵树,但实际上已经越来越像:

text
链表

查找效率也会从理想状态下的:

text
O(log n)

逐渐退化到:

text
O(n)

所以必须想办法让树保持平衡。

于是就有了:

text
平衡二叉树

典型的平衡二叉树会尽量保证左右子树高度接近。

例如:

text
        8
      /   \
     4     12
    / \    / \
   2   6  10  14
      / \     / \
     5   7   13 15

根节点:

text
8

左子树:

text
4、2、6、5、7

右子树:

text
12、10、14、13、15

左右两边保持得比较平衡。

当插入或者删除节点以后,平衡树会通过一定的调整方式,让树重新保持平衡。

常见的平衡二叉树包括:

text
AVL 树

红黑树

它们都会通过:

text
旋转

等方式调整树的结构。

AVL 树示意图:

text
          8
        /   \
       4     12
      / \   /  \
     2   6 10  14
        / \
       5   7

AVL 树是一种高度平衡的二叉查找树,它要求左右子树的高度差不超过 1。

由于 AVL 树的平衡程度比较高,因此插入和删除时可能需要更多的旋转操作来维持平衡,但查找效率较高。

AVL 树比较适合:

text
读操作较多的场景

例如需要频繁查找或者保证数据有序性的场景。

AVL 树最初由两位苏联计算机科学家:

text
Adelson-Velskii

Landis

在 1962 年提出。

因此:

text
AVL

就是两人名字首字母的组合。

红黑树示意图:

text
          8B
        /    \
       4R    12R
      / \    / \
     2B 6B 10B 14B
        / \
       5R 7R

其中:

text
R
    ↓
Red
红色


B
    ↓
Black
黑色

红黑树是一种通过:

text
红色节点

黑色节点

以及一系列规则维持相对平衡的二叉查找树。

主要规则:

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

红黑树的平衡程度没有 AVL 树那么严格,因此:

text
插入

删除

时需要进行的旋转调整通常更少。

同时它仍然可以保持较好的查找效率,因此比较适合:

text
读写操作比较均衡的场景

1. 自然顺序 ​

默认情况下,TreeMap 会根据:

text
Key 的自然顺序

进行排列。

例如整数:

text
1

2

3

4

5

默认按照升序排列。

java
public static void main(
        String[] args) {

    TreeMap<Integer, String> mapInt =
            new TreeMap<>();

    mapInt.put(
            3,
            "灭绝师太"
    );

    mapInt.put(
            2,
            "灭绝师太"
    );

    mapInt.put(
            1,
            "灭绝师太"
    );

    mapInt.put(
            5,
            "灭绝师太"
    );

    mapInt.put(
            4,
            "灭绝师太"
    );

    System.out.println(
            mapInt
    );
}

输出:

text
{1=灭绝师太, 2=灭绝师太, 3=灭绝师太, 4=灭绝师太, 5=灭绝师太}

TreeMap 是怎么做到的?

来看 put() 方法的源码:

java
public V put(
        K key,
        V value) {

    // 根节点
    Entry<K, V> t =
            root;

    // TreeMap 为空
    if (t == null) {

        // 检查 Key 类型是否合法
        compare(
                key,
                key
        );

        // 创建根节点
        root =
                new Entry<>(
                        key,
                        value,
                        null
                );

        size =
                1;

        return null;
    }

    int cmp;

    Entry<K, V> parent;

    // 获取 Comparator
    Comparator<? super K> comparator =
            this.comparator;

    // 使用 Comparator
    if (comparator != null) {

        do {

            parent =
                    t;

            cmp =
                    comparator.compare(
                            key,
                            t.key
                    );

            if (cmp < 0) {

                // 进入左子树
                t =
                        t.left;

            } else if (cmp > 0) {

                // 进入右子树
                t =
                        t.right;

            } else {

                // Key 相同,更新 Value
                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 {

                // Key 相同,更新 Value
                return t.setValue(
                        value
                );
            }

        } while (t != null);
    }

    // 没找到相同 Key,创建新节点
    Entry<K, V> entry =
            new Entry<>(
                    key,
                    value,
                    parent
            );

    if (cmp < 0) {

        parent.left =
                entry;

    } else {

        parent.right =
                entry;
    }

    // 插入后重新调整红黑树
    fixAfterInsertion(
            entry
    );

    size++;

    return null;
}

整体过程:

text
获取 root
    ↓
TreeMap 是否为空

如果为空:

text
创建根节点
    ↓
size = 1

如果不为空:

text
判断是否存在 Comparator

存在 Comparator:

text
Comparator.compare()
    ↓
比较 Key

不存在 Comparator:

text
Comparable.compareTo()
    ↓
比较 Key

比较结果:

text
cmp < 0
    ↓
进入左子树


cmp > 0
    ↓
进入右子树


cmp == 0
    ↓
Key 已存在
    ↓
更新 Value

如果一直没有找到相同 Key:

text
创建新的 Entry
    ↓
挂到 parent 左侧或右侧
    ↓
fixAfterInsertion()
    ↓
重新调整树的平衡

最后:

text
size++

如果更新的是已有 Key:

text
返回原来的 Value

其中非常重要的一句:

java
cmp =
        k.compareTo(
                t.key
        );

就是使用:

text
Comparable

进行 Key 的自然顺序比较。

如果 Key 是 String,就会调用 String 的:

java
compareTo()

例如源码中的比较逻辑:

java
public int compareTo(
        String anotherString) {

    // 两个字符串长度
    int len1 =
            value.length;

    int len2 =
            anotherString.value.length;

    // 取较短的长度
    int lim =
            Math.min(
                    len1,
                    len2
            );

    char[] v1 =
            value;

    char[] v2 =
            anotherString.value;

    int k =
            0;

    while (k < lim) {

        char c1 =
                v1[k];

        char c2 =
                v2[k];

        if (c1 != c2) {

            return c1 - c2;
        }

        k++;
    }

    return len1 - len2;
}

例如最终得到:

text
{a=灭绝师太, b=灭绝师太, c=灭绝师太, d=灭绝师太, e=灭绝师太}

可以看到:

text
a

b

c

d

e

按照字母自然顺序排列。

2. 自定义排序 ​

如果:

text
自然顺序

不能满足需求,就可以在创建 TreeMap 时指定:

text
Comparator

自定义排序规则。

例如逆序:

java
TreeMap<Integer, String> mapIntReverse =
        new TreeMap<>(
                Comparator.reverseOrder()
        );

mapIntReverse.put(
        3,
        "灭绝师太"
);

mapIntReverse.put(
        2,
        "灭绝师太"
);

mapIntReverse.put(
        1,
        "灭绝师太"
);

mapIntReverse.put(
        5,
        "灭绝师太"
);

mapIntReverse.put(
        4,
        "灭绝师太"
);

System.out.println(
        mapIntReverse
);

输出:

text
{5=灭绝师太, 4=灭绝师太, 3=灭绝师太, 2=灭绝师太, 1=灭绝师太}

TreeMap 提供了可以指定排序规则的构造方法:

java
public TreeMap(
        Comparator<? super K> comparator) {

    this.comparator =
            comparator;
}

Comparator.reverseOrder() 返回一个反序比较器,用来将自然顺序反转。

源码:

java
private static class ReverseComparator
        implements Comparator<Comparable<Object>>,
                   Serializable {

    // 逆序比较器单例
    static final ReverseComparator REVERSE_ORDER =
            new ReverseComparator();

    /**
     * 进行逆序比较
     */
    public int compare(
            Comparable<Object> c1,
            Comparable<Object> c2) {

        return c2.compareTo(
                c1
        );
    }

    /**
     * 反序列化后仍返回单例对象
     */
    private Object readResolve() {

        return Collections.reverseOrder();
    }

    /**
     * 再次反转后恢复自然顺序
     */
    @Override
    public Comparator<Comparable<Object>> reversed() {

        return Comparator.naturalOrder();
    }
}

HashMap 本身不保证遍历顺序。

而 TreeMap 可以始终按照:

text
自然顺序

或者

自定义顺序

排列。

对于需要按照 Key 排序的场景非常有用。

3. 排序的好处 ​

既然 TreeMap 中的 Key 已经是有序的,那么获取:

text
最大 Key

最小 Key

小于某个值的 Key

大于某个值的 Key

就方便很多。

例如:

java
Integer highestKey =
        mapInt.lastKey();

Integer lowestKey =
        mapInt.firstKey();

Set<Integer> keysLessThan3 =
        mapInt
                .headMap(3)
                .keySet();

Set<Integer> keysGreaterThanEqTo3 =
        mapInt
                .tailMap(3)
                .keySet();

System.out.println(
        highestKey
);

System.out.println(
        lowestKey
);

System.out.println(
        keysLessThan3
);

System.out.println(
        keysGreaterThanEqTo3
);

TreeMap 提供了:

java
lastKey()

获取最后一个 Key。

java
firstKey()

获取第一个 Key。

headMap():

text
获取指定 Key 之前的数据

tailMap():

text
获取从指定 Key 开始的数据

输出:

text
5
1
[1, 2]
[3, 4, 5]

再来看一个例子:

java
TreeMap<Integer, String> treeMap =
        new TreeMap<>();

treeMap.put(
        1,
        "value1"
);

treeMap.put(
        2,
        "value2"
);

treeMap.put(
        3,
        "value3"
);

treeMap.put(
        4,
        "value4"
);

treeMap.put(
        5,
        "value5"
);

// 获取小于 3 的键值对
Map<Integer, String> headMap =
        treeMap.headMap(
                3
        );

System.out.println(
        headMap
);

// 获取大于等于 4 的键值对
Map<Integer, String> tailMap =
        treeMap.tailMap(
                4
        );

System.out.println(
        tailMap
);

// 获取大于等于 2 且小于 4 的键值对
Map<Integer, String> subMap =
        treeMap.subMap(
                2,
                4
        );

System.out.println(
        subMap
);

输出:

text
{1=value1, 2=value2}

{4=value4, 5=value5}

{2=value2, 3=value3}

所以:

text
headMap(3)
    ↓
小于 3


tailMap(4)
    ↓
大于等于 4


subMap(2, 4)
    ↓
大于等于 2
并且小于 4

4. 如何选择 Map ​

在学习 TreeMap 之前,已经学习了:

text
HashMap

LinkedHashMap

那这三个 Map 应该怎么选择?

主要考虑以下因素:

  • 是否需要按照 Key 的自然顺序或者自定义顺序排序。如果需要按照 Key 排序,可以使用 TreeMap;如果不需要排序,可以使用 HashMap 或 LinkedHashMap。
  • 是否需要保持插入顺序。如果需要保持插入顺序,可以使用 LinkedHashMap;如果不需要,则可以使用 TreeMap 或 HashMap。
  • 是否需要高效查找。HashMap 和 LinkedHashMap 理想情况下查找时间复杂度为 O(1),TreeMap 的查找时间复杂度为 O(log n)。

LinkedHashMap 内部:

text
哈希表
+
双向链表

哈希表负责:

text
查找键值对

双向链表负责:

text
维护顺序

所以其查找仍然主要通过哈希表完成。

简单对比:

特性TreeMapHashMapLinkedHashMap
排序支持不支持不支持
插入顺序不保证不保证保证
查找效率O(log n)理想情况下 O(1)理想情况下 O(1)
空间占用通常较大通常较小通常较大
适用场景需要排序无需排序需要保持插入顺序

Noctiflorous Garden · MIT License