元组和集合:Python 里最"拧巴"的两个数据类型
你有没有过这种时刻:明明只是想让数据别被改,结果代码跑着跑着,某个变量不知道被谁偷偷动了,bug 藏在几百行代码里,找了半天才定位到——原来是列表被某个函数顺手改了一个元素。
如果你经历过这种痛,那你一定会爱上元组。
先聊元组:那个"不许改"的便签纸
元组用圆括号,长得像列表,但性格完全相反。列表是那种"随便改,随便加"的敞亮盒子,元组则是写满字就贴死的便签纸——创建之后,你不能增、不能删、不能改。
triangle_sides = (3, 4, 5)
triangle_sides[0] = 6 # TypeError: 'tuple' object does not support item assignment
就这么简单粗暴。
但你别觉得元组是"阉割版列表"。它恰恰是因为不能改,才在某些场景下比列表更可靠。比如:
- 一年12个月的名字——你不想让谁手滑把"三月"改成"十三月"
- 你家经纬度坐标
(116.4, 39.9)——不该变的东西,就别给它变的权限 - 函数返回多个值——Python 底层就是用元组打包返回的
元组能当字典的键,列表不行。 这是很多人忽略的一个点。字典的键要求不可变,元组满足,列表不满足。所以当你需要用一个组合值作为键时,元组就是唯一选择。
一个冷知识:单元素元组必须加逗号,(99) 是整数,(99,) 才是元组。这个坑几乎每个初学者都踩过。
再看集合:那个自动去重的百宝袋
集合用花括号,但注意——{} 是空字典,不是空集合。空集合必须用 set() 创建。这是另一个高频坑。
集合的核心特性就三条:无序、不重复、元素必须可哈希。
guests = {"小明", "小红", "小刚", "小明"} # 小明自动去重
print(guests) # {'小红', '小刚', '小明'},顺序随机
无序意味着你不能用下标访问。不重复意味着天然去重。元素必须可哈希意味着只能放不可变类型——数字、字符串、元组都可以,列表不行。
集合最强大的地方在于数学运算:交集 &、并集 |、差集 -。这在处理"哪些人同时参加了两个活动"、"哪些商品在两个仓库都有库存"这类问题时,一行代码搞定,比循环遍历不知道高到哪里去了。
class1 = {"小明", "小红", "小刚"}
class2 = {"小刚", "小丽", "小亮"}
both = class1 & class2 # {'小刚'}
all_students = class1 | class2 # {'小明', '小红', '小刚', '小丽', '小亮'}
only_class1 = class1 - class2 # {'小明', '小红'}
一个常见的认知误区
很多人以为元组里放了列表,元组就"可变"了。这个理解需要修正。
t = (1, [2, 3], 4)
t[1][0] = 5 # 这是合法的!
元组的不可变,指的是不能改变元组里每个位置指向的对象引用。你不能把 t[1] 换成另一个列表,但你可以修改 t[1] 这个列表内部的内容。就像一张便签纸贴死了不能撕下来,但纸上写的内容如果是个可擦写的白板,你还是能擦掉重写。
这个概念搞清楚了,很多关于元组的题目就迎刃而解。
题目实战:当元组和集合遇上矩阵求和
来看一道经典题。给你一个 n 行 m 列的矩阵,然后 k 次询问,每次给两个坐标点 (x1,y1) 和 (x2,y2),求这个子矩阵内所有数字的和。
数据范围:n,m ≤ 1000,k ≤ 200000。矩阵元素可能为负数。
如果每次询问都暴力遍历子矩阵,单次复杂度 O(n×m),k 次就是 O(k×n×m),在数据范围下直接超时。
这题考察的核心是二维前缀和。
思路是这样的:先预处理出一个二维前缀和数组 prefix[i][j],表示从 (1,1) 到 (i,j) 这个矩形内所有元素的和。然后每次询问,通过容斥原理在 O(1) 时间内算出答案:
ans = prefix[x2][y2] - prefix[x1-1][y2] - prefix[x2][y1-1] + prefix[x1-1][y1-1]
为什么可以这样?画个图就明白了。prefix[x2][y2] 是整个大矩形的和,减掉上面多出来的部分,再减掉左边多出来的部分,但左上角被减了两次,所以要加回来一次。
代码核心就几行:
# 预处理二维前缀和
for i in range(1, n+1):
for j in range(1, m+1):
prefix[i][j] = matrix[i][j] + prefix[i-1][j] + prefix[i][j-1] - prefix[i-1][j-1]
# 每次查询 O(1) 回答
for _ in range(k):
x1, y1, x2, y2 = map(int, input().split())
ans = prefix[x2][y2] - prefix[x1-1][y2] - prefix[x2][y1-1] + prefix[x1-1][y1-1]
print(ans)
这题和元组、集合有什么关系?其实没有直接关系。但它的思想——预处理换查询时间——和集合的思想有异曲同工之妙:集合用哈希表的空间换取了 O(1) 的查找和去重时间。编程里很多优化,本质都是在时间和空间之间做权衡。
回到元组和集合本身。我见过太多人,列表用到底,字典用到底,从来不用元组和集合。不是说不行,但元组能给你多一层安全保障,集合能让你少写十行去重代码。工具多一件,解决问题的思路就宽一分。
元组是"不该变的东西就别让它变"的纪律,集合是"重复的东西自动消失"的魔法。下次写代码的时候,多想想:这个数据需要被修改吗?不需要就上元组。这个列表里有重复吗?有就转成集合。你会发现,代码变干净了,bug 变少了,思路也清晰了。
关于作者
我是赵老师,持有 NOI 信息学奥赛教练证书,拥有 15 年以上的软件开发经验,从事信息学少儿编程教学已有 8 年时间。
这些年累计帮助 多名 学生通过编程特长升入自己心仪的目标学校。
如果你在编程学习上有任何疑问,欢迎联系我:18620372957(微信同号)