【Java容器源码】TreeMap 源码分析

た 入场券 2022-12-05 13:51 305阅读 0赞

TreeMap 底层的数据结构就是红黑树,和 HashMap 的红黑树结构一样。

不同的是,TreeMap 利用了红黑树左节点小,右节点大的性质,根据 key 进行排序,使每个元素能够插入到红黑树大小适当的位置,维护了 key 的大小关系,适用于 key 需要排序的场景。

因为底层使用的是平衡红黑树的结构,所以 containsKey、get、put、remove 等方法的时间复杂度都是 log(n)。


TreeMap 继承关系,核心成员变量,主要构造函数:

  1. public class TreeMap<K,V>
  2. extends AbstractMap<K,V>
  3. implements NavigableMap<K,V>, Cloneable, java.io.Serializable{
  4. // 比较器,如果外部有传进来 Comparator 比较器,首先用外部的
  5. // 如果外部比较器为空,则使用 key 自己实现的 Comparable#compareTo 方法
  6. private final Comparator<? super K> comparator;
  7. // 红黑树的根节点
  8. private transient Entry<K,V> root;
  9. // 红黑树的已有元素大小
  10. private transient int size = 0;
  11. // 树结构变化的版本号,用于迭代过程中的快速失败场景
  12. private transient int modCount = 0;
  13. //--------------------------------红黑树节点-------------------------------------
  14. static final class Entry<K,V> implements Map.Entry<K,V> {
  15. K key;
  16. V value;
  17. Entry<K,V> left;
  18. Entry<K,V> right;
  19. Entry<K,V> parent; // 相较于平时算法的树,这里多了parent
  20. boolean color = BLACK;
  21. Entry(K key, V value, Entry<K,V> parent) {
  22. }
  23. public K getKey() {
  24. }
  25. public V getValue() {
  26. }
  27. public V setValue(V value) {
  28. }
  29. public boolean equals(Object o) {
  30. }
  31. public int hashCode() {
  32. }
  33. public String toString() {
  34. }
  35. }
  36. //---------------------------------构造器------------------------------------
  37. // 传入比较器,实现
  38. public TreeMap(Comparator<? super K> comparator) {
  39. this.comparator = comparator;
  40. }
  41. // 传入map
  42. public TreeMap(Map<? extends K, ? extends V> m) {
  43. comparator = null;
  44. putAll(m);
  45. }
  46. // 空参构造
  47. public TreeMap() {
  48. comparator = null;
  49. }
  50. .........
  51. }

TreeMap的关键是key要进行比较

  • key实现Compareable接口,重写compareTo方法
  • 自定义比较器实现Comparator

    public interface Comparator {

    1. // T一般是Map的key
    2. int compare(T o1, T o2);
    3. boolean equals(Object obj);
    4. }

1.增加元素

put()

put 大概的步骤:

  1. 如果红黑树根节点为空,新增节点直接作为根节点
  2. 寻找父节点

    • 优先使用外部比较器Comparator,没有时再使用key的compareTo方法
    • 找到自己应该挂在红黑树那个节点,通过比较大小即可,红黑树左边小,右边大
    • 如果key比较相等的话,直接赋值原来的值
  3. 新建节点到找到的父节点上
  4. 着色旋转

    public V put(K key, V value) {

    1. // 判断红黑树的节点是否为空,为空的话,新增的节点直接作为根节点
    2. Entry<K,V> t = root;
    3. // 红黑树根节点为空,直接新建
    4. if (t == null) {
    5. // key是不是null。
    6. // 外部比较器为空的话,也判断key有没有实现Comparable
    7. compare(key, key); // type (and possibly null) check
    8. root = new Entry<>(key, value, null);
    9. size = 1;
    10. modCount++;
    11. return null;
    12. }
    13. //-----------------------------寻找父节点-------------------------------------
    14. int cmp;
    15. Entry<K,V> parent;
    16. // split comparator and comparable paths
    17. Comparator<? super K> cpr = comparator;
    18. if (cpr != null) {
    19. // 如果有传入比较器
    20. // 找到key应该新增的位置,就是应该挂载那个节点的头上
    21. do {
    22. // 当结束时,parent就是t上次比过的对象,t的值和parent的值最接近
    23. parent = t;
    24. cmp = cpr.compare(key, t.key);
    25. // key小于t,把t左边的值赋予t,循环再比,红黑树左边的值比较小
    26. if (cmp < 0)
    27. t = t.left;
    28. // key大于t,把t右边的值赋予t,循环再比,红黑树右边的值比较大
    29. else if (cmp > 0)
    30. t = t.right;
    31. // 如果相等的话,直接覆盖原值
    32. else
    33. return t.setValue(value);
    34. // t 为空,说明已经到叶子节点了
    35. } while (t != null);
    36. }
    37. // 没有比较器,调用 key 的 compareTo 方法
    38. else {
    39. // 若key=null ,抛出异常
    40. if (key == null)
    41. throw new NullPointerException();
    42. @SuppressWarnings("unchecked")
    43. // 拿到key自己实现的比较器
    44. Comparable<? super K> k = (Comparable<? super K>) key;
    45. do {
    46. // 重复上面的步骤
    47. parent = t;
    48. cmp = k.compareTo(t.key);
    49. if (cmp < 0)
    50. t = t.left;
    51. else if (cmp > 0)
    52. t = t.right;
    53. else
    54. return t.setValue(value);
    55. } while (t != null);
    56. }
    57. //--------------------------------执行新增------------------------------------
    58. Entry<K,V> e = new Entry<>(key, value, parent);
    59. // cmp 代表最后一次对比的大小,小于 0 ,代表 e 在上一节点的左边
    60. if (cmp < 0)
    61. parent.left = e;
    62. // cmp 代表最后一次对比的大小,小于 0 ,代表 e 在上一节点的右边,相等的情况已经处理了。
    63. else
    64. parent.right = e;
    65. //--------------------------------旋转着色-------------------------------------
    66. fixAfterInsertion(e);
    67. size++;
    68. modCount++;
    69. return null;
    70. }

