【Java面试经典】谈谈你用过的集合(二)Set

99 阅读5分钟

上一篇我们讲到了List,今天来讲一下Set 其实这俩货最重要的区别就是List允许重复,Set不行,以及List可以根据索引直接访问,Set就不行了,接下来我们详细说说Set的存储结构、操作特点以及适用场景等

1. 存储结构(基于 JDK 源码)

  • HashSet

    • 在 JDK 源码中,HashSet 内部是通过一个 HashMap 来实现的。它本质上是对 HashMap 的简单包装。当创建一个 HashSet 时,会初始化一个内部的 HashMap。例如,以下是 HashSet 部分源码中的构造函数:

private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public HashSet() {
    map = new HashMap<>();
}
  • 可以看到,HashSet 中的元素被存储在这个内部的 HashMap 的键(key)部分,而值(value)部分都使用一个固定的虚拟对象(PRESENT)。这种存储方式利用了 HashMap 基于哈希表的存储结构来实现元素的快速存储和查找。元素通过哈希函数计算存储位置,在发生哈希冲突时,通过链表(Java 8 后部分情况会转换为红黑树)来解决。

  • TreeSet

    • TreeSet 内部是基于 TreeMap 实现的。TreeSet 的元素存储在 TreeMap 的键(key)位置,而值(value)部分使用了一个固定的PRESENT对象(类似于 HashSet 对 HashMap 的使用方式)。在 TreeMap 内部,是一个红黑树(Red - Black Tree)结构。红黑树的节点(Entry)包含键、值、指向左子树和右子树的指针以及颜色标记,用于维护红黑树的平衡特性。从 TreeSet 的部分源码构造函数可以看出这种关系:
private transient NavigableMap<E,Object> m;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
TreeSet(NavigableMap<E, Object> m) {
    this.m = m;
}
public TreeSet() {
    this(new TreeMap<E,Object>());
}
  • 当向 TreeSet 中添加元素时,会根据元素的自然顺序(如果元素实现了Comparable接口)或者通过自定义比较器(在 TreeSet 构造函数中指定)来确定元素在红黑树中的位置,从而实现元素的有序存储。

2. 操作特点(基于 JDK 源码)

-   **HashSet**    -   **添加(add)操作**:在源码中,HashSet 的`add`方法实际上是调用了内部 HashMap 的`put`方法,将元素作为键,`PRESENT`作为值放入 HashMap。因为 HashMap 的`put`操作会先计算键的哈希值来确定存储位置,所以添加操作的时间复杂度平均为 O (1)。但在哈希冲突严重的情况下,需要遍历链表或者红黑树来检查元素是否已经存在,时间复杂度可能退化为 O (n)。
    -   **删除(remove)操作**:HashSet 的`remove`方法也是调用内部 HashMap 的`remove`方法。它先通过元素的哈希值找到存储位置,然后检查该位置的元素是否与要删除的元素相等(通过`equals`方法)。如果相等,则删除。时间复杂度平均为 O (1),最坏情况为 O (n)。
    -   **包含(contains)操作**:同样是调用内部 HashMap 的`containsKey`方法,先计算哈希码找到存储位置,再检查元素是否存在。时间复杂度平均为 O (1),最坏情况为 O (n)。

-   **TreeSet**    -   **添加(add)操作**:在源码中,TreeSet 的`add`方法调用内部 TreeMap 的`put`方法。由于 TreeMap 是基于红黑树的,添加元素时需要在红黑树中找到合适的插入位置,这涉及到比较元素大小并沿着树的路径进行查找。插入后还需要进行平衡调整,所以时间复杂度为 O (log n)。
    -   **删除(remove)操作**:TreeSet 的`remove`方法调用内部 TreeMap 的`remove`方法。它先在红黑树中找到要删除的元素,然后进行删除操作并调整树的结构以保持平衡,时间复杂度为 O (log n)。
    -   **包含(contains)操作**:调用内部 TreeMap 的`containsKey`方法,在红黑树中查找元素是否存在,通过比较元素大小沿着树的路径查找,时间复杂度为 O (log n)。

3. 适用场景(基于操作特点和存储结构)

-   **HashSet**    -   适用于需要快速判断元素是否存在,以及对元素进行去重的场景。例如,在一个大型数据集中检查某个元素是否已经出现过,或者从一个包含重复数据的集合中快速去除重复元素。由于其底层基于哈希表,平均时间复杂度较低,在这种对性能要求较高的查找和去重场景中表现出色。
    -   在实现简单的缓存标记场景中也很有用。例如,标记已经处理过的任务或者已经访问过的资源,将任务 ID 或者资源标识符存储在 HashSet 中,方便快速检查是否已经处理或访问。

-   **TreeSet**    -   当需要对元素进行排序并存储,且需要按照顺序进行遍历或者范围查询时,TreeSet 是很好的选择。例如,在统计排名数据时,将成绩或者分数存储在 TreeSet 中,能够自动按照大小顺序排列,方便查询排名情况或者某个分数区间内的元素数量。
    -   对于需要维护一个有序的元素集合,并且在添加、删除和查找操作时能够保持较好的时间复杂度平衡的场景,如实现一个简单的有序字典或者配置参数集合,按照参数名称的顺序存储和操作,TreeSet 可以提供高效的解决方案。


-   **HashSet 与 TreeSet的异同**    -   从 JDK 源码可以看出,两者都不允许存储重复元素。HashSet 通过 HashMap 的键的唯一性来保证,TreeSet 通过 TreeMap 的键的唯一性来保证。
    -   都没有像 List 那样通过索引访问元素的方式。HashSet 的存储基于哈希表,不保证元素的顺序;TreeSet 的存储基于红黑树,元素是按照自然顺序或自定义比较器的顺序存储的。

好,以上是Set部分,下一篇我们讲一下Map