跳转到内容

Huffman 编码

是什么

Huffman 编码是一套按出现频率给符号分配二进制位的压缩方法。日常类比:

写中文的时候,“的”、“了” 出现得最多,给它们配 1 位代号;“鳙”、“魉” 几乎不出现,给它们配 12 位代号。平均下来,整段文字的总位数比”每个字一律 8 位”少很多。

你的电脑里现在跑着的 ZIP / GZIP / PNG / JPEG / MP3,底层全都还在用这个 1952 年的算法

它的核心是一句话:频率高的符号,给短码;频率低的,给长码——但所有码字之间不能”开头一样”,不然解码会歧义。

为什么重要

不理解 Huffman,下面这些事都没法解释:

  • 为什么同一段英文文本压缩后能小一半,但加密后的随机数据几乎压不动
  • 为什么 GZIP 一个 100 字节的小文件反而变大——因为码表本身要存
  • 为什么 Shannon 在 1948 年说的”信息熵下界”不是空话,70 年后还是工业代码的天花板
  • 为什么一个 MIT 学生为了逃期末考试写出来的作业,直接打败了他教授提出的算法

核心要点

Huffman 的过程拆三步:

  1. 频率统计:扫一遍数据,看每个符号出现多少次。类比:先数清楚一篇文章里”的”出现 200 次、“鳙”出现 0 次。

  2. 构建二叉树(最小堆贪心):把所有符号当作”小树”放进堆里。每次取出权重最小的两棵合并,新节点的权重等于两个子节点之和,放回堆。重复到堆里只剩一棵——这就是 Huffman 树。

  3. 左右路径就是编码:从根开始,左走标 0、右走标 1,到叶子的路径就是该符号的二进制码。自动是前缀码——任何码都不是另一个的前缀,所以解码无歧义。

第 2 步的”贪心”听起来不靠谱,但 Huffman 证明了它是最优的(同 shannon-1948 的熵下界相比误差 ≤ 1 bit/symbol)。证明用的是”交换论证”:假设有更优的码,把最小两个频率换到树最深处,能证明换完不会更差——所以 Huffman 已经是最优。

实践案例

案例 1:手算一棵 Huffman 树

5 个字母 A B C D E,频率分别为 0.4 / 0.2 / 0.2 / 0.1 / 0.1。

构造步骤(每次合并两个最小):

步骤操作当前森林
0初始A:0.4, B:0.2, C:0.2, D:0.1, E:0.1
1合并 D+E → 0.2A:0.4, B:0.2, C:0.2, 0.2
2合并 B+C → 0.4A:0.4, 0.2, 0.4
3合并 0.2+0.4 → 0.6A:0.4, 0.6
4合并 0.4+0.6 → 1.0根:1.0

最终码:A=1、B=010、C=011、D=0010、E=0011。注意 A 用 1 位(最常出现)、D/E 用 4 位(最罕见)。

案例 2:“ABBA” 压缩对比

普通 8-bit ASCII:4 个字符 × 8 bit = 32 bit。 用上述 Huffman:A=1、B=010,“ABBA” = 1 + 010 + 010 + 1 = 8 bit

压缩比 75%。当然这是玩具例子——真实场景还要存码表,所以小文件未必划算。

案例 3:5 行 Python 实现

import heapq
from collections import Counter
def huffman(text):
heap = [[w, [s, ""]] for s, w in Counter(text).items()]
heapq.heapify(heap)
while len(heap) > 1:
lo, hi = heapq.heappop(heap), heapq.heappop(heap)
for pair in lo[1:]: pair[1] = '0' + pair[1]
for pair in hi[1:]: pair[1] = '1' + pair[1]
heapq.heappush(heap, [lo[0]+hi[0]] + lo[1:] + hi[1:])
return sorted(heap[0][1:], key=lambda p: (len(p[1]), p))

huffman("ABBA") 立刻看到码表。整个核心思想就是这么轻——这也是 Huffman 能撑 70 年的原因。

踩过的坑

  1. 静态 vs 自适应:教科书讲的是”先扫一遍统计频率再编码”(静态 Huffman),需要两遍扫描。流式数据(聊天记录、日志)做不到——要用 自适应 Huffman(FGK / Vitter),编码端和解码端同步更新树。代价是慢 2-3 倍。

  2. 频率均匀压不动:如果数据已经加密 / 已经压缩过,每个字节的概率几乎相同,熵接近 8 bit/byte,Huffman 给每个字节也接近 8 bit。再压一遍反而变大(因为多了码表开销)。

  3. 小文件不划算:100 字节的输入,码表本身就要 100+ 字节存。GZIP 用了一个 hack——静态 Huffman 模式直接复用 RFC 1951 写死的码表,不传码表。但这个码表是给”英文文本 + 源代码”调优的,对中文/二进制效果不好。

  4. 整数 bit 是天花板:信息熵 H 可以是 0.469 bit/symbol,但 Huffman 必须给每个符号至少 1 bit。当一个符号概率特别高(如 0.9),Huffman 的冗余可达 100%。算术编码(1976)和 ANS(2009)能突破这道墙——后者已经在 Zstd / JPEG XL 里取代 Huffman。

  5. bit endianness 写错全错:自己写解码器最容易踩的坑——DEFLATE 的码字是 LSB-first(最低位先发),写成 MSB-first 整流就是乱码。这种 bug 不会”部分错”——要么全对要么全错。

