Python字典为什么查得快详解(3个实验看懂哈希表底层)
作者:青春不朽512
系列:Python 底层设计逻辑|类型:原理拆解型|分发:CSDN / 博客园 / 腾讯云
字数:约 1600 字
结论先行:dict 查得快,不是因为它"会找",而是因为它"会算"——用键的哈希值直接算出该去哪个槽位拿数据,理论上一步到位,和字典里存了多少个键几乎无关。但这份快是有代价的:键必须可哈希、查找实际是 O(1) 平均而非最坏、还白放着约三分之一的内存。这三个坑,用三个实验看清楚。
1. 先建立直觉:dict 不是"表",是"带传送门的格子间"
列表查一个值,得从第 0 个开始比,最坏翻到底——O(n)。字典不干这事:
d = {'name': '张三', 'age': 18}
d['age'] # 不是遍历,是 hash('age') → 算出槽位 → 直接取
每个键进来,先算 hash(key),再按槽位数量取模,落进对应的格子。查找同理:算一次哈希,去对应格子拿。格子数远大于元素数时,一步就中。
2. 实验一:10 万个键,dict 和 list 差多少
import time
N = 100000
lst = list(range(N))
dct = {i: i for i in range(N)}
target = N - 1 # 查最后一个,list 的最坏情况
t0 = time.perf_counter()
_ = target in lst
t1 = time.perf_counter()
_ = target in dct
t2 = time.perf_counter()
print(f'list: {(t1-t0)*1000:.3f} ms')
print(f'dict: {(t2-t1)*1000:.3f} ms')
典型输出:list 要几百毫秒,dict 只要几微秒——差 2~3 个数量级。把 N 再放大 10 倍,dict 的时间几乎不变,list 线性变长。 这一条就是"哈希表 O(1)"的体感。
3. 实验二:哈希冲突是常态,不是事故
槽位总比键少,冲突必然发生。CPython 用开放寻址(探测下一个空槽)解决:
# 构造一批"爱打架"的键:哈希值对 8 同余
keys = [i for i in range(64) if hash(i) % 8 == 0]
d = {k: k for k in keys}
print(len(d)) # 8 个键全在,查找依然正确
为什么重要:冲突不丢数据,但会让查找从"一步"变成"探几步"。键太集中(或有人恶意构造碰撞键)时,dict 会退化成慢查找——这也是为什么 PYTHONHASHSEED 默认随机化:让攻击者算不出你的字符串哈希。
4. 实验三:为什么 list 不能做键
d = {}
d[[1, 2]] = 'x' # TypeError: unhashable type: 'list'
d[(1, 2)] = 'x' # tuple 可以
print(d) # {(1, 2): 'x'}
为什么翻车:list 内容能改,改了哈希就变了——今天算出的槽位在 A,明天值一改槽位算到 B,字典就永远找不到它了。所以 Python 要求键可哈希:hash() 能算、且对象生命周期内哈希不变(不可变)。tuple 元素全不可变时才可哈希:([1],) 照样报错。
5. 两个反直觉点
① dict 有序且"记性"在键上:Python 3.7+ dict 保序,但这是实现细节顺带的福利,别依赖它做业务逻辑。
② 空 dict 不省多少,装满才扩容:dict 元素超过容量的 2/3 会触发扩容(重新哈希全部键,一次性 O(n))。所以预分配大字典没有意义——扩容均摊下来仍是 O(1)。
6. dict vs set vs list(选型对照)
| 维度 | dict | set | list |
|---|---|---|---|
| 查找 | O(1) 平均 | O(1) 平均 | O(n) |
| 键/元素要求 | 可哈希 | 可哈希 | 无要求 |
| 存什么 | 键→值 | 去重的键 | 有序序列 |
| 内存 | 偏高(约 1/3 空闲槽) | 偏高 | 紧凑 |
| 典型场景 | 映射/缓存/计数 | 去重/成员判断 | 顺序处理 |
对照结论:按 key 取值 → dict;只问"在不在" → set;要顺序、要索引 → list。别用 list 做"存在性检查",数据一上万就是灾难。
到此这篇关于Python字典为什么查得快的文章就介绍到这了,更多相关Python字典查得快内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
