CC++ & Algorithm

元组和集合: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(微信同号)

这篇文章对你有帮助吗?

成为第一个评价的人

评论0

还没有评论,来抢沙发~

评论加载中...

想系统学习这个知识点?查看完整知识点 →