TreeMap的put源码详解
1 | 1.TreeMap中每一个节点的内部属性 |
TreeMap 是一种基于红黑树实现的 NavigableMap 接口的 Map 实现类,它保证了键值对的顺序是有序的。TreeMap 中的每个节点是一个 Entry,而它们之间通过红黑树的结构来连接,保证了在插入、删除、查找等操作时的对数时间复杂度。
1. TreeMap 节点的内部属性
每个节点(Entry)包含以下属性:
K key:节点的键。V value:节点的值。Entry<K,V> left:左子节点。Entry<K,V> right:右子节点。Entry<K,V> parent:父节点。boolean color:节点的颜色,红黑树会使用红色和黑色来平衡树的结构。
2. TreeMap 类中的一些成员变量
Comparator<? super K> comparator:用于比较键的比较器。默认情况下,TreeMap会使用自然顺序(即Comparable接口),如果传入了自定义比较器,则使用该比较器。Entry<K,V> root:根节点,用于存储树的最上层节点。int size:TreeMap中元素的数量。
3. 构造方法
- 空参构造:
public TreeMap() { comparator = null; // 默认使用自然顺序比较键 } - 带参构造:
public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; // 使用自定义的比较器 }
4. 添加元素
TreeMap 的 put 方法用来插入元素。我们首先来看 put(K key, V value) 的实现:
1 | public V put(K key, V value) { |
这个方法会调用下面的 put(K key, V value, boolean replaceOld) 方法。第二个参数 replaceOld 表示当键重复时是否覆盖值。
put 方法的核心部分:
-
首先判断是否是空树:
如果根节点为空(即root == null),说明这是第一次添加元素,此时直接调用addEntryToEmptyMap将新节点添加为根节点。 -
非空树的情况
:
如果树不为空,会进入循环判断新节点应该插入的位置。插入的位置由比较器或者自然顺序决定:
- 如果使用了比较器,就通过
comparator.compare()来比较。 - 如果没有比较器,则使用键的
compareTo方法进行比较。
- 如果使用了比较器,就通过
-
键是否已存在
:
- 如果找到相同的键,且
replaceOld为true,则覆盖旧的值;否则,保持原值不变。
- 如果找到相同的键,且
-
插入新节点:
如果没有找到相同的键,则会调用addEntry方法将新节点插入到合适的位置,并调用fixAfterInsertion方法来进行红黑树的平衡操作。
5. 插入节点方法
1 | private void addEntry(K key, V value, Entry<K, V> parent, boolean addToLeft) { |
该方法会创建一个新的 Entry 对象,并将其插入到父节点的左子节点或右子节点。插入后,需要调用 fixAfterInsertion 来确保红黑树的平衡。
6. 红黑树的平衡(fixAfterInsertion)
在 TreeMap 中,红黑树的平衡操作是通过 fixAfterInsertion 来完成的。此方法确保树的红黑性质在插入节点后得到修复。
红黑树的修复遵循如下规则:
- 节点的颜色初始为红色。
- 如果父节点是红色的,则需要进行调整。
- 情况1:父节点是左子节点,叔叔节点是红色,则将父节点和叔叔节点颜色设置为黑色,祖父节点设置为红色,并将当前节点向上移动。
- 情况2:父节点是左子节点,叔叔节点是黑色,当前节点是右子节点,则需要左旋父节点。
- 情况3:父节点是右子节点,叔叔节点是红色,则处理方式与情况1类似;父节点是右子节点,叔叔节点是黑色,当前节点是左子节点,则需要右旋父节点。
每次修复完成后,根节点的颜色会被设置为黑色。
7. 旋转操作
红黑树的旋转操作用于调整树的结构,常见的旋转操作包括左旋和右旋。这些操作有助于保持树的平衡。
- 左旋:如果当前节点是父节点的右子节点且不满足红黑树的性质,就需要进行左旋。
- 右旋:如果当前节点是父节点的左子节点且不满足红黑树的性质,就需要进行右旋。
这些旋转操作的目的是保持树的平衡,使得树的高度不会过高,从而保证查找、插入和删除操作的时间复杂度为 O(log N)。
8.课堂思考问题
8.1 TreeMap 添加元素时,键是否需要重写 hashCode 和 equals 方法?
答案:不需要。
解析:
TreeMap是基于红黑树实现的,元素的排序是根据键的比较结果来决定的,不是基于键的哈希值。所以在TreeMap中,并不需要重写hashCode和equals方法。TreeMap会通过Comparator(如果传入了比较器)或者键实现的Comparable接口来决定键的顺序,而不是依赖于哈希值。- 这与
HashMap不同,HashMap是基于哈希表实现的,依赖于hashCode和equals方法来定位键的位置。
8.2 HashMap 是哈希表结构的,JDK 8 开始由数组、链表、红黑树组成的。既然有红黑树,HashMap 的键是否需要实现 Comparable 接口或者传递比较器对象呢?
答案:不需要。
解析:
HashMap内部使用的是哈希值来决定元素的存储位置。哈希值是在hashCode方法中计算的,所以不需要使用Comparable接口或者传递比较器。- 在
HashMap中,红黑树的引入是为了优化链表的查找性能。当哈希冲突发生时,如果链表长度超过某个阈值(8),链表就会转换成红黑树。 - 红黑树的引入仅仅是为了提高插入和查找的效率,特别是在哈希冲突严重时,转换为红黑树会使得查找的复杂度从 O(n) 降低到 O(log n)。
- 由于
HashMap只关心哈希值和equals方法来确定键的位置,所以并不需要Comparable接口或比较器对象来进行排序。
8.3 TreeMap 和 HashMap 谁的效率更高?
答案:HashMap 的效率一般更高,但在特定的情况下,TreeMap 可能会更高。
解析:
HashMap:基于哈希表实现,平均情况下put、get操作的时间复杂度为 O(1)。但在最坏情况下,如果发生严重的哈希冲突(例如所有元素都映射到同一个桶),就会退化成链表,时间复杂度为 O(n)。TreeMap:基于红黑树实现,所有操作(put、get等)的时间复杂度都是 O(log n)。即使哈希冲突严重,TreeMap也能保持稳定的性能。
在最坏情况下,如果 TreeMap 中的节点构成链表(例如所有元素的比较结果相同),其效率会比 HashMap 更高,但这种情况非常少见。一般情况下,HashMap 的效率更高,特别是在没有严重哈希冲突的情况下。
8.4 你觉得在 Map 集合中,Java 会提供一个如果键重复了,不会覆盖的 put 方法吗?
答案:putIfAbsent 本身并不重要,关键是传递了“如果键重复了,是否覆盖”的思想。
解析:
putIfAbsent方法的作用是:如果键不存在,则插入键值对;如果键已存在,则不做任何操作。这可以避免键重复时覆盖值的情况。- 这个问题的背后,实际上是在思考如何控制逻辑中的“覆盖”行为。在实际编程中,我们经常需要根据不同的需求来决定是否允许覆盖现有值,
putIfAbsent提供了一种不覆盖的方案。 - 这里提到的“代码中的逻辑都有两面性”,可以理解为在实际开发中,我们常常需要控制一些操作的“行为”,而
boolean和int类型的变量常常用来控制这种行为的不同状态。比如replaceOld参数控制了是否替换旧值,而putIfAbsent则是控制是否仅在键不存在时插入值。
8.5 三种双列集合,如何选择?(HashMap,LinkedHashMap,TreeMap)
答案:选择不同的集合取决于你的需求:
-
HashMap:
- 默认选择:当我们只关心元素的插入和查询效率,并不关心元素的顺序时,
HashMap是最合适的选择。 - 效率:
HashMap的查询和插入操作的时间复杂度是 O(1),适用于大多数场景。
- 默认选择:当我们只关心元素的插入和查询效率,并不关心元素的顺序时,
-
LinkedHashMap:
- 当需要保证元素的插入顺序时使用:
LinkedHashMap保证了插入顺序或访问顺序(通过构造器指定)。在某些场景下,我们需要根据插入顺序或访问顺序来处理元素,LinkedHashMap便提供了这一功能。 - 适用场景:如缓存实现、LRU(最近最少使用)缓存、处理顺序敏感的数据时。
- 当需要保证元素的插入顺序时使用:
-
TreeMap:
- 当需要保证元素的顺序时使用:
TreeMap会按照键的自然顺序或通过传入的比较器进行排序。如果需要一个排序的映射,TreeMap是最好的选择。 - 适用场景:如实现排序后的数据存储,或者需要按照某种规则对元素进行顺序遍历时。
- 当需要保证元素的顺序时使用:
总结:
TreeMap用于需要顺序排序的场景。LinkedHashMap用于需要保持元素插入顺序的场景。HashMap用于大多数普通的Map使用场景,提供了最好的性能(除非有特殊的顺序需求)。
这就是对课堂思考问题的详细解答,希望对你有帮助!如果有任何问题,随时向我提问!
9.总结
TreeMap 是通过红黑树来维护元素的有序性,并保证了操作的对数时间复杂度。它提供了一个有序的 Map,并且允许根据自然顺序或者自定义比较器进行排序。插入元素时,红黑树的平衡操作确保了树的高度不会过高,从而保持较高的性能。
如果你对其中某个部分的实现有疑问,或者希望我更详细地解释某个步骤,随时告诉我!