跳到主要内容

4.4 集合 set

数据结构的最后一位成员。先看一个具体需求:一个抽奖名单里有人重复提交了,怎么去掉重复的名字?

用列表 + 循环当然能写出来,但 Python 有个数据结构天生就干这个——集合(set)。它只有两条脾气:

  1. 不重复:同样的元素只保留一个
  2. 无顺序:元素没有位置编号,不能问"第几个"

这一节内容不多,重点掌握"去重"和"交并差"两个绝活。

创建集合

用花括号 {} 直接列元素(注意和字典区分:字典里是 键: 值 对,集合里是单个值):

tags = {"红", "绿", "蓝"}
print(type(tags)) # <class 'set'>

它的两条脾气眼见为实。放进重复元素,自动只留一个

nums = {1, 2, 2, 3, 3, 3}
print(nums) # {1, 2, 3}

没有顺序,打印出来的排列可能和你写的不一样,也不能索引:

tags = {"红", "绿", "蓝"}
print(tags[0]) # ❌ TypeError: 'set' object is not subscriptable

想逐个处理就用 for 遍历(顺序不保证):

for t in tags:
print(t)

⚠️ 特例:空集合必须写 set(),不能写 {} 因为空花括号被字典抢注了:

a = {}
print(type(a)) # <class 'dict'> ← 是空字典!
b = set()
print(type(b)) # <class 'set'> ← 这才是空集合

绝活一:一行代码去重

开头的抽奖问题,答案就一行——列表转集合(去重),再转回列表

names = ["小明", "小红", "小明", "小刚", "小红"]
unique = list(set(names))
print(unique) # ['小刚', '小明', '小红'](顺序可能变化)

拆开看这个嵌套(和 int(input()) 一样由内到外):

  1. set(names):列表 → 集合,重复元素在这一步被自动扔掉
  2. list(...):集合 → 列表,变回可以索引、排序的列表

list(set(某列表)) 值得当口诀背下来。注意副作用:去重后原来的顺序会乱(集合无序)。乱了想恢复整齐可以接一个排序:sorted(set(nums))

基本操作:增、删、查

tags = {"红", "绿"}

tags.add("蓝") # 增:加一个元素(重复添加不报错,但也不会多出一份)
tags.remove("红") # 删:元素不存在会报 KeyError
tags.discard("紫") # 删的温柔版:不存在也不吭声

print(len(tags)) # 2
print("绿" in tags) # True

removediscard 的选择逻辑,与字典的 []get() 如出一辙:该在而不在算 bug → 用 remove 让它报错;可有可无 → 用 discard

in 判断要特别说一句:集合做 in 判断的速度极快,而且不随元素变多而变慢;列表则是从头到尾一个个找,数据一多就慢。所以"频繁判断某元素在不在一大堆数据里"的场景,先把列表转成集合是行业常规操作。现阶段数据量小感受不到差距,先把结论记住。

绝活二:集合运算——交集、并集、差集

两个集合之间可以做数学课上的"集合运算",三个运算符:

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

print(a & b) # {3, 4} 交集:两边都有的
print(a | b) # {1, 2, 3, 4, 5, 6} 并集:合在一起(自动去重)
print(a - b) # {1, 2} 差集:a 有而 b 没有的

用文氏图理解:

a b
┌──────┬──┬──────┐
│ 1 2 │3 │ 5 6 │
│ │4 │ │
└──────┴──┴──────┘

a & b 就是中间重叠区
a | b 是整个图形
a - b 是左边不重叠的部分(注意 a - b ≠ b - a)

换成人话场景立刻就懂了:

python_class = {"小明", "小红", "小刚"}
english_class = {"小红", "小丽"}

print(python_class & english_class) # {'小红'} 两门都报的
print(python_class | english_class) # 四个人 至少报了一门的
print(python_class - english_class) # {'小明', '小刚'} 只报了 Python 的
print(english_class - python_class) # {'小丽'} 只报了英语的

这类"找共同好友"、"找两份名单的差异"的需求,用列表写要好几层循环,用集合就是一个运算符。

全章收官:四种数据结构怎么选

四兄弟到齐,一张表看清各自的定位:

结构写法有序?可改?可重复?一句话定位
列表 list[1, 2]一串数据,默认首选
元组 tuple(1, 2)定死不变的固定搭配
字典 dict{"k": v}键不可有名字的数据,按键查值
集合 set{1, 2}去重、判存在、交并差

选择思路走一遍流程:

  1. 数据是"名字 → 值"的对应关系?→ 字典
  2. 需要去重或交并差运算?→ 集合
  3. 内容永远不变?→ 元组
  4. 其他情况 → 列表

练习

新建 practice44.py 完成:

1. 把列表 ["a", "b", "a", "c", "b"] 去重后从小到大排序打印。

2. 你的爱好 {"篮球", "音乐", "编程"},朋友的爱好 {"音乐", "游戏"}。求:共同爱好、两人爱好的总和、只有你有的爱好。

3. 不运行,先猜输出,再验证:

s = {1, 1, 2, 3}
print(len(s))

e = {}
print(type(e))

a = {1, 2, 3}
b = {2, 3, 4}
print(a - b == b - a)

4. 下面代码想统计列表里有多少个不同的数字,但结果不对,找出原因修好它:

nums = [3, 7, 3, 1, 7, 7]
count = len(nums)
print(f"共有 {count} 个不同的数字")
点击查看答案
# 1
items = ["a", "b", "a", "c", "b"]
print(sorted(set(items))) # ['a', 'b', 'c']

# 2
mine = {"篮球", "音乐", "编程"}
friend = {"音乐", "游戏"}
print("共同爱好:", mine & friend) # {'音乐'}
print("所有爱好:", mine | friend)
print("只有我有:", mine - friend) # {'篮球', '编程'}

第 3 题:

  • len(s)3(重复的 1 只留一个:{1, 2, 3})
  • type(e)<class 'dict'>{} 是空字典,不是空集合!)
  • a - b == b - aFalsea - b 是 {1},b - a 是 {4},差集有方向)

第 4 题: len(nums) 数的是全部元素(6 个),没有去重。先转集合再数:

nums = [3, 7, 3, 1, 7, 7]
count = len(set(nums))
print(f"共有 {count} 个不同的数字") # 共有 3 个不同的数字

本章小结

  • 集合两条脾气:不重复、无顺序(没有索引,不能 s[0]
  • 空集合必须 set(){} 是空字典
  • 去重口诀:list(set(lst));要整齐就 sorted(set(lst))
  • add 增,remove(严格)/ discard(温柔)删;集合的 in 判断飞快
  • & 交集、| 并集、- 差集(有方向)
  • 选型流程:键值对→字典,去重/交并差→集合,定死→元组,其余→列表

数据结构四兄弟集齐!它们是后面一切程序的原材料。下一章学习编程中最重要的概念之一:5.1 函数入门