故事:你的“智能去重排序书架”(TreeSet<E>)​

7 阅读6分钟

这次我们把 Java 的 TreeSet 想象成一个 ​​“自动去重且智能排序的魔法书架”​​ 📚。这篇文章揭秘的就是这个书架如何利用 TreeMap(我们之前讲过的红黑树魔法图书馆)的核心能力,实现元素唯一且自动排序的功能。


​故事主角:你的“智能去重排序书架”(TreeSet<E>)​

  1. ​书架的本质:一个“伪装”的图书馆 (TreeMap 的华丽马甲)​

    • 最核心的秘密:TreeSet ​​自己几乎不存东西​​!它内部藏着一个 TreeMap<E, Object>

    • 怎么存元素?当你往书架 (TreeSet) 上放一本书(元素 e)时:

      • 书架管理员 (TreeSet) 会拿着这本书 e,跑到它内部的魔法图书馆 (TreeMap)。
      • 对图书馆管理员 (TreeMap) 说:“请把这本书 e 作为​​键(Key)​​ 存起来,对应的​​值(Value)​​ 就用这个固定的 PRESENT 小书签📑(一个静态空对象)”。
    • ​源码证据:​

      java
      Copy
      // TreeSet 的核心属性:一个 NavigableMap (实际就是 TreeMap)
      private transient NavigableMap<E, Object> m;
      // 那个固定的“小书签”值
      private static final Object PRESENT = new Object();
      
      // 添加元素 (add)
      public boolean add(E e) {
          return m.put(e, PRESENT) == null; // 关键!调用 TreeMap 的 put
      }
      
    • ​为什么这么设计?​

      • ​去重:​​ TreeMap 的键 (Key) 是唯一的。如果 e 已经作为键存在图书馆里了,put(e, PRESENT) 会返回旧的 PRESENT (非 null),add 方法就知道重复了,返回 false。如果不存在,put 返回 nulladd 返回 true
      • ​排序:​​ TreeMap 天然就会根据键 (e) 的自然顺序或比较器 (Comparator) 来排序键。TreeSet 继承了这种排序能力,因为它遍历的就是 TreeMap 的键。
      • ​高效:​​ 直接复用 TreeMap 强大的红黑树实现(插入、删除、查找都是 O(log n)),TreeSet 自己只需要做简单的包装。
  2. ​书架管理员 (TreeSet) 的日常工作 (核心操作揭秘)​

    • ​上架新书 (add(e)):​

      • 如上所述,本质是调用 TreeMap.put(e, PRESENT)
      • 红黑树的魔法(找位置、插入、变色、旋转保持平衡)都在 TreeMap.put 里发生。TreeSet 只关心返回值是不是 null 来判断是否成功(是否重复)。
    • ​下架旧书 (remove(o)):​

      • 本质是调用 TreeMap.remove(o)
      • 如果 TreeMap.remove(o) 返回了 PRESENT(说明原来有这个键),TreeSet.remove 就返回 true(成功移除)。如果返回 null,说明没这本书,返回 false
      • 红黑树的删除和再平衡魔法也在 TreeMap.deleteEntry 里完成。
      java
      Copy
      public boolean remove(Object o) {
          return m.remove(o) == PRESENT; // 关键!调用 TreeMap 的 remove
      }
      
    • ​检查书架是否有某本书 (contains(o)):​

      • 本质是问图书馆 (TreeMap):“你有这个键 (o) 吗?” (TreeMap.containsKey(o))。
      • TreeMap.containsKey 内部使用高效的二叉查找 (O(log n)) 在红黑树中查找。
      java
      Copy
      public boolean contains(Object o) {
          return m.containsKey(o); // 关键!调用 TreeMap 的 containsKey
      }
      
    • ​数书 (size()) 和 检查空书架 (isEmpty()):​

      • 直接问图书馆 (TreeMap) 有多少个键值对 (size) 或者是不是空的 (isEmpty)。
      java
      Copy
      public int size() {
          return m.size();
      }
      public boolean isEmpty() {
          return m.isEmpty();
      }
      
    • ​按顺序浏览所有书 (iterator()):​

      • 本质是获取 TreeMap 的​​键集合​​ (navigableKeySet()) 的迭代器。
      • 这个迭代器会按照键(也就是 TreeSet 的元素)的排序顺序(从小到大)遍历。
      • 迭代器还支持 remove,调用它实际是调用 TreeMap 的删除。
      java
      Copy
      public Iterator<E> iterator() {
          return m.navigableKeySet().iterator(); // 关键!获取 TreeMap 键的迭代器
      }
      
  3. ​书架的智能导航功能 (来自 NavigableSet 接口)​

    • 书架管理员 (TreeSet) 还精通导航术!它能快速找到:

      • ​第一本书 (first())​​:书架最左边(最小)的书。调用 TreeMap.firstKey()
      • ​最后一本书 (last())​​:书架最右边(最大)的书。调用 TreeMap.lastKey()
      • ​比某本书 e 小的最大书 (lower(e))​​:调用 TreeMap.lowerKey(e)
      • ​比某本书 e 小或等于的最大书 (floor(e))​​:调用 TreeMap.floorKey(e)
      • ​比某本书 e 大或等于的最小书 (ceiling(e))​​:调用 TreeMap.ceilingKey(e)
      • ​比某本书 e 大的最小书 (higher(e))​​:调用 TreeMap.higherKey(e)
      • ​拿走并返回第一本书 (pollFirst())​​:调用 TreeMap.pollFirstEntry().getKey()
      • ​拿走并返回最后一本书 (pollLast())​​:调用 TreeMap.pollLastEntry().getKey()
    • ​按范围找书 (subSetheadSettailSet):​

      • 比如想找书名在 "Apple" 到 "Orange" 之间的所有书(不去动原书架)。
      • TreeSet 调用 TreeMap.subMap(...) 得到一个 SubMap(图书馆的某个区域视图)。
      • 然后用这个 SubMap 创建一个​​新的​​ TreeSet 书架。这个新书架​​不是​​复制书籍,而是​​指向​​原图书馆的那个特定区域!非常高效。
      java
      Copy
      public NavigableSet<E> subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive) {
          return new TreeSet<>(m.subMap(fromElement, fromInclusive, toElement, toInclusive)); // 创建视图
      }
      
  4. ​书架的规矩与特点​

    • ​元素必须可比 (Comparable 或 Comparator):​​ 和 TreeMap 的键一样,放进 TreeSet 的元素要么实现 Comparable 接口,要么在创建 TreeSet 时提供一个 Comparator。否则书架管理员不知道怎么排序,会懵 (ClassCastException)。

    • ​拒绝重复 (Set 特性):​​ 相同的书只能放一本(基于 equals 判断)。

    • ​性能 (O(log n)):​​ 得益于底层的红黑树 (TreeMap),添加、删除、查找、获取极值(first/last)等操作平均都是 O(log n) 的时间复杂度。比 HashSet (O(1)) 慢,但换来了自动排序和强大的导航能力。

    • ​内存开销:​​ 每个元素在 TreeMap 中对应一个节点,节点存储键(元素)、值(PRESENT)、父节点、左孩子、右孩子、颜色。比 HashSet 的 Entry(键、值、哈希、下一个)通常开销略大。

    • ​非线程安全:​​ 和 TreeMap 一样,这个书架设计给单线程使用。如果多个线程同时上书下书,书架可能会乱套。解决方案:

      • ​加锁 (synchronized):​​ 用 Collections.synchronizedSortedSet(new TreeSet<>()) 包装。
      • ​用并发书架 (ConcurrentSkipListSet):​​ 基于跳表实现,线程安全且有序。
  5. ​什么时候用这个智能书架 (TreeSet)?​

    • ​需要自动排序时:​​ 比如存储学生成绩对象并按分数从高到低排。
    • ​需要去重且排序时:​​ 比如从一堆杂乱数字中提取唯一值并从小到大输出。
    • ​需要频繁查找“邻居”元素时:​​ 比如在一个时间线事件集合中,快速找到某个时间点之前或之后最近的事件 (floorceiling)。
    • ​需要范围查询时:​​ 比如查询价格在 100 到 200 之间的所有商品 (subSet)。

