← 返回题目列表

HashSet、LinkedHashSet、TreeSet 有什么区别?分别怎么选?

高频 中等 第 12 / 30 题 更新于 2026/07/26
SetHashSetLinkedHashSetTreeSet

简化版

三者都不允许重复元素,区别在底层结构和顺序HashSet 底层是 HashMap,无序,增删查 O(1),最常用;LinkedHashSet 是 HashSet + 一条双向链表,保持插入顺序,性能几乎不变;TreeSet 底层是 TreeMap(红黑树),按元素排序,增删查 O(log n),能做范围查询。选择口诀:不在乎顺序用 HashSet,要插入顺序用 LinkedHashSet,要排序/范围查询用 TreeSet。

详细版

三者都实现 Set 接口(去重),但底层各托一个 Map:

维度HashSetLinkedHashSetTreeSet
底层HashMapLinkedHashMapTreeMap(红黑树)
顺序无序插入顺序排序(自然序或 Comparator)
增删查O(1)O(1)O(log n)
null 元素允许一个允许一个不允许
去重依据hashCode + equalshashCode + equalscompareTo/compare
额外能力保持插入顺序first/last/floor/ceiling/subSet
Set<String> hash = new HashSet<>();        // 无序:遍历顺序不确定
Set<String> linked = new LinkedHashSet<>(); // 插入顺序:加入什么顺序,遍历就什么顺序
Set<String> tree = new TreeSet<>();         // 排序:遍历时按字典序

for (String s : List.of("banana", "apple", "cherry")) {
    hash.add(s); linked.add(s); tree.add(s);
}
// hash:   顺序不定,如 banana, cherry, apple
// linked: banana, apple, cherry      (插入顺序)
// tree:   apple, banana, cherry      (排序)

⚠️ 三个 Set 本质是「只用 key 的 Map」:Set 的元素就是 Map 的 key,value 填一个固定占位对象。所以它们的顺序、复杂度、去重规则,全部继承自对应的 Map。

完整版教学

一、核心认知:Set 都是「阉割版 Map」

理解这三个 Set 只需一句话:它们内部各自 new 了一个对应的 Map,把元素当 key 存进去,value 用同一个占位对象。

HashSet       → 内部 HashMap<E, Object>       元素当 key
LinkedHashSet → 内部 LinkedHashMap<E, Object>
TreeSet       → 内部 TreeMap<E, Object>
add(e)  → map.put(e, PRESENT)   // PRESENT 是一个 static final Object
contains(e) → map.containsKey(e)

Map 的 key 天然不重复,正好就是 Set「去重」的能力来源。所以 Set 三兄弟的所有差异(顺序、复杂度、null、去重依据),都是它们背后 HashMap / LinkedHashMap / TreeMap 差异的直接投影。记住这点,就不用单独背 Set,全从 Map 推导。

二、HashSet:无序但最快

HashSet 背后是 HashMap,元素放在哈希桶里,位置由 hash & (n-1) 决定,跟插入先后无关,所以无序——而且扩容 rehash 后顺序还会变。好处是增删查都是平均 O(1),是三者里最快、最省的,也是 90% 场景的默认选择。

去重依据是 hashCode + equals:先用 hashCode 定位桶,同桶内再用 equals 精判。所以放进 HashSet 的自定义对象必须正确重写 hashCode 和 equals,否则两个「逻辑相等」的对象会被当成不同元素,去重失效。

class Point { int x, y; }  // 没重写 hashCode/equals
Set<Point> set = new HashSet<>();
set.add(new Point());
set.add(new Point());
set.size();  // 2 !两个逻辑相同的点没被去重,因为默认按对象地址判等

三、LinkedHashSet:插入顺序的秘密

LinkedHashSet 继承 HashSet,底层换成 LinkedHashMap——在哈希桶之外,额外用一条双向链表把所有元素按插入顺序串起来。哈希桶负责 O(1) 定位,链表负责记住顺序,遍历时走链表,于是既有 O(1) 性能又能保持插入顺序。

HashSet 的桶(无序):       LinkedHashSet 额外的顺序链:
桶0: C                       head → A → B → C → D → tail
桶1: A                       (按加入先后串联,遍历走这条链)
桶2: D → B

代价只是每个节点多两个指针(before/after)和一点内存,时间复杂度仍是 O(1)。需要「去重 + 保持加入顺序」时它最合适,比如做有序去重、缓存最近访问列表。它不排序,只忠实记录你加入的先后。

