首页 › programming › 哈希表神课深读:碰撞的艺术

一条白板神课:哈希表凭什么敢说自己O(1)

概述

程序员每天都在用字典、Map、HashMap,但很少有人能说清楚:为什么一次查找能不比较任何元素就直接命中?英国老牌科普频道 Computerphile 有一堂用一块白板讲完哈希表本质的经典课《Hash Tables and Hash Functions》,由诺丁汉大学计算机系的大卫·布雷尔斯福德(David Brailsford)教授出镜主讲。这条视频没有动画、没有花活,却常年被各国程序员当作理解哈希表的第一课:它把“数组为什么快、哈希函数为什么必不可少、冲突为什么注定发生、扩容为什么必然到来”这条论证链讲得干净利落。本文以这堂课为骨架,补上它没讲的现代工业细节与安全暗战,让你看完不只“懂了”,还能懂得比视频更深一层。

一、视频基本信息

项目 内容
视频标题原文 Hash Tables and Hash Functions
中文译名 哈希表与哈希函数
频道 Computerphile
出镜人 David Brailsford(诺丁汉大学计算机科学教授,Computerphile 常驻主讲人之一)
发布时间 2010年代中期,具体日期未能实时核验
热度 频道早期数据结构类经典之一,常被工程社区反复推荐

注:本次撰写环境网络受限,未能实时抓取视频页面,播放量等易变数据此处从略;频道名、出镜人、系列归属均为该频道长期稳定信息。

二、这条视频讲了什么:一条四步论证链

布雷尔斯福德的讲法有个鲜明特点:从“你已经懂的东西”出发,一步步逼出“你不得不引入的东西”。整堂课实际上是四步递进的论证。

第一步:直查很快,但前提苛刻。 他先在白板上画一个朴素数组:如果键本身就是 0 到 n 的下标,存取就是一步到位的 O(1)。问题是真实世界的键是用户名、邮箱、订单号——它们不是数字,更不是连续的数字。想用“下标直查”的爽感,就必须先把任意键变成数组下标。

第二步:哈希函数是那座桥。 把任意长度的键(字符串、结构体、对象)喂给一个确定性函数,吐出一个固定范围内的整数,再对桶数 m 取模,就得到下标。视频强调两个关键词:确定性(同一个键每次必须得到同一个下标,否则存进去就找不回来)和均匀性(好的哈希函数让键尽量撒匀,不扎堆)。注意这一步只要求“快且散”,不要求“可逆”——哈希表不是加密,丢信息是常态。

第三步:冲突是注定的,不是意外。 这是全课最有味道的一处。桶只有 m 个,可能的键却有天文数字个,鸽笼原理保证冲突必然发生——哪怕你的哈希函数再完美,两个不同的键也可能落进同一个桶(更不用说 m 远小于键空间时的“生日悖论”效应)。于是只剩一个问题:冲突来了怎么办?视频给出两大流派:链地址法(每个桶挂一条链表,冲突的元素往后串)和开放寻址法(桶被占了就按某种规则往下找下一个空位,比如线性探测、二次探测、双重哈希)。这一“分叉”决定了后续一切工程抉择。

第四步:装载因子与扩容。 元素越塞越多,链表越来越长、探测越来越远,性能从 O(1) 滑向 O(n)。于是引入装载因子(元素数 ÷ 桶数)作为体检指标:超过阈值(工程上常取 0.7 上下)就申请一个更大的桶数组(通常翻倍),把所有元素重新哈希一遍搬过去。视频点到这里为止——但这恰恰是最值得深挖的地方,后文详述。

三、知识与理论溯源:视频背后七十年的积累

这堂课是科普,但它的每一步都踩在真实的学术与工业史节点上。逐层往下溯源,能看到一条完整的演进线。

思想起点(1953)。 哈希表的最早构想通常追溯到 IBM 的汉斯·彼得·卢恩(Hans Peter Luhn)1953 年 1 月的一份内部备忘录,他在设计文献检索系统时提出了“用键的函数计算地址、用链表处理冲突”的方案——链地址法比大部分人的想象要老得多。