适用 vs 不适用场景

适用

  • 文本压缩,符号频率分布明显偏斜(自然语言、源代码、日志)
  • 已知概率分布不变的场景(HTTP 头部压缩 / HPACK 用静态 Huffman)
  • 速度敏感场景——表查询每符号约 1ns,比算术编码快 5 倍

不适用

  • 已加密 / 已压缩的数据(熵接近最大,Huffman 帮不上忙)
  • 极低熵分布(如 99% A、1% B)—— 用算术编码或 ANS
  • 流式数据无法预扫描时——需要自适应 Huffman 或 ANS
  • 对压缩率敏感、速度可以让步——ANS 同样快、压缩率更高

历史小故事(可跳过)

  • 1948:Shannon 在 shannon-1948 定义信息熵,证明编码下界是 H(X),但他给出的 Shannon-Fano 编码不是最优
  • 1949:Robert Fano 改进 Shannon 的算法,叫 Shannon-Fano。仍然次优。
  • 1951 秋:MIT 信息论课,Fano 给学生选项:“期末考试 OR 写论文证明 Shannon-Fano 最优(或给出更好的)“。学生 David Huffman 选了写论文——理由是不想考试。
  • 1951 冬:Huffman 卡住几乎放弃,想”自顶向下”的 Shannon-Fano 一直证不出来。在最后一刻他换思路:“自底向上”——每次合并两个最小频率。瞬间解开。
  • 1952 年 9 月:4 页论文发表在 Proc. IRE,10 个公式。
  • 1977:Lempel-Ziv 提出 LZ77(字典匹配),打开”前置压缩”思路。
  • 1991:DEFLATE = LZ77 + Huffman,被 ZIP / GZIP / PNG 吸收,成为事实标准。
  • 2009:Duda 提出 ANS,速度匹敌 Huffman、压缩率匹敌算术编码。
  • 2016:Zstd 用 LZ77 + ANS,逐步蚕食 Huffman 的工业份额——但 HTTP / PNG / ZIP 因为生态锁定还在用 Huffman。

教授提出的算法被自己学生在期末作业里超越——计算机史上没几个这种故事。

学到什么

  1. 简单算法 + 正确证明 = 70 年:Huffman 论文 4 页,C 实现 50 行,但因为有数学最优性证明,70 年没被根本性取代——只是被 ANS 在边缘场景蚕食。
  2. 贪心配最小堆是经典套路:每次取最小两个、合并、放回。这种结构后来在 Dijkstra、Prim、A* 里反复出现。
  3. 整数 bit 是 Huffman 的边界:理论下界 H 可以是分数,但 Huffman 必须给整数 bit——这就是它和算术编码 / ANS 的本质差距。
  4. 生态比算法重要:ANS 技术上更优,但 HTTP / PNG / ZIP 还在用 Huffman——因为换协议需要全行业升级,门槛远高于”算法更好”本身。

延伸阅读

  • 经典教材:Cover & Thomas《Elements of Information Theory》第 5 章——把 Shannon 第一定理 + Huffman 一起讲透
  • 工程视角:Sayood《Introduction to Data Compression》——LZ77 / Huffman / 算术 / ANS 一条线讲完
  • 现代继承者:Duda 2009《Asymmetric Numeral Systems》arXiv:0902.0271——ANS 原论文
  • DEFLATE 标准:RFC 1951——附 Canonical Huffman 编码细节,工程必读
  • 视频:3Blue1Brown 没专门做 Huffman 但《Information Theory Basics》系列把熵讲得很直观

关联

  • shannon-1948 —— Shannon 定义信息熵,给出编码下界 H(X);Huffman 的最优性证明就是逼近这个下界
  • turing-1936 —— 计算理论根基;Huffman 是”什么是最优编码”这个可计算问题的具体解
  • lambda-calculus —— 同样诞生在 1930s-50s 的”用极简结构表达普适问题”思潮中
  • godel-1931 —— 数学不完备性;Huffman 反方向证明”在整数 bit 限制下最优是可达的”

反向链接