Python 算法基础篇之集合

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 的 setdict 一样,底层都是哈希表(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)查找
相关推荐
平行侠1 小时前
A15 工业路由器IP前缀高速检索与内存压缩系统
网络·tcp/ip·算法
头歌实践平台1 小时前
招聘大数据可视化
大数据·python
Cloud_Shy6182 小时前
Python 数据分析基础入门:《Excel Python:飞速搞定数据分析与处理》学习笔记系列(第八章 使用读写包操作 Excel 文件 上篇)
python·数据分析·excel·pandas
阿旭超级学得完2 小时前
C++11包装器(function和bind)
java·开发语言·c++·算法·哈希算法·散列表
輕華2 小时前
uv工具详解——Python包与项目管理器完全指南
开发语言·python·uv
li星野2 小时前
位运算 & 数学 & 高频进阶九题通关(Python + C++)
c++·python·学习·算法
用户8356290780513 小时前
使用 Python 在 PowerPoint 中添加并控制音频播放
后端·python
jerryinwuhan3 小时前
hello算法,简单讲(1)
算法·排序算法
y = xⁿ3 小时前
20天速通LeetCodeday15:BFS广度优先搜索
算法·宽度优先