从源码中,我们可以看到

  • 新增节点时,就是利用了红黑树左小右大的特性,从根节点不断往下查找,直到找到节点是 null 为止,节点为 null 说明到达了叶子结点;
  • 查找过程中,发现 key 值已经存在,直接覆盖;

注:跟 HashMap 不同,TreeMap 是禁止 key 是 null 值的,因为需要根据 key 的大小关系来判断当前 Node 放到哪个位置。

2.2 获取元素:get & getEntry

get()

  1. public V get(Object key) {
  2. Entry<K,V> p = getEntry(key);
  3. return (p==null ? null : p.value);
  4. }

getEntry()

  1. final Entry<K,V> getEntry(Object key) {
  2. // Offload comparator-based version for sake of performance
  3. if (comparator != null)
  4. return getEntryUsingComparator(key);
  5. if (key == null)
  6. throw new NullPointerException();
  7. @SuppressWarnings("unchecked")
  8. Comparable<? super K> k = (Comparable<? super K>) key;
  9. // 通过简单的查询红黑树查找到
  10. Entry<K,V> p = root;
  11. while (p != null) {
  12. int cmp = k.compareTo(p.key);
  13. if (cmp < 0)
  14. p = p.left;
  15. else if (cmp > 0)
  16. p = p.right;
  17. else
  18. return p;
  19. }
  20. return null;
  21. }

总结对比

最后有一个问题,HashMap、TreeMap、LinkedHashMap 三者有什么相同点和不同点?

1)相同点:

  1. 三者在特定的情况下,都会使用红黑树;
  2. 底层的 hash 算法相同;
  3. 在迭代的过程中,如果 Map 的数据结构被改动,都会报 ConcurrentModificationException 的错误。

2)不同点:

  1. 底层数据结构不同

    1. HashMap 数据结构以数组为主,查询非常快;
    2. TreeMap 数据结构以红黑树为主,利用了红黑树左小右大的特点,可以实现 key 的排序;
    3. LinkedHashMap 在 HashMap 的基础上增加了链表的结构,实现了插入顺序访问和最少访问删除两种策略;
  2. 由于三种 Map 底层数据结构的差别,导致了三者的使用场景的不同

    1. TreeMap 适合需要根据 key 进行排序的场景
    2. LinkedHashMap 适合按照插入顺序访问,或需要删除最少访问元素的场景
    3. 剩余场景我们使用 HashMap 即可,我们工作中大部分场景基本都在使用 HashMap;
  3. 由于三种 map 的底层数据结构的不同,导致上层包装的 api 略有差别。

发表评论

表情:
评论列表 (有 0 条评论,305人围观)

还没有评论,来说两句吧...

相关阅读

    相关 TreeMap分析

    阅读本文章之前需要了解Comparator接口及Comparable接口的基本使用,推荐先阅读博主关于红黑树的讲解文章,传送地址:[快速理解红黑树,从二叉排序树 → AVL树

    相关 java分析05-TreeMap

    说什么王权富贵,坚持!      今天,我们来看下TreeMap集合。作为Map集合中的一员大将,她的职责还是很大的, 除了常见的存储键值对和快速查找,她还有很多技能,例如