跳到主要内容
约 7 分钟5 张卡

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. 参考资料​

来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明