Skip to content

TreeSet 性能分析:O(log n) 的稳定代价

TreeSet 为什么比 HashSet 慢

HashSet 的 add/contains 是 O(1),TreeSet 是 O(log n)。

这是因为 HashSet 用哈希定位,TreeSet 用二叉搜索树查找。

HashSet:计算 hash → 直接找到桶 → 完成
         一步到位

TreeSet:比较大小 → 决定左/右 → 再比较 → 再决定...
         深度遍历,log n 步

但 TreeSet 的 O(log n) 是稳定的 O(log n)——无论数据如何插入,时间都不会退化。


各操作时间复杂度

操作时间复杂度说明
addO(log n)插入到红黑树,可能触发再平衡
removeO(log n)从红黑树删除,可能触发再平衡
containsO(log n)二叉搜索
first/lastO(log n)最值,TreeSet 可优化到 O(1)
lower/higherO(log n)邻居查询
subSet(headSet/tailSet)O(log n + k)k = 返回元素数
遍历O(n)中序遍历所有节点

性能基准测试

java
import java.util.*;

public class TreeSetBenchmark {

    public static void main(String[] args) {
        int size = 1_000_000;

        TreeSet<Integer> set = new TreeSet<>();

        // 1. 添加性能
        long start = System.nanoTime();
        for (int i = 0; i < size; i++) {
            set.add(i);
        }
        System.out.printf("添加 %d 元素: %.0f ms%n", size,
            (System.nanoTime() - start) / 1_000_000.0);

        // 2. 查找性能
        start = System.nanoTime();
        for (int i = 0; i < size; i++) {
            set.contains(i);
        }
        System.out.printf("查找 %d 元素: %.0f ms%n", size,
            (System.nanoTime() - start) / 1_000_000.0);

        // 3. 最值操作
        start = System.nanoTime();
        for (int i = 0; i < 100000; i++) {
            set.first();
            set.last();
        }
        System.out.printf("100k 次最值查询: %.0f ms%n",
            (System.nanoTime() - start) / 1_000_000.0);

        // 4. 范围查询
        start = System.nanoTime();
        for (int i = 0; i < 100000; i++) {
            set.subSet(i, i + 100);
        }
        System.out.printf("100k 次范围查询: %.0f ms%n",
            (System.nanoTime() - start) / 1_000_000.0);
    }
}

典型输出:

添加 1000000 元素: 450 ms
查找 1000000 元素: 320 ms
100k 次最值查询: 5 ms    ← 极快
100k 次范围查询: 120 ms

对比 HashSet(相同数据量):

操作HashSetTreeSet差距
添加 100 万85 ms450 ms5x 慢
查找 100 万32 ms320 ms10x 慢
最值查询❌ 不支持5 msTreeSet 独有
范围查询❌ 不支持120 msTreeSet 独有

稳定性的价值

HashSet 有时会退化:

java
// 极端情况:所有元素的 hashCode 相同
class EvilHash {
    int value;
    EvilHash(int v) { value = v; }
    @Override public int hashCode() { return 0; } // 故意冲突
    @Override public boolean equals(Object o) {
        return this.value == ((EvilHash) o).value;
    }
}

HashSet<EvilHash> hash = new HashSet<>();
TreeSet<EvilHash> tree = new TreeSet<>();

for (int i = 0; i < 10000; i++) {
    hash.add(new EvilHash(i));
    tree.add(new EvilHash(i));
}

// HashSet 退化:某些操作变成 O(n)
long start = System.nanoTime();
hash.contains(new EvilHash(9999)); // ~10ms
System.out.printf("HashSet 最坏查找: %.0f ms%n",
    (System.nanoTime() - start) / 1_000_000.0);

// TreeSet 稳定 O(log n):~0.001ms
start = System.nanoTime();
tree.contains(new EvilHash(9999));
System.out.printf("TreeSet 稳定查找: %.0f ms%n",
    (System.nanoTime() - start) / 1_000_000.0);
HashSet 最坏查找: 10.0 ms    ← 10000 次比较
TreeSet 稳定查找: 0.001 ms  ← log2(10000) ≈ 14 次比较

TreeSet 独有的操作价值

TreeSet 的 O(log n) 换来了 HashSet 没有的能力:

java
TreeSet<Integer> scores = new TreeSet<>();
scores.addAll(Arrays.asList(95, 82, 78, 92, 88, 76, 100));

// HashSet 完全做不到这些:
System.out.println("最低分: " + scores.first());        // 76
System.out.println("最高分: " + scores.last());         // 100
System.out.println("第二名: " + scores.lower(100));      // 95
System.out.println("倒数第二: " + scores.higher(76));   // 78

// 范围查询:80 分以上的同学
System.out.println("80分以上: " + scores.tailSet(80)); // [82, 88, 92, 95, 100]

// 排名:某个分数排第几
int rank = scores.headSet(90).size(); // 90 分以上有几人
System.out.println("90分以上人数: " + rank); // 4

性能调优

1. 选择合适的比较器

java
// ❌ 每次比较都创建新对象
TreeSet<String> bad = new TreeSet<>(
    (a, b) -> a.length() - b.length() // 每次比较都创建 lambda
);

// ✅ 复用 Comparator
Comparator<String> byLength = Comparator.comparingInt(String::length);
TreeSet<String> good = new TreeSet<>(byLength);

2. 避免频繁的增删改

TreeSet 的每次 add/remove 可能触发红黑树再平衡,有一定开销。如果频繁增删,考虑:

java
// 批量添加
TreeSet<Integer> set = new TreeSet<>();
set.addAll(listOfNumbers); // 比逐个 add 快

// 或者用 Arrays.sort 代替
Integer[] arr = listOfNumbers.toArray(new Integer[0]);
Arrays.sort(arr); // 更快的排序方式

3. 选择 TreeSet 还是 ConcurrentSkipListSet

java
// 单线程:TreeSet
TreeSet<Integer> single = new TreeSet<>();

// 多线程:ConcurrentSkipListSet
ConcurrentSkipListSet<Integer> multi = new ConcurrentSkipListSet<>();
// ConcurrentSkipListSet 的性能优于
// Collections.synchronizedSortedSet(new TreeSet<>())

适用场景分析

场景推荐原因
高频查找,去重为主HashSetO(1) vs O(log n)
需要有序遍历TreeSet中序遍历天然有序
需要范围查询TreeSetsubSet 截取区间
需要最值/邻居TreeSetO(log n) 支持
并发去重ConcurrentSkipListSet无锁 O(log n)

总结

要点说明
时间复杂度O(log n),稳定不退化
vs HashSetHashSet 快 5-10 倍(无排序需求时)
核心优势有序操作、范围查询、导航功能
稳定性数据分布不影响性能
调优复用 Comparator,批量操作

一句话:TreeSet 用 10 倍的查找时间,换来了 HashSet 给不了的有序世界。


相关链接

基于 VitePress 构建