你每天打开的每一个网页、收发的每一封邮件、手机里每一张照片、甚至你正在读的这一行字,背后几乎都站着同一个 1952 年的算法——哈夫曼编码(Huffman Coding)。它安静地躲在 gzip、zip、PNG、甚至 HTTP 响应头的 Content-Encoding: gzip 里,几十年如一日地把人类产生的海量文本压掉三到四成的体积。
而 Tom Scott 的这支短片,是我见过把"变长编码为什么不会乱套"这件事讲得最干净利落的入门材料。它不堆公式,而是用一个反直觉的洞察贯穿全片:字母不平等,常见字母该更短,而且——关键是——变长编码根本不需要分隔符。 这后半句,才是真正让人"哦!"的那一下。本文除了拆解视频里的每一步逻辑,还会带你走到视频没说的地方:为什么真实的 gzip 不是只用哈夫曼、为什么它和 LZ77 必须组队出场、以及 arithmetic coding 到底比它强在哪。
| 项目 | 内容 |
|---|---|
| 视频标题 | How Computers Compress Text: Huffman Coding and Huffman Tree |
| 频道 | Tom Scott(订阅超 550 万) |
| 时长 | 约 10 分钟 |
| 播放量 | 约 203 万次 |
| 发布日期 | 2017-09-11 |
| 核心主题 | 变长编码、前缀码与哈夫曼树的构造 |
视频开篇就抛出一个很多人没想过的事实:ASCII 用 7 个 bit(实际存 8 个)表示一个字符,无论这个字符有多常见。 字母 e 在英文文本里出现频率约 12.7%,而 q 只有 0.09%——一个比另一个常见一百多倍,可它们在硬盘上都老老实实占满同样的 8 个 bit。
这是设计上的"绝对公平",但对存储是灾难。Tom Scott 把它比作"给每个学生发一模一样大的书包,不管他装的是一块橡皮还是整套百科全书"。公平,但浪费。(以上为视频原话的转述。)
我的延伸分析: 这种浪费的根源是 ASCII 诞生于电报/打孔卡时代,当时硬件更看重"等长好对齐、好寻址",而不是省空间。变长编码的思想其实更古老——莫尔斯电码(Morse code)早在 1840 年代就已经是变长的了,只是它用了一种很笨的办法来解决"变长怎么分词"的问题。
视频用莫尔斯电码做了一个绝妙的引子:E 是一个点(·),T 是一个划(—),而 Q 是 --·-。常见字母短,罕见字母长——这正是压缩的直觉。Samuel Morse 当年据说数过印刷厂字模盒里还剩多少铅字来估计频率,频率越高、码越短。
但莫尔斯有个要命的麻烦:它必须靠"停顿"来分隔字母。 你听到 ·· ·—,如果中间没有停顿,你根本分不清是 EE 还是 IA 还是 IEA——因为码长不固定,边界是模糊的。换句话说,莫尔斯牺牲了"自同步"能力,把分词的活儿甩给了人耳和时钟。
我的延伸分析: 莫尔斯的困境,本质上就是"变长编码"和"无歧义解码"之间的张力。计算机可没有"人耳"来感知停顿——所有的 0 和 1 是连成一串的。所以问题被精炼成一句话:能不能设计一套变长码,让一串 01 拼在一起也不会有歧义? 哈夫曼的天才,正是给出了"能"的答案,而且是最优的答案。
这是整支视频、也是整个算法最核心的"啊哈"时刻。Tom Scott 告诉你:只要保证"没有任何一个字符的编码,是另一个字符编码的前缀",你就不需要任何分隔符。 这叫前缀码(prefix code),也叫前缀无关码。
举例:如果 E = 0,那么任何以 0 开头的码都不能用作别的字符,因为只要解码器读到第一个 0,它就知道——这只能是 E,立刻输出,继续读下一位。同理,如果 T = 10,那以 10 开头的更长码也都被"占用"了。解码器永远能在读到某个码的瞬间唯一确定它对应哪个字符,不需要回看、不需要分隔。
我的延伸分析: 这里藏着一个很美的对偶——前缀码和"二叉树"是同一件事的两副面孔。把每个字符挂在二叉树的叶子节点上,从根走到叶子,左拐记 0、右拐记 1,每个叶子得到的路径就是它的编码。"没有任何码是另一个码的前缀",等价于"所有字符都只放在叶子节点上、内部节点不放字符"。 因为一旦某个字符占了内部节点,那经过它的所有叶子就都成了它的"前缀"。这个"叶子属性"保证了一串 01 能从左到右贪婪地、唯一地切分。理解了这棵树,你就理解了哈夫曼的全部。
视频随后进入实操。哈夫曼编码要工作,必须先知道"谁常见、谁罕见",所以第一步是扫描全文、统计每个字符出现的次数。假设你有一段文本,统计出 a 出现 45 次、b 13 次、c 12 次、d 16 次、e 9 次、f 5 次(这是数据结构教科书里的经典例子,Tom Scott 用的是类似的小样本演示)。
我的延伸分析: 这一步揭示了哈夫曼编码的一个本质特征——它是"自适应于内容"的压缩。同一套字母表,不同内容(英文小说 vs C 源码 vs DNA 序列)会统计出不同的频率,从而长出不同的树、得到不同的编码表。这也意味着:解码方必须知道这棵树长什么样,否则拿到 01 串也解不出来。这就是后面"两份文件"问题的根源。
这是视频的高潮。Tom Scott 边画边讲:把所有字符当作一棵只有根的"子树",放进一个按频率排序的优先队列(最小堆,min-heap)里。然后反复做同一件事——
结果是:频率越低的字符,被合并得越早,长在树的越深处,编码越长;频率越高的字符,撑到最后才被合并,长在越浅的位置,编码越短。 e 这种高频字符可能只分到 1–2 bit,而 q、z、f 这种罕见字符可能要 5–6 bit。这跟莫尔斯的直觉完全一致,但它是用一棵自动长出来的树实现的,而且——接下来的事实很关键——它是给定频率分布下的最优前缀码。
我的延伸分析: 为什么"合并最小的两个"就是最优?这是贪心算法里少有的、可以被严格证明最优的案例(证明由 Huffman 本人 1952 年给出,核心是交换论证 exchange argument:任何最优树里,两个最罕见的字符一定是兄弟且在最深层,否则可以交换而得到更优的树)。换句话说,你不需要搜索、不需要回溯、不需要试错,一遍扫描 + 一遍建树,就拿到了"前缀码世界里的理论最优解"。算法复杂度是 O(n log n),n 是字符种类数——通常就 256 种左右,几乎瞬时。
有了树,编码就是把每个字符替换成"从根到它的路径",比如 a→0、b→101、c→100、d→111、e→1101、f→1100(示意)。解码则反过来:从根出发,逐位读 01,每读一位就走左/右,一旦走到叶子就输出字符、回到根继续。一个对编码、一个对树,双向奔赴。
我的延伸分析: 这里有个常被忽略的工程细节——编码和解码用的是同一棵树,但这棵树本身也得存下来。 这就引出了哈夫曼编码最经典的"两份文件"困境。
视频末尾点出一个关键的"但是":哈夫曼编码产出的不是一个文件,而是两个——压好的 01 串,加上那棵"字典树"本身。 如果你只压缩一句话,字典树可能比原文还大,反而"越压越大"。所以哈夫曼编码只在数据量足够大、且字符频率分布不均时才划算。
我的延伸分析: 这正是真实世界为什么不全用哈夫曼的原因之一。实际的 gzip/zip 面对这个问题的办法有几种:用固定的预定义码表(如 DEFLATE 的 fixed Huffman codes,省去存树,但次优);或对小块数据用稍微过简化的树;又或者在树本身的存储上做二次压缩。理解了这一点,你才会明白为什么"压缩小文件反而变大"是每个程序员迟早会踩的坑——这不是 bug,是信息论的铁律。
视频讲到这里就收尾了,但如果你就此止步,会错过真正改变世界的那一步。哈夫曼编码有一个绕不过去的天花板:它只能给"整数 bit"。 如果某个字符的理论最优长度是 2.3 bit,哈夫曼只能给它 2 或 3 bit,永远有"凑整"的浪费。
所以现代压缩都是"组合拳",最经典的就是 DEFLATE = LZ77 + 哈夫曼:
两层叠加,才有了 gzip、zip、PNG(PNG 的图像数据流就是 DEFLATE 压的)。而你手机浏览器收到的每个 Content-Encoding: gzip 响应,走的也是这条路。Brotli(Google 推、现代 HTTPS 优先用)更进一步:内置一套为 Web 文本(HTML/CSS/JS)预训练的巨型字典 + 二阶哈夫曼,压缩率比 gzip 再高 15–25%。
我的延伸分析: 而 arithmetic coding(算术编码)是另一个方向——它突破"整数 bit"限制,把一整段消息编码成 0 到 1 之间的一个小数,理论上能逼近香农极限(Shannon limit),比哈夫曼更省。但它编码慢、专利曾长期阻碍普及,所以日常你摸到的还是哈夫曼家族。可以说,哈夫曼编码是"够好且极快"的那个甜点——它不是理论上最强的,却是性价比之王,这大概也是它能活到 2026 年的原因。
flowchart TD
A["原始文本<br/>定长 8 bit/字符"] --> B["① 频率统计<br/>谁常见 谁罕见"]
B --> C["② 构造哈夫曼树<br/>反复合并最小的两个"]
C --> D{"前缀码 / 二叉树叶子属性<br/>任何码都不是另一码的前缀"}
D --> E["③ 编码:高频字符短 低频字符长"]
D --> F["④ 解码:从根逐位走 遇叶子即输出<br/>无需分隔符"]
E --> G["输出:压缩后的 01 串 + 字典树"]
G --> H{"两份文件问题<br/>小数据可能越压越大"}
H -.真实工程.-> I["DEFLATE = LZ77去重 + Huffman变长"]
I --> J["gzip / zip / PNG / HTTP gzip"]
I -.进阶.-> K["Brotli:预训练字典 + 二阶Huffman<br/>arithmetic coding:逼近香农极限"]
style D fill:#fff3cd,stroke:#d39e00,stroke-width:2px
style I fill:#d1ecf1,stroke:#0dcaf0,stroke-width:2px
如果你是后端 / Web 工程师:你应该真的理解 Content-Encoding: gzip 在干什么。开启 gzip 能让 HTML/JSON 响应体缩到原来的三成左右,直接省带宽、降首屏延迟;而对于纯文本 API(尤其是大量重复字段的 JSON),考虑上 Brotli(br),压缩率再高一截,但要配 HTTPS。注意:对已经压缩过的二进制(jpg、mp4、webp)开 gzip 是纯浪费——它们内部早已用更狠的算法压过,再上 gzip 既不省空间,又白烧 CPU。这个判断力,就来自你懂"压缩是有适用域的"。
如果你在做存储 / 数据库:明白"压缩的代价是字典"很重要。对大量重复的日志、时序数据,列式存储 + 字典编码(本质就是哈夫曼/LZ 思想的演化)能带来数量级的压缩比;但对每条都不同的小记录,别盲目套压缩器。遇到"压完反而变大"不要慌,那是统计样本不足。
如果你是算法学习者:哈夫曼是理解"贪心算法 + 优先队列"最经典的案例,也是信息论入门的最好抓手。建议你手写一遍建树过程:用最小堆,O(n log n)。LeetCode 上"根据字符出现频率排序"等题就是它的简化版。再进一步,去读 DEFLATE 的 RFC 1951,你会看到哈夫曼如何和 LZ77 缝合成真实工业标准。
认知升级:这个视频最值钱的一课,其实不在哈夫曼本身,而在那个"前缀码"的洞察——很多看似需要额外标记(分隔符)的问题,其实可以通过约束结构(让码互相不互为前缀)来彻底消灭。 这种"用结构换掉约定"的思维方式,在 API 设计、协议设计、数据建模里处处通用。会这么想问题的人,设计的系统天然就更简洁。
哈夫曼编码用一棵按频率自底向上长出来的二叉树,让常见字母更短、罕见字母更长,并靠"前缀无关"这一条性质消灭了所有分隔符——它是变长压缩的性价比之王,也是理解整个 gzip/zip/PNG 世界的那把钥匙。
本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。
curl 直连 DuckDuckGo 与 YouTube,成功抓取到真实搜索结果与视频元数据。JsTptu56GM8),实测 2,003,694 次播放、发布于 2017-09-11、频道 Tom Scott。youtube_transcript.py,因云 IP 被 YouTube 拦截未取到(符合任务预期降级方案),基于已核实内容完成深度解读。<20 字,domain = programming。[!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 等场景全覆盖。