理论定音(唐纳德·克努特)。 开放寻址法的性能分析由高德纳在《计算机程序设计艺术》第三卷第 6.4 节系统完成,线性探测的“一次堆积”(clustering)现象——被占桶连成片、越连越长——从此成为教科书标准内容。布雷尔斯福德在白板上随手画的那种“占位往下挪”,背后是克努特用概率模型算出的期望探测次数曲线。

概率与安全的分水岭(1979 / 2011 / 2012)。 卡特与韦格曼(Carter & Wegman)1979 年提出全域哈希(universal hashing),证明随机选择哈希函数族可以把对手锁在概率账本之外。这一理论在 2011 年变成了现实的救火队:当年混沌计算机大会(28C3)上,亚历山大·克林克与尤利安·瓦尔德(Alexander Klink & Julian Wälde)做了著名的《对 Web 应用平台的高效拒绝服务攻击》演讲,展示攻击者只需构造几千个哈希值全相同的表单键塞进一次 POST 请求,就能让 PHP、Python、Java、Ruby 等语言内置的哈希表退化成 O(n) 链表,用几十 KB 流量烧干一个 CPU 核(PHP 侧随后登记为 CVE-2011-4885)。这就是哈希洪水攻击(hash flooding),一堂“理论上的均匀性假设可以被恶意输入击穿”的公开课。各语言的整改殊途同归:Python 引入随机化哈希与 PYTHONHASHSEED 环境变量;密码学家阿马松与伯恩斯坦(Aumasson & Bernstein)2012 年发表的 SipHash——专为哈希表防碰撞设计、带每进程随机密钥的短输入哈希——先后成为 Rust 标准库、Python、Ruby 等的默认选择。

现代工业形态(2016 / 2017 / 2019)。 视频里那条“白板哈希表”在今天长成了完全不同的样子:Python 核心开发者雷蒙德·赫廷杰(Raymond Hettinger)2016 年的演讲《Modern Dictionaries》公开了 CPython 紧凑字典设计——稀疏下标数组 + 紧凑条目数组两层结构,省内存约三成,附带效果是字典保序,这一特性在 Python 3.7 起写入语言规范。Java 8 通过 JEP 180 给链地址法装上保险丝:单个桶链表长度达到 8 就“树化”为红黑树,把恶意碰撞下的最坏情况从 O(n) 压到 O(log n)。谷歌的库鲁昆迪斯(Matt Kulukundis)在 CppCon 2017 的演讲《Designing a Fast, Efficient, Cache-friendly Hash Table, Step by Step》展示了 SwissTable:以 SIMD 按组探测元数据、把开放寻址做到缓存友好,这套实现经开源的 hashbrown 移植后,在 Rust 1.36(2019 年)成为标准库 HashMap 的底层。

四、批判性审查:哪些话不能照单全收

这堂课作为入门是优秀的,但按深读标准,有四处“账面简化”值得较真。

其一,“O(1)”是带脚注的数字。 它依赖两条脚注:平均情况分析(不是最坏情况)与简单均匀哈希假设(键均匀散布——这在善意输入下近似成立,在哈希洪水攻击面前直接破产)。视频没有展开这两条脚注,而工业界过去十几年的大量工程(随机种子、SipHash、树化)几乎都是在为这两条脚注打补丁。

其二,链地址法与开放寻址法的对比是“2010 年视角”。 视频把两大流派讲成并列选项,这在教学上成立;但现代硬件改变了权重——链地址法每个节点一次指针跳转,缓存极不友好,这正是谷歌、Rust 阵营转向 SwissTable 式开放寻址的根本原因。今天新写高性能哈希表,链地址法基本出局。

