Python 集合
Python 的 list 和 tuple 用位置组织元素,dict 用键关联值,set 保存不重复的可哈希元素。选择容器时,先确定是否需要顺序、重复项、原地修改和按键查找,再考虑具体操作的成本。
这些差异会直接影响现场编码:用集合去重是否丢失顺序、把列表放进字典键是否报错、统计结果是否保留了重复次数,都需要能够解释。
1. 面试核心
先记住这几条,再读正文list可增删和替换元素,tuple的元素绑定不可修改;两者都是有序序列。元组是否可哈希,还取决于其中每个元素是否可哈希。卡 · 容器的职责- 字典的键和集合元素必须可哈希,字典的值可以是任意对象。相等的可哈希对象必须具有相同哈希值,哈希相同不保证相等。卡 · 可哈希性与键
- Python 3.7 起,字典保留插入顺序;集合不提供可依赖的元素顺序。需要保留首次出现顺序时,可使用
dict.fromkeys去重,前提是元素可哈希。卡 · 去重与聚合 - 切片采用左闭右开的边界,列表切片会创建新的外层列表。切片不是深拷贝,内层可变对象仍可能共享。卡 · 切片与遍历
- CPython 列表按下标访问是常数级,头部插入或删除需要移动元素;字典和集合的查找通常依赖哈希。选择结构要结合最频繁的操作,不能把所有容器操作都记作 O(1)。卡 · 操作成本
2. 容器的职责
| 容器 | 组织方式 | 可变性 | 重复元素 | 适合的任务 |
|---|---|---|---|---|
list | 按位置,有序 | 可变 | 允许 | 收集结果、按下标处理、排序 |
tuple | 按位置,有序 | 元素绑定不可变 | 允许 | 表示固定结构、作为可哈希组合键的候选 |
dict | 键到值的映射,保留插入顺序 | 可变 | 键唯一,值可重复 | 计数、分组、根据 ID 查结果 |
set | 不重复元素,无顺序保证 | 可变 | 不保留重复项 | 成员判断、交集、差集 |
results = ["ok", "fail", "ok"]
counts = {"ok": 2, "fail": 1}
allowed = {"ok", "skip"}
print(results[0]) # ok
print(counts["ok"]) # 2
print(set(results) - allowed) # {'fail'}
三个容器回答的是不同问题:results 保留每次执行结果,counts 保留出现次数,集合差集找出不在允许范围内的结果。把列表直接转成集合后,重复次数就无法从集合中恢复。
元组的创建关键在逗号:(1,) 是单元素元组,(1) 只是整数表达式。元组里仍可以包含列表;内层可变性与复制边界见 Python 对象模型。
3. 可哈希性与键
可哈希对象具有在其生命周期内保持稳定的哈希值,并支持用于比较的相等性。相等的对象必须有相同哈希值,才能在哈希容器中按同一个键处理。
整数、字符串和由可哈希元素组成的元组都可用作字典键;列表、字典和普通集合不可哈希。不可变集合 frozenset 可以作为键,它的元素也必须可哈希。
by_case = {("login", 1): [200, 201]}
print(by_case[("login", 1)]) # [200, 201]
# invalid = {["login", 1]: 200} # 运行时报 TypeError:list 不可哈希
# invalid = {("login", []): 200} # 运行时报 TypeError:元组内的 list 不可哈希
字典值中的列表可以修改,因为查找键时不依赖值的哈希。这里要检查的是键,不是字典里的每个对象都必须不可变。
“可哈希”和“不可变”也不是完全相同的概念。默认按对象身份比较的用户自定义实例可以可哈希,即使实例属性能够修改;如果自定义了按内容比较与计算哈希的行为,就必须维护相等性和哈希的约定。
3.1 相等键的合并
值相等且哈希一致的键不会在字典里各占一条独立记录:
mapping = {1: "integer", True: "boolean", 1.0: "float"}
print(len(mapping)) # 1
print(mapping[1]) # float
1、True 和 1.0 相等,并具有相同哈希值,后续赋值更新同一个条目的值。不同类型的业务 ID 如果必须区分,应在建模时明确表示类型,不能只依赖容器替业务区分。
4. 切片与遍历
切片使用 sequence[start:stop:step] 选取一段序列,包含 start,不包含 stop;步长不能为零。负数下标从末尾计算,负步长可以反向取值。
values = [0, 1, 2, 3, 4]
print(values[1:4]) # [1, 2, 3]
print(values[::2]) # [0, 2, 4]
print(values[::-1]) # [4, 3, 2, 1, 0]
print(values[9:20]) # []
单个下标越界会抛出 IndexError,切片边界通常会裁剪到有效范围。列表切片需要为结果中的元素建立新的外层列表,复制 k 个元素需要 O(k) 时间和额外空间,里面的元素对象不会因此被递归复制。
遍历列表时删除当前元素可能使后面的元素前移,导致漏查;遍历字典时增删键可能抛出 RuntimeError。如果目标是筛选结果,构造新容器更容易推演:
values = [0, 0, 1, 2]
positive = [value for value in values if value > 0]
print(positive) # [1, 2]
counts = {"ok": 2, "fail": 0}
nonzero = {key: count for key, count in counts.items() if count > 0}
print(nonzero) # {'ok': 2}
如果调用方持有原列表并要求它原地变化,可以使用 values[:] = positive 替换内容,而不是只给局部变量绑定新列表。共享关系仍按对象模型判断。
5. 去重与聚合
去重前应先确定是否保留顺序,以及重复的定义是什么。可哈希元素按首次出现顺序去重,可以利用字典保留插入顺序的性质:
case_ids = [3, 1, 3, 2, 1]
unique_ids = list(dict.fromkeys(case_ids))
print(unique_ids) # [3, 1, 2]
list(set(case_ids)) 可以去重,但不能承诺保留首次出现顺序。元素是字典等不可哈希对象时,应选择明确的业务键,例如每条记录的 id;若按完整结构比较,则需另行定义相等性与成本。
分组和计数用字典保留“键对应的累计状态”:
statuses = [200, 500, 200]
counts = {}
for status in statuses:
counts[status] = counts.get(status, 0) + 1
print(counts) # {200: 2, 500: 1}
get(status, 0) 在键不存在时返回零,不会仅因调用 get 就把零写入字典。上面的赋值才负责更新条目。
dict.fromkeys(keys, []) 会让所有键共享同一个默认列表。如果要按键收集不同结果,应使用字典推导式分别创建列表,或者使用 collections.defaultdict(list),而不是把共享列表当成独立数据。
6. 操作成本
下表针对 CPython 中内置容器的常见实现,假设元素比较和哈希计算本身的成本可视为常数。它是选择结构的依据,不是 Python 语言对所有解释器的耗时保证。
| 操作 | 常见成本 | 原因与限制 |
|---|---|---|
| 列表按下标读写 | O(1) | 根据位置访问元素引用 |
列表尾部 append | 摊还 O(1) | 大多数直接追加,扩容时需要更大存储 |
| 列表头部插入、删除 | O(n) | 后续元素需要移动 |
| 在列表中判断成员 | O(n) | 最坏需要逐个比较 |
| 字典按键查找、集合成员判断 | 平均 O(1),最坏 O(n) | 哈希定位,冲突和元素比较影响成本 |
| 列表切片 | O(k) | 为切片中的 k 个元素建立新列表 |
| 排序 | 最坏 O(n log n) | 排序还受比较或 key 计算成本影响 |
需要频繁从队头取元素时,使用 collections.deque 的 popleft(),避免反复执行 list.pop(0)。如果主要操作是按下标读取,列表更直接;不能因为双端队列头尾操作方便,就认定所有序列任务都该换成它。
list.sort() 原地排序并返回 None,sorted(iterable) 返回新列表。写 values = values.sort() 会让 values 变成 None,丢失原先通过这个名字访问列表的方式。
7 · 面试题卡片
本页题目与参考回答
7.1 Python 列表和元组有什么区别?
考察重点:序列、可变性、内部对象和可哈希条件。
参考回答
列表可以增删和替换元素,元组的元素绑定不可修改;两者都支持有序访问。 列表适合持续收集和调整元素,元组适合表示固定结构。
元组包含的可变对象仍可能修改,所以“元组不可变”不等于内部所有对象不可变。元组只有在全部元素可哈希时才能作为字典键;列表不可哈希。
7.2 Python 字典的键和值分别有什么要求?
考察重点:哈希与相等性约定、键的唯一性以及值的独立约束。
参考回答
键必须可哈希,值可以是任意对象。 字符串、整数和元素都可哈希的元组可作为键;列表、字典和普通集合不能。
哈希值在对象生命周期内应保持稳定,相等的键必须具有相同哈希。哈希冲突不表示键相等,还要比较相等性。字典值可以是列表或另一个字典,不受键的可哈希要求限制。
7.3 怎样对列表去重并保留首次出现的顺序?
考察重点:去重规则、顺序要求、可哈希条件与复杂度。
参考回答
元素可哈希时可以用 list(dict.fromkeys(values))。 字典保留插入顺序,重复键不新增条目,因此留下首次出现的顺序;通常需要 O(n) 时间和 O(n) 额外空间,具体取决于哈希与比较成本。
如果不需要顺序,可以转为集合;如果元素不可哈希,先明确是否按业务键去重,不能直接套用集合或字典。
7.4 Python 列表切片怎样处理边界,是否会复制内部对象?
考察重点:左闭右开、负步长、边界裁剪与浅拷贝。
相关内容:切片与遍历。
参考回答
列表切片包含起点、不包含终点,返回新的外层列表,内部元素仍可共享。 负步长可以反向选择,步长为零会报错,越界边界会按有效范围裁剪。
切片长度为 k 时需要 O(k) 的复制成本。对嵌套列表执行 copied = original[:] 后,替换 copied 的元素不改原外层列表,但修改共享的内层列表仍会影响两者。
7.5 大量成员判断和队头取值分别适合哪种容器?
考察重点:操作频率、时间成本与结构选择。
相关内容:操作成本。
参考回答
大量成员判断可优先考虑集合,频繁队头取值可考虑双端队列 deque。 集合平均 O(1) 的成员判断以元素可哈希为前提,还需计入建集合的成本;deque.popleft() 避免列表头部删除造成的整体移动。
如果数据量很小、只查一次,创建额外结构未必值得;需要按下标随机访问时仍应考虑列表。
8. 参考资料
- Python 3.12:内置类型、序列与映射
- Python 3.12:可哈希对象
- Python 教程:数据结构
- Python 3.12 collections:deque
- CPython 3.12:列表实现
- CPython 3.12:字典实现
来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明