假设要根据名字查成绩,最朴素的做法是用两个列表,靠下标一一对应:

names = ['Michael', 'Bob', 'Tracy']
scores = [95, 75, 85]# 查 Bob 的成绩:先遍历 names 找到索引 1,再 scores[1] 取 75
每次查找都要从头到尾遍历列表。计算机科学里管这叫 O(n)——3 个人最坏找 3 次,10 万个人最坏找 10 万次,数据越多越慢。
d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
d['Bob'] # 75,一步到位,不管 d 里有多少条数据
Dict 不靠"遍历",靠**"算"**。就像查字典——你不是从第一页翻到最后一页找"哈希"这个词,而是翻到目录,按拼音 H 一查,直接跳到对应页码。dict 的名字(dictionary)就是这么来的。不管字典有 100 页还是 10000 页,查目录都是一次操作——这就是 O(1),和数据量无关。
Dict 的底层是哈希表(Hash Table),核心逻辑是"不靠找,靠算"。
存入数据时:
key(如 'Bob')→ 哈希函数 → 哈希值(如 284719)→ 内存存储位置 → 放入 value
取数据时,对同样的 key 再算一次哈希,得到同样的位置,直接取走 value。
打个比方:快递柜。哈希函数就像快递柜系统——用你的手机号算出一个柜子编号(比如 38 号)。你取件时不需要把小区所有柜子都打开找一遍,你知道就是 38 号,走过去开门就行。不管是 50 个柜子还是 5000 个柜子,你取一次都是"走到 38 号"这一个动作。
Dict 有代价——吃内存。哈希表需要预留很多空位来减少"冲突"(两个 key 算到同一个位置,这个话题下次展开)。
| Dict(哈希表) | List(顺序存储) | |
|---|---|---|
| 查找 | O(1),直接算位置 | O(n),逐个找 |
| 内存 | 大,预留空位 | 小,紧凑排列 |
| 顺序 | 无(按哈希值分布) | 有(按插入顺序) |
你一定见过这个报错:
d = {}
d[[1, 2, 3]] = 'a list'
# TypeError: unhashable type: 'list'
哈希函数有个铁律:同一个输入,必须永远产出同一个输出。 否则存的时候算出一个位置,取的时候算出另一个位置,数据就丢了。
那什么输入能保证永远不变?
'Bob'——字符串对象创建后就固定了,你没法改它42——整数永远是 42[1, 2, 3]——但你可以 append、pop、改某个下标的值!想象你允许列表当 key:
1. d[[1,2,3]] = 'hello' → 对 [1,2,3] 算哈希,存入位置 A
2. key.append(4) → key 变成了 [1,2,3,4]
3. d[[1,2,3,4]] → 对 [1,2,3,4] 算哈希,得到位置 B
4. 数据在 A,你在 B 找 → KeyError,而且整个哈希表结构被打乱
这就是为什么 Python 规定:key 必须是可哈希的,也就是不可变类型。
| 可当 key(不可变) | 不能当 key(可变) |
|---|---|
str、int、float、bool | list(可增删改) |
tuple(元素也都不可变才行) | dict、set |
tuple 有个坑:(1, 2, 3) 可哈希,(1, [2, 3]) 不行——因为里面藏了一个 list。
Set 和 dict 底层是同一套哈希表实现,唯一区别:dict 存 key + value,set 只存 key。
因为哈希表的 key 不能重复,所以 set 天生去重:
s = set([1, 2, 3, 2, 5])
print(s) # {1, 2, 3, 5} —— 重复的 2 自动消失
基本操作用 add() 和 remove(),不展开了。
需求:两个列表找共有元素,再去重合并。
手写循环版:
a = [1, 2, 3, 2]
b = [2, 3, 4]# 找共有元素
common = []
for x in a:
if x in b and x not in common:
common.append(x)
print(common) # [2, 3]# 去重合并
merged = []
for x in a + b:
if x not in merged:
merged.append(x)
print(merged) # [1, 2, 3, 4]
十几行,两层判断,费眼 。
Set 版:
a = [1, 2, 3, 2]
b = [2, 3, 4]sa = set(a) # {1, 2, 3}
sb = set(b) # {2, 3, 4}sa & sb # 交集 → {2, 3}
sa | sb # 并集 → {1, 2, 3, 4}
& 就是"两个集合都有的",| 就是"合起来去重"——中学数学的集合运算,Python 里两个符号搞定。
sort() 和 replace() 的故事# 代码 A
a = ['c', 'b', 'a']
print(a.sort()) # ?
print(a) # ?# 代码 B
s = 'abc'
print(s.replace('a', 'A')) # ?
print(s) # ?
# A
print(a.sort()) # None ← 为什么不是排序后的列表?
print(a) # ['a', 'b', 'c'] ← 原列表确实变了# B
print(s.replace('a', 'A')) # 'Abc'
print(s) # 'abc' ← 没变!
这里有一个很容易混淆的点:变量名只是贴在对象上的标签,不是对象本身。
a.sort() 是可变对象的行为——直接改了列表对象自身的内容(把元素重排了),返回 None 是在说"别找我要新对象,我原地改完了"。打个比方:你在白板上擦掉 c b a 重写成 a b c——白板还是那块白板。
str.replace() 是不可变对象的行为——字符串对象一旦创建就不能改,所以 replace 只能创建了一个新字符串 'Abc' 返回给你。原来的 'abc' 纹丝不动。就像你有一张写着 abc 的纸,replace 不是拿涂改液去改——它拿了一张新纸,上面写着 Abc,递给你。
| 不可变 (Immutable) | 可变 (Mutable) |
|---|---|
str、int、float、bool | list、dict、set |
tuple(元素也都不可变时) |
这张表和前面"什么能当 dict key"是完全对应的——不可变的才能哈希,才能当 key。一环扣一环。
四个核心原理:
&、并集 | 是核心用法list.sort() 返回 None 不是 bug,是设计最后留一个问题:dict 有 1000 万个 key,查询还是 O(1) 吗? 理论上是——但内存不够开始 swap 到硬盘时,情况就变了。哈希冲突与动态扩容,下次聊 。