Python 的 dict 为什么查询是 O(1)?拆开哈希表、扰动函数与扩容阈值

很多人背过一句话:"Python 的字典查询是 O(1) 的。"但真到了面试或者调性能的时候,这句话经不起两问:凭什么是常数?常数就不是要算哈希吗?哈希不要时间?今天我们就把 CPython 3.13 的 dict 拆开,用实测把"O(1)"钉死。

一、先上实测:规模涨 1000 倍,查询时间纹丝不动

我在 CPython 3.13.14 上跑了一组对比:dict.get 和 list.index 找中间那个元素,规模从 100 一路拉到 10 万。

ini 复制代码
n=    100  dict.get=0.0001ms  list.index=0.0007ms  speedup=6x
n=   1000  dict.get=0.0001ms  list.index=0.0061ms  speedup=47x
n=  10000  dict.get=0.0001ms  list.index=0.0607ms  speedup=474x
n= 100000  dict.get=0.0001ms  list.index=0.6090ms  speedup=4743x

重点看 dict.get 那一列:从 100 到 10 万,它一直停在 0.0001ms(约 0.1 微秒) ,根本不随数据量变化。而 list.index 是线性上涨的------这就是 O(1) 和 O(n) 最直白的脸对脸。当数据到 10 万条,列表查找比字典慢了 4700 多倍。

二、那哈希计算不耗时吗?

耗,但它是"一次"的,不随表里有多少元素变。CPython 的 dict 底层是一个开放寻址(open addressing)的哈希表,找值分两步:

  1. 对 key 调用 hash(),得到一个整数;
  2. 用这个整数算出在表里的下标,直接去那个槽位取。

关键在第 2 步。CPython 不是直接拿哈希值当下标,而是经过一次 扰动(perturbation) 处理,再和当前表大小做位与。所以无论表里是 100 个还是 10 万个元素,你都只做一次哈希 + 一次定位,和总数无关。这也是为什么 dict.get 的时间不随 n 变------它压根没"遍历"这一说。

三、哈希冲突怎么办?------扰动函数在这里救场

光算下标还不够。两个不同的 key 完全可能算出同一个槽位,这就是哈希冲突。CPython 用的就是开头说的"扰动函数":把哈希值的高位也卷进来参与寻址,冲突时按一个固定序列往后探测下一个空槽(开放寻址)。

实测上,整数 key 和字符串 key 的查询代价几乎一样:

ini 复制代码
int key get  = 0.1us
str key get  = 0.1us

说明在常规数据量下,冲突带来的额外探测成本被压得极低,体感上就是"常数时间"。

四、创建字典不是 O(1),别混了

O(1) 说的是查询。创建是另一回事,它得一个个算哈希、插槽位,所以是 O(n):

ini 复制代码
n=   100  create=0.006ms
n=  1000  create=0.062ms
n= 10000  create=0.725ms

所以"字典查询 O(1)"和"字典构建 O(n)"要分开记。一个常见误用是:为了查一次,现造一个大字典------那构建的 O(n) 早把查询的 O(1) 优势吃光了。真要省,用 set 做成员判断,或预先建好复用。

五、什么时候 O(1) 会"翻车"

两种典型情况会退化:

  • key 的 __hash__ 写得差:比如所有对象返回同一个常数,哈希表退化成链表,查询变 O(n)。
  • 装载率过高触发扩容:CPython 在装载率到约 2/3 时会整体 rehash 扩容(新表大小取大于当前 2 倍的 2 的幂)。单次插入偶尔会"卡"一下,但均摊下来插入仍是 O(1)。

小结

dict.get 在 CPython 3.13 下,10 万规模仍稳在 0.1 微秒,和 100 条时一模一样------这就是 O(1) 的实证。它靠的是"一次哈希 + 一次定位"的开放寻址,扰动函数把冲突成本压平;查询快,但创建是 O(n),别把两者混为一谈。下次有人问"为什么用字典不用列表存查找",把这组 4700 倍的数据甩过去就够了。

相关推荐
像风一样的男人@2 小时前
python --基于scp的文件传输助手
开发语言·python
用户0332126663672 小时前
使用 Python 在 Word 文档中添加、修改和删除变量
python
专注仿真2 小时前
动态规划算法详解
人工智能·python·大模型训练·控制算法
全栈陈序员3 小时前
【Python】基础语法入门(二十五)—— 网络编程入门:用 socket 与 requests 联网
python
adminwolf3 小时前
GoLiveChat:Open-Source Live Chat Software for Website
python
工具派4 小时前
PDF签名贴上去为什么发虚?尺寸、DPI与坐标的边界实测
java·python·pdf
谙忆4 小时前
PNG改成ICO为什么不能用:用Python检查多尺寸目录与256px帧
开发语言·python
长空任鸟飞_阿康4 小时前
如何设计支持 Skills 的 Agent?一套可扩展的四层架构
python·ai·架构·skills
滴啦嘟啦哒4 小时前
【具身笔记】【原理探究】一、对PI05源码的实践笔记
笔记·python·vla·具身