概述
-
在数据结构中,集合(Set) 是一种无序、不重复 的元素集合,核心特性是"去重"和"快速判断元素是否存在",底层通常基于哈希表(散列表) 或红黑树实现(哈希表实现的集合称为"哈希集合",红黑树实现的称为"有序集合")。
-
集合与我们之前讲的字典(Dictionary) 关系密切:集合本质是"只存键、不存值"的字典,利用字典"键唯一"的特性实现去重,同时继承了哈希表的高效查找能力。
-
资料:
https://pan.quark.cn/s/43d906ddfa1b
一、集合的核心特点
- 元素唯一:集合中不会有重复元素,重复插入的元素会被自动忽略;
- 无序性:元素的存储顺序与插入顺序无关(哈希集合),有序集合(如 TreeSet)会按元素大小排序;
- 高效操作:判断元素是否存在、插入、删除的平均时间复杂度为 O(1)(哈希集合);
- 元素不可变:集合中的元素必须是不可变类型(如整数、字符串、元组),不能存储列表、字典等可变类型(否则无法保证哈希值稳定)。
二、集合的基本操作
集合的操作围绕"元素的增删查去重"展开,核心操作如下:
| 操作 | 描述 | 时间复杂度(哈希集合) |
|---|---|---|
add(element) |
向集合中添加元素,重复元素不生效 | O(1)(平均) |
remove(element) |
删除集合中的元素,元素不存在抛异常 | O(1)(平均) |
discard(element) |
删除集合中的元素,元素不存在不报错 | O(1)(平均) |
contains(element) |
判断元素是否在集合中 | O(1)(平均) |
size() |
返回集合中元素的个数 | O(1) |
clear() |
清空集合所有元素 | O(n) |
union(set2) |
求两个集合的并集(A ∪ B) | O(n + m) |
intersection(set2) |
求两个集合的交集(A ∩ B) | O(min(n, m)) |
difference(set2) |
求两个集合的差集(A - B) | O(n) |
is_subset(set2) |
判断当前集合是否是 set2 的子集 | O(n) |
三、集合的底层实现
集合的底层实现与字典高度一致(以哈希集合为例):
- 底层使用哈希表(桶数组) 存储元素;
- 插入元素时,先通过哈希函数计算元素的索引,再存入对应桶中;
- 利用哈希表的冲突解决策略(如链地址法)处理哈希冲突;
- 由于集合"只存元素不存值",本质是将元素作为"键"存入哈希表,值字段可忽略(如 Python 的
set底层就是无值的dict)。
对比字典与集合的底层逻辑:
| 数据结构 | 哈希表存储内容 | 核心特性 |
|---|---|---|
| 字典 | 键值对(Key-Value) | 键唯一、通过键查值 |
| 集合 | 仅键(Key) | 元素唯一、快速判重 |
四、集合的实现(Python示例)
Python 中的 set 是哈希集合的经典实现,直接使用即可。以下是自定义简易哈希集合,帮助理解底层逻辑:
python
class HashSet:
def __init__(self, capacity=8):
self.capacity = capacity # 桶数组大小
self.buckets = [None] * self.capacity # 桶数组(每个桶存储链表)
self.size = 0 # 元素个数
self.load_factor = 0.75 # 负载因子阈值
class Node:
"""集合节点(链表节点)"""
def __init__(self, value):
self.value = value
self.next = None
def _hash(self, value):
"""哈希函数:计算元素的桶索引"""
return hash(value) % self.capacity
def _resize(self):
"""扩容:桶数组翻倍,重新哈希所有元素"""
old_buckets = self.buckets
self.capacity *= 2
self.buckets = [None] * self.capacity
self.size = 0
# 重新插入旧桶中的所有元素
for bucket in old_buckets:
current = bucket
while current:
self.add(current.value)
current = current.next
def add(self, value):
"""添加元素(去重)"""
# 检查元素是否已存在
if self.contains(value):
return False # 重复元素,添加失败
# 检查是否需要扩容
if self.size / self.capacity >= self.load_factor:
self._resize()
index = self._hash(value)
# 插入链表头部
new_node = self.Node(value)
new_node.next = self.buckets[index]
self.buckets[index] = new_node
self.size += 1
return True
def remove(self, value):
"""删除元素(元素不存在抛异常)"""
if not self.contains(value):
raise KeyError(f"元素 {value} 不在集合中")
self._remove(value)
return True
def discard(self, value):
"""删除元素(元素不存在不报错)"""
if self.contains(value):
self._remove(value)
return True
return False
def _remove(self, value):
"""内部删除逻辑"""
index = self._hash(value)
current = self.buckets[index]
prev = None
while current:
if current.value == value:
# 调整链表指针
if prev:
prev.next = current.next
else:
self.buckets[index] = current.next
self.size -= 1
return
prev = current
current = current.next
def contains(self, value):
"""判断元素是否在集合中"""
index = self._hash(value)
current = self.buckets[index]
while current:
if current.value == value:
return True
current = current.next
return False
def size(self):
"""返回元素个数"""
return self.size
def clear(self):
"""清空集合"""
self.buckets = [None] * self.capacity
self.size = 0
def union(self, other_set):
"""返回两个集合的并集"""
result = HashSet()
# 添加当前集合的所有元素
for bucket in self.buckets:
current = bucket
while current:
result.add(current.value)
current = current.next
# 添加另一个集合的所有元素(自动去重)
for bucket in other_set.buckets:
current = bucket
while current:
result.add(current.value)
current = current.next
return result
def intersection(self, other_set):
"""返回两个集合的交集"""
result = HashSet()
# 遍历较小的集合,提高效率
if self.size < other_set.size:
target_set = self
check_set = other_set
else:
target_set = other_set
check_set = self
for bucket in target_set.buckets:
current = bucket
while current:
if check_set.contains(current.value):
result.add(current.value)
current = current.next
return result
def difference(self, other_set):
"""返回当前集合相对于 other_set 的差集(A - B)"""
result = HashSet()
for bucket in self.buckets:
current = bucket
while current:
if not other_set.contains(current.value):
result.add(current.value)
current = current.next
return result
def is_subset(self, other_set):
"""判断当前集合是否是 other_set 的子集"""
for bucket in self.buckets:
current = bucket
while current:
if not other_set.contains(current.value):
return False
current = current.next
return True
def __str__(self):
"""打印集合"""
elements = []
for bucket in self.buckets:
current = bucket
while current:
elements.append(str(current.value))
current = current.next
return "{" + ", ".join(elements) + "}"
# 使用示例
s1 = HashSet()
s1.add(1)
s1.add(2)
s1.add(3)
s1.add(2) # 重复元素,不生效
print(s1) # 输出: {1, 2, 3}
s2 = HashSet()
s2.add(3)
s2.add(4)
s2.add(5)
# 并集
print(s1.union(s2)) # 输出: {1, 2, 3, 4, 5}
# 交集
print(s1.intersection(s2)) # 输出: {3}
# 差集
print(s1.difference(s2)) # 输出: {1, 2}
# 子集判断
print(s1.is_subset(s2)) # 输出: False
五、集合的优缺点
优点
- 自动去重:无需手动处理重复元素,简化开发;
- 高效判重:平均 O(1) 时间复杂度判断元素是否存在,远快于数组、链表;
- 集合运算便捷:原生支持并集、交集、差集等数学运算,适合数据筛选;
- 实现简单:底层基于哈希表,逻辑与字典高度复用,易理解。
缺点
- 无序性 :哈希集合无法保证元素顺序,如需有序需使用 TreeSet(Java)或
sorted()排序; - 元素不可变:限制了元素类型,无法存储可变对象;
- 内存开销大:哈希表的桶数组和链表指针会占用额外空间,空间利用率低于数组;
- 最坏性能差:哈希冲突严重时,操作时间复杂度退化为 O(n)(依赖良好的哈希函数和扩容策略)。
六、集合的应用场景
集合的"去重"和"快速判重"特性使其在以下场景中高频使用:
- 数据去重:如筛选列表中的唯一元素、处理用户输入去重;
- 元素存在性判断:如判断用户是否在黑名单中、关键词是否在敏感词库中;
- 集合运算:如筛选两个数据集的共同元素(交集)、合并数据集并去重(并集)、找出两个数据集的差异(差集);
- 缓存判重:如爬虫去重(避免重复爬取同一 URL)、任务调度去重(避免重复执行同一任务);
- 数学建模:如集合论相关的算法实现、图论中的节点集合操作。
七、集合 vs 字典 vs 数组
| 数据结构 | 核心特性 | 核心优势 | 适用场景 |
|---|---|---|---|
| 集合 | 无序、不重复、快速判重 | 自动去重、集合运算便捷 | 去重、判重、数据筛选 |
| 字典 | 键值对映射、键唯一 | 通过键快速查值 | 键值对存储、索引查找 |
| 数组 | 有序、可重复、索引访问 | 随机访问快、空间利用率高 | 有序数据存储、遍历 |
八、总结
集合是一种高效的去重数据结构,底层基于哈希表实现,核心优势是"自动去重"和"快速判重"。它与字典同源,本质是"无值字典",继承了哈希表的 O(1) 平均时间复杂度操作。
实际开发中,直接使用编程语言内置集合(如 Python 的 set、Java 的 HashSet、C++ 的 unordered_set)即可,这些实现已优化了哈希函数、冲突解决和扩容策略,性能稳定且支持丰富的集合运算,无需重复造轮子。