四、TreeSet:有序与导航查询

TreeSet 背后是 TreeMap(红黑树),元素按排序组织,中序遍历即升序。代价是增删查都要沿树下行,O(log n),比 HashSet 慢。但它换来了 HashSet 给不了的能力——范围查询和最接近查询

TreeSet: [10, 20, 30, 40, 50]
first()      → 10        last()       → 50
floor(35)    → 30        ceiling(35)  → 40    (≤35 最大 / ≥35 最小)
lower(30)    → 20        higher(30)   → 40    (严格小于 / 大于)
headSet(30)  → [10,20]   subSet(20,40)→ [20,30]

去重依据是 compareTo/compare 返回 0(不看 equals!),元素必须可比较(实现 Comparable 或传 Comparator),且不允许 null(没法和别的元素比较大小)。这跟 HashSet 用 hashCode/equals 判重是本质区别——若 Comparator 写得让两个不同元素 compare 返回 0,它们会被当成重复而丢一个。

五、三者复杂度与顺序对照

操作HashSetLinkedHashSetTreeSet
add/remove/containsO(1)O(1)O(log n)
遍历顺序不确定插入顺序排序
找最小/最大O(n)(要扫全部)O(n)O(log n)(first/last)
范围查询不支持不支持O(log n)
内存开销最小中(多一条链表)较大(红黑树节点)

一个数字直觉:往三者各放 100 万个元素并做 100 万次 contains,HashSet 最快,LinkedHashSet 略慢(多维护链表),TreeSet 明显慢(每次 log n 次比较)。所以不需要顺序就别用 TreeSet,log n 的常数在大数据量下拖累明显。

六、选型决策与常见搭配

需要去重吗?──否──▶ 用 List
   │是
是否需要顺序?
   ├─ 不在乎顺序 ──────────▶ HashSet(默认,最快)
   ├─ 要「加入的先后」顺序 ──▶ LinkedHashSet
   └─ 要「排序 / 范围查询」──▶ TreeSet

实战:给列表去重又想保持原顺序,用 new LinkedHashSet<>(list);要去重后排序,用 new TreeSet<>(list);只是判断「有没有见过」,HashSet 足矣。

记忆钩子:「Hash 无序最快、Linked 记住插入顺序、Tree 帮你排序」——一个背后 HashMap、一个 LinkedHashMap、一个 TreeMap,Set 只是它们的 keySet。

七、常见误区与追问

  • 误区:HashSet 的元素有固定顺序。 完全无序,且扩容后顺序还会变;要顺序请用 LinkedHashSet 或 TreeSet。
  • 误区:TreeSet 判重用 equals。 用的是 compareTo/compare 返回 0;Comparator 写不好会把不同元素误判为重复而丢数据。
  • 误区:三个 Set 都能存 null。 HashSet、LinkedHashSet 允许一个 null;TreeSet 不允许(null 无法比较大小,抛 NPE)。
  • 误区:LinkedHashSet 是排序的。 它是插入顺序,不是排序;排序只有 TreeSet。
  • 追问:往 HashSet 放自定义对象要注意什么? 必须正确重写 hashCode 和 equals,且保持一致,否则去重失效;用不可变对象当元素更安全。
  • 追问:TreeSet 的元素没实现 Comparable 会怎样? 若也没传 Comparator,add 时抛 ClassCastException;需让元素实现 Comparable 或构造时传 Comparator。
  • 追问:如何给 List 去重并保持原顺序? new ArrayList<>(new LinkedHashSet<>(list));用 HashSet 会打乱顺序,用 TreeSet 会变成排序。

八、加强记忆

把 Set 三兄弟记成「三个只留 key 的 Map」:HashSet 托 HashMap,哈希桶定位、无序、增删查 O(1)、按 hashCode+equals 去重,是默认首选;LinkedHashSet 托 LinkedHashMap,在桶之外多挂一条双向链表记录插入先后,于是「去重 + 保持加入顺序」且性能几乎不变;TreeSet 托 TreeMap 红黑树,按大小排序、O(log n)、按 compare 去重且不容 null,唯一能做 first/last/floor/ceiling/subSet 等范围与最接近查询。选型就一句「不在乎顺序 HashSet、要插入顺序 LinkedHashSet、要排序或范围 TreeSet」;所有细节都从背后的 Map 推导,不用死记。