1. 集合是什么?
1.1 生活中的集合
想象你有一个音乐播放列表,里面收藏了你喜欢的歌曲。这个播放列表有几个特点:
- 没有重复:同一首歌不会收藏两次
- 无序排列:你不在乎哪首歌在前哪首在后
- 快速查找:你能瞬间判断某首歌是否在列表中
这就是集合的本质!
集合(Set) :一种无序、不重复 的元素集合,基于哈希表实现,支持 O(1) 的成员检测。
1.2 为什么需要集合?
python
# 场景:从用户行为日志中提取去重后的用户ID
user_logs = [1001, 1002, 1001, 1003, 1002, 1004, 1001]
# 用列表去重(低效)
unique_users_list = []
for uid in user_logs:
if uid not in unique_users_list: # O(n) 查找!
unique_users_list.append(uid)
print(unique_users_list) # [1001, 1002, 1003, 1004]
# 时间复杂度:O(n²)
# 用集合去重(高效)
unique_users_set = set(user_logs) # O(n)
print(unique_users_set) # {1001, 1002, 1003, 1004}
# 时间复杂度:O(n)
💡 核心优势 :集合的
in操作是 O(1) ,列表是 O(n)。数据量越大,差距越明显。
2. 集合的创建与基本操作
2.1 创建集合的四种方式
python
# 方式1:花括号(注意:空{}是字典!)
s1 = {1, 2, 3, 4, 5}
print(type(s1)) # <class 'set'>
# 方式2:set() 构造函数
s2 = set([3, 4, 5, 6, 7]) # 从列表
s3 = set((1, 2, 3)) # 从元组
s4 = set("hello") # 从字符串 → {'h', 'e', 'l', 'o'}
s5 = set(range(5)) # 从 range → {0, 1, 2, 3, 4}
# 方式3:集合推导式(类似列表推导式)
s6 = {x**2 for x in range(10) if x % 2 == 0} # {0, 4, 16, 36, 64}
# 方式4:frozenset(不可变集合,可作为字典键)
fs = frozenset([1, 2, 3])
print(type(fs)) # <class 'frozenset'>
⚠️ 重要 :
{}创建的是空字典 ,不是空集合!空集合必须用set()。
2.2 添加与删除元素
python
s = {1, 2, 3}
# ─── 添加元素 ───
# add():添加单个元素
s.add(4)
print(s) # {1, 2, 3, 4}
s.add(3) # 添加重复元素,无效果
print(s) # {1, 2, 3, 4}
# update():添加多个元素(可迭代对象)
s.update([5, 6]) # 添加列表
s.update({7, 8}) # 添加集合
s.update((9, 10)) # 添加元组
s.update("ab") # 添加字符串 → 添加 'a', 'b'
print(s) # {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 'a', 'b'}
# ─── 删除元素 ───
s = {1, 2, 3, 4, 5}
# remove():删除指定元素,不存在则报错
s.remove(3)
print(s) # {1, 2, 4, 5}
# s.remove(10) # KeyError: 10
# discard():删除指定元素,不存在不报错(推荐)
s.discard(4) # 成功删除
s.discard(10) # 静默忽略,不报错
# pop():随机删除并返回一个元素
item = s.pop()
print(f"弹出: {item}, 剩余: {s}")
# clear():清空集合
s.clear()
print(s) # set()
2.3 查询与遍历
python
s = {'Python', 'Java', 'Go', 'Rust'}
# 成员检测(集合最强功能!)
print('Python' in s) # True ← O(1)
print('C++' in s) # False ← O(1)
# 遍历(无序!)
for lang in s:
print(lang)
# 输出顺序不确定:可能是 Go, Java, Python, Rust...
# 长度
print(len(s)) # 4
# 判断空集
print(bool(s)) # True
print(bool(set())) # False
3. 集合运算:数学之美
集合最优雅的地方在于它直接支持数学集合运算。
数学符号 vs Python 运算符:
A ∪ B (并集) → A | B 或 A.union(B)
A ∩ B (交集) → A & B 或 A.intersection(B)
A - B (差集) → A - B 或 A.difference(B)
A △ B (对称差) → A ^ B 或 A.symmetric_difference(B)
A ⊆ B (子集) → A <= B 或 A.issubset(B)
A ⊇ B (超集) → A >= B 或 A.issuperset(B)
3.1 并集:合并去重
python
frontend = {'HTML', 'CSS', 'JavaScript', 'React'}
backend = {'Python', 'Java', 'SQL', 'JavaScript'}
# 并集:所有技能(去重)
all_skills = frontend | backend
print(all_skills)
# {'HTML', 'CSS', 'JavaScript', 'React', 'Python', 'Java', 'SQL'}
# 等价写法
all_skills = frontend.union(backend)
3.2 交集:共同元素
python
# 交集:前后端都会的技能
common = frontend & backend
print(common) # {'JavaScript'}
# 等价写法
common = frontend.intersection(backend)
3.3 差集:独有元素
python
# 差集:仅前端会的
only_frontend = frontend - backend
print(only_frontend) # {'HTML', 'CSS', 'React'}
# 差集:仅后端会的
only_backend = backend - frontend
print(only_backend) # {'Python', 'Java', 'SQL'}
# 对称差集:仅一方会的(不会双方都有的)
exclusive = frontend ^ backend
print(exclusive)
# {'HTML', 'CSS', 'React', 'Python', 'Java', 'SQL'}
3.4 子集与超集
python
skills = {'Python', 'SQL', 'Linux'}
backend_must = {'Python', 'SQL'}
# 子集判断
print(backend_must <= skills) # True
print(backend_must.issubset(skills)) # True
# 超集判断
print(skills >= backend_must) # True
print(skills.issuperset(backend_must)) # True
# 真子集(严格子集)
print(backend_must < skills) # True(skills 更大)
print(skills < skills) # False(不是真子集)
# 不相交判断
print({'A', 'B'}.isdisjoint({'C', 'D'})) # True(无共同元素)
3.5 运算可视化
frontend = {HTML, CSS, JS, React}
backend = {Python, Java, SQL, JS}
┌─────────────────────────────┐
│ 并集 frontend | backend │
│ {HTML, CSS, JS, React, │
│ Python, Java, SQL} │
│ ┌───┐ │
│ {JS} │ │ │
│ 交集 │ │ │
│ └───┘ │
│ 仅前端 仅后端 │
│ {HTML,CSS, {Python,Java, │
│ React} SQL} │
└─────────────────────────────┘
4. 集合的底层原理
4.1 基于哈希表实现
Python 的 set 和 dict 一样,底层都是哈希表(Hash Table)。
哈希表原理:
键(key) → 哈希函数 → 哈希值 → 取模 → 数组索引
↓
┌─────────┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │
└─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┘
│ │ │ │ │ │
None 42 None 17 None 99
查找 42:hash(42) % 6 = 1 → 直接访问索引1 → O(1)
4.2 为什么集合元素必须可哈希?
python
# ✅ 可哈希元素(不可变类型)
s = {1, "hello", (1, 2), frozenset([3, 4])}
# ❌ 不可哈希元素(可变类型)
# s = {[1, 2]} # TypeError: unhashable type: 'list'
# s = {{1, 2}} # TypeError: unhashable type: 'set'
# s = {{"a": 1}} # TypeError: unhashable type: 'dict'
# 原理:哈希值必须稳定,可变对象哈希值会变
4.3 集合 vs 列表 vs 字典
| 特性 | 列表 list |
集合 set |
字典 dict |
|---|---|---|---|
| 有序性 | ✅ 有序 | ❌ 无序 | Python 3.7+ 有序 |
| 重复元素 | ✅ 允许 | ❌ 不允许 | ❌ 键不允许重复 |
| 查找速度 | O(n) | O(1) | O(1) |
| 内存占用 | 小 | 较大(哈希表) | 较大 |
| 适用场景 | 有序序列 | 去重、成员检测 | 键值映射 |
| 可哈希元素 | 无要求 | 必须可哈希 | 键必须可哈希 |
5. 集合在算法中的应用
5.1 两数之和(哈希优化版)
python
def two_sum(nums: list[int], target: int) -> list[int]:
"""
用集合/字典优化:从 O(n²) 降到 O(n)
"""
seen = set()
for i, num in enumerate(nums):
complement = target - num
if complement in seen: # O(1) 查找!
return [nums.index(complement), i]
seen.add(num)
return []
# 测试
print(two_sum([2, 7, 11, 15], 9)) # [0, 1]
print(two_sum([3, 2, 4], 6)) # [1, 2]
5.2 判断数组是否有重复元素
python
def contains_duplicate(nums: list[int]) -> bool:
"""利用集合去重特性"""
return len(nums) != len(set(nums))
# 测试
print(contains_duplicate([1, 2, 3, 4])) # False
print(contains_duplicate([1, 2, 3, 3])) # True
5.3 寻找两个数组的交集
python
def intersection(nums1: list[int], nums2: list[int]) -> list[int]:
"""
求两个数组的交集(去重)
方法1:集合运算(推荐)
"""
return list(set(nums1) & set(nums2))
def intersection_v2(nums1: list[int], nums2: list[int]) -> list[int]:
"""
方法2:遍历较小集合,检测是否在另一个集合中
适合一个数组很大,一个很小的情况
"""
if len(nums1) > len(nums2):
nums1, nums2 = nums2, nums1 # 保证 nums1 较小
set2 = set(nums2)
return [x for x in set(nums1) if x in set2]
# 测试
print(intersection([1, 2, 2, 1], [2, 2])) # [2]
print(intersection_v2([4, 9, 5], [9, 4, 9, 8, 4])) # [9, 4]
5.4 最长连续序列
python
def longest_consecutive(nums: list[int]) -> int:
"""
最长连续序列
思路:用集合 O(1) 查找,只从序列起点开始计数
时间:O(n),空间:O(n)
"""
num_set = set(nums)
longest = 0
for num in num_set:
# 只从序列的起点开始(num-1 不在集合中)
if num - 1 not in num_set:
current_num = num
current_streak = 1
while current_num + 1 in num_set:
current_num += 1
current_streak += 1
longest = max(longest, current_streak)
return longest
# 测试
print(longest_consecutive([100, 4, 200, 1, 3, 2])) # 4 ([1,2,3,4])
print(longest_consecutive([0, 3, 7, 2, 5, 8, 4, 6, 0, 1])) # 9
5.5 单词拆分
python
def word_break(s: str, word_dict: list[str]) -> bool:
"""
判断字符串能否被拆分成字典中的单词
用集合加速查找
"""
word_set = set(word_dict)
n = len(s)
dp = [False] * (n + 1)
dp[0] = True # 空字符串可被拆分
for i in range(1, n + 1):
for j in range(i):
if dp[j] and s[j:i] in word_set: # O(1) 查找
dp[i] = True
break
return dp[n]
# 测试
print(word_break("leetcode", ["leet", "code"])) # True
print(word_break("applepenapple", ["apple", "pen"])) # True
print(word_break("catsandog", ["cats", "dog", "sand", "and", "cat"])) # False
6. 性能对比与最佳实践
6.1 成员检测性能对比
python
import time
def benchmark():
"""对比列表和集合的成员检测性能"""
# 准备数据
n = 100000
data = list(range(n))
target = n - 1 # 最后一个元素(最坏情况)
# 列表查找
start = time.time()
for _ in range(1000):
found = target in data # O(n)
list_time = time.time() - start
# 集合查找
data_set = set(data)
start = time.time()
for _ in range(1000):
found = target in data_set # O(1)
set_time = time.time() - start
print(f"数据量: {n}")
print(f"列表查找 1000 次: {list_time:.4f}秒")
print(f"集合查找 1000 次: {set_time:.4f}秒")
print(f"集合快 {list_time / set_time:.0f} 倍")
benchmark()
典型输出:
数据量: 100000
列表查找 1000 次: 2.3456秒
集合查找 1000 次: 0.0001秒
集合快 23456 倍
6.2 去重性能对比
python
import random
def dedupe_benchmark():
"""对比不同去重方法的性能"""
data = [random.randint(0, 10000) for _ in range(100000)]
# 方法1:列表遍历(O(n²))
import time
start = time.time()
result = []
for x in data:
if x not in result:
result.append(x)
t1 = time.time() - start
# 方法2:集合去重(O(n))
start = time.time()
result = list(set(data))
t2 = time.time() - start
# 方法3:dict.fromkeys(保持顺序,Python 3.7+)
start = time.time()
result = list(dict.fromkeys(data))
t3 = time.time() - start
print(f"列表遍历: {t1:.4f}秒")
print(f"集合去重: {t2:.4f}秒")
print(f"字典去重: {t3:.4f}秒")
dedupe_benchmark()
6.3 最佳实践
python
# ✅ 推荐用法
# 1. 快速去重
unique = list(set(original_list))
# 2. 成员检测(大量查询时)
if item in some_set: # 比 list 快得多
# 3. 集合运算替代循环
common = set(a) & set(b) # 比双重循环优雅
# 4. 过滤重复
seen = set()
for item in items:
if item not in seen:
seen.add(item)
process(item)
# ❌ 避免用法
# 1. 不要依赖集合顺序(虽然 Python 3.7+ 有插入顺序,但不保证)
# 需要有序用 dict.fromkeys() 或 sorted()
# 2. 不要存储可变对象
# bad_set = {[1, 2]} # TypeError
# 3. 小数据量没必要转集合
# 10个元素的列表,in 操作也很快
7. 总结
7.1 核心要点速查
| 操作 | 方法 | 时间复杂度 | 说明 |
|---|---|---|---|
| 创建 | set() / {} |
O(n) | 空集合用 set() |
| 添加 | add() |
O(1) | 单个元素 |
| 添加多个 | update() |
O(k) | 可迭代对象 |
| 删除 | remove() / discard() |
O(1) | discard 更安全 |
| 成员检测 | in |
O(1) | 最强功能! |
| 并集 | ` | /union()` |
O(len(s)+len(t)) |
| 交集 | & / intersection() |
O(min(len(s),len(t))) | |
| 差集 | - / difference() |
O(len(s)) | |
| 对称差 | ^ / symmetric_difference() |
O(len(s)+len(t)) |
7.2 集合使用场景
什么时候用集合?
├── 需要去重
│ └── list → set → list
├── 频繁成员检测
│ └── if x in container: 用 set 替代 list
├── 集合运算
│ └── 交集、并集、差集等数学运算
├── 过滤重复
│ └── 配合 seen 集合记录已处理元素
└── 算法优化
└── 用 O(1) 查找替代 O(n) 查找
7.3 与列表、字典的关系
┌─────────────┐
│ 可变容器 │
└──────┬──────┘
│
┌─────────┼─────────┐
│ │ │
▼ ▼ ▼
list set dict
(有序) (无序) (键值对)
可重复 不重复 键不重复
O(n)查找 O(1)查找 O(1)查找