​总结一下,这篇文章讲的就是:​

  1. TreeSet 是什么?​​ 一个基于 ​TreeMap (红黑树)​​ 实现的、​​元素唯一​​且​​自动排序​​的集合 (Set)。

  2. ​核心魔法:借力 TreeMap​。TreeSet 自己是个“薄薄的外壳”,它把元素 (E) 当作 TreeMap 的​​键(Key)​​ 存储,并用一个固定的 PRESENT 对象作为​​值(Value)​​。所有核心功能(排序、去重、查找、导航)都委托给 TreeMap 完成。

  3. ​排序规则:​​ 必须提供元素的 ​​自然顺序 (Comparable)​​ 或 ​​自定义比较器 (Comparator)​​。

  4. ​核心操作原理:​

    • add(e) -> TreeMap.put(e, PRESENT) (利用键唯一性去重)
    • remove(o) -> TreeMap.remove(o) (检查返回值是否是 PRESENT)
    • contains(o) -> TreeMap.containsKey(o) (高效二叉查找)
    • first/last/lower/floor/ceiling/higher -> 调用对应的 TreeMap.firstKey/lastKey/lowerKey/floorKey/ceilingKey/higherKey
    • subSet/headSet/tailSet -> 创建基于 TreeMap 子映射 (SubMap) 的 TreeSet ​​视图​​ (高效)
    • iterator() -> 获取 TreeMap 键集合的迭代器 (按顺序遍历)
  5. ​关键特性:​

    • ​有序且唯一。​
    • ​非线程安全。​
    • ​元素必须可比 (Comparable/Comparator)。​
    • 性能:核心操作平均 O(log n)。比 HashSet (O(1)) 慢,但比无序的 List 查找 (O(n)) 快得多,且自带排序。
    • 空间:比 HashSet 开销略大 (红黑树节点结构)。
  6. ​为什么用它?​​ 当你需要 ​​元素唯一​​ 且 ​​自动排序​​,或者需要 ​​高效的极值/邻居查找/范围查询​​ 时,TreeSet 是你的首选!

​一句话记住 TreeSet:它就像一个聪明又省事的图书管理员,内部雇佣了红黑树魔法图书馆 (TreeMap) 来干活,自己只负责保证你放的书不重复,并且总是按顺序摆得整整齐齐,还能快速帮你找到任何位置的书!​​ 📚✨ 下次需要有序且唯一的集合,就交给 TreeSet 吧!