其三,扩容被一句带过,实则是延迟杀手。 “满了就翻倍重哈希”在演示里是几笔白板字,在真实系统里是一次让所有键重新搬家的大动作:百万级条目的表扩容会带来数十毫秒级的停顿尖刺,对延迟敏感的服务(交易、游戏、实时通信)必须在容量规划里预分配、或采用渐进式搬迁(Redis 的做法就是分多次增量 rehash,避免一次卡死)。视频不提,是因为白板装不下,但读者不能不知道。

其四,视频完全没有涉足安全维度。 对 2010 年代中期的科普课这无可厚非,但“哈希表”这个话题在今天的安全语境里,一半的故事在对抗上:可预测的哈希 + 用户可控的键 = 可被利用的放大器。这也是“多懂一层”最值钱的一层。

五、认知提升与实用启示

1. 默认哈希不是安全哈希,键来自用户时必须换武器。 任何把用户输入直接当 HashMap/字典键做聚合、缓存的代码,都要确认底层用了带随机种子的哈希(Rust 的 RandomState/SipHash、Python 3.4+ 的默认随机化)或树化保险(Java 8+)。自研哈希函数、或在跨进程/跨版本场景下要求“哈希值稳定”而改用固定种子时,要意识到自己主动拆掉了防洪水闸门,需要另行设计限流。

2. 把 rehash 当成一次可观测的延迟事件,而不是隐形成本。 高并发服务在容量规划阶段就应预估条目规模并预分配(如 Go 的 make(map, n)、Rust 的 with_capacity);对停顿敏感的场景,优先选择渐进式 rehash 的实现。监控里给“哈希表大小/装载因子”留一个指标位,比出事故后翻代码快得多。

3. 不要脑补“各语言的 map 都一样”。 Python 字典保序、开放寻址;Java HashMap 链地址 + 树化、遍历无序;Go 的 map 遍历刻意随机化且并发读写直接报错;Rust HashMap 每次进程启动换哈希种子——同一份数据、同一个名字,行为契约完全不同。换语言时重读一遍容器文档的这几行,是性价比最高的防坑动作。

六、一张图看懂哈希表的命运分叉

flowchart TD
    A["任意键<br/>用户名 / ID / 对象"] --> B["哈希函数<br/>定长整数摘要"]
    B --> C["取模映射<br/>落进 m 个桶"]
    C --> D{"桶里有人吗"}
    D -->|空桶| E["直接写入<br/>平均 O(1)"]
    D -->|撞车| F["冲突分流"]
    F --> G["链地址法<br/>桶后挂链表"]
    F --> H["开放寻址法<br/>按探测序列找位"]
    G --> I["恶意键全部同哈希<br/>链表退化 O(n)"]
    H --> I
    I --> J["工业防御<br/>随机种子 SipHash 树化"]
    E --> K["装载因子超限<br/>翻倍扩容重哈希"]
    J --> K
    K --> L["现代形态<br/>紧凑字典 / SwissTable"]

延伸阅读

  1. Tom Scott《Hashing Algorithms and Security - Computerphile》—— 同频道姊妹篇,从安全视角讲哈希函数为什么会“撞”,与本课正好互补。
  2. Raymond Hettinger《Modern Dictionaries》(PyCon 2016 演讲,YouTube 可查)—— CPython 紧凑字典作者亲述,看理论哈希表如何长成工业品。

参考来源

  1. Computerphile 频道 — 本文主视频《Hash Tables and Hash Functions》及 Tom Scott 哈希安全课所在频道
  2. JEP 180: Handle Frequent HashMap Collisions with Red-Black Trees — Java 8 链表树化的官方提案
  3. SipHash 官方页面 — Aumasson 与 Bernstein 的防碰撞哈希函数,Rust 等语言默认哈希
  4. Python 官方文档:PYTHONHASHSEED — 哈希随机化的官方说明
  5. IANA 时区库说明页外的本条替代:CppCon 2017 SwissTable 演讲 — Matt Kulukundis《Designing a Fast, Efficient, Cache-friendly Hash Table》收录于 CppCon 官方频道

注:以上链接为各主体官方长期稳定入口;因本次采集环境网络受限,未能对页面做实时可达性核验。


本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入