3. 香农编码:从第一定理到手算趣事

香农编码,说白了就是信息论里最早的那套编码方法。它不像哈夫曼编码那么流行,但它的思想——用更少的比特表示更常见的东西——是所有压缩技术的根基。我个人觉得,搞懂香农编码,才算真正摸到了压缩的门槛。

3.1 香农第一定理:无失真编码的极限

香农第一定理,也叫无失真信源编码定理。它告诉我们一件事:任何无损压缩,平均码长都不可能小于信源的熵。

嗯,这里要注意。这个定理不是告诉你具体怎么编,而是给你画了一条红线。你编得再好,也跨不过去。

核心公式:

  • 信源熵:H(S) = -Σ p(i) · log₂ p(i)
  • 平均码长:L̄ = Σ p(i) · l(i)
  • 极限条件:L̄ ≥ H(S)

我在项目中遇到过不少刚入行的同学,上来就问「这个压缩率能到多少?」。其实你先算算信源的熵,心里就有底了。如果熵是 4.5 bit,那平均码长至少 4.5 bit,低于这个数就是骗人的。

3.2 香农编码算法:一步一步来

香农编码的算法其实很直白。你想想看,它就是把概率大的符号分到短的码字,概率小的分到长的码字。具体怎么做?

  1. 排序:把所有符号按概率从大到小排好。
  2. 算累积概率:从第一个符号开始,累加前面的概率。
  3. 定码长:每个符号的码长 l(i) = ⌈-log₂ p(i)⌉。
  4. 取二进制小数:把累积概率转成二进制,取前 l(i) 位。

举个例子,假设有 4 个符号:

符号 概率 累积概率 码长 二进制小数 码字
A 0.5 0.0 1 0.0 0
B 0.25 0.5 2 0.10 10
C 0.125 0.75 3 0.110 110
D 0.125 0.875 3 0.111 111

你看,概率大的 A 只用了 1 位,概率小的 C 和 D 用了 3 位。这就是香农编码的直觉。

小技巧: 手算二进制小数时,反复乘 2 取整就行。比如 0.75 × 2 = 1.5 → 取 1,剩下 0.5;0.5 × 2 = 1.0 → 取 1,剩下 0。所以 0.75 的二进制就是 0.11。

3.3 编码效率计算

编码效率 η 的定义很简单:

η = H(S) / L̄ × 100%

拿上面的例子算一下:

  • 熵 H = -0.5·log₂0.5 - 0.25·log₂0.25 - 0.125·log₂0.125 - 0.125·log₂0.125 = 0.5 + 0.5 + 0.375 + 0.375 = 1.75 bit
  • 平均码长 L̄ = 0.5×1 + 0.25×2 + 0.125×3 + 0.125×3 = 0.5 + 0.5 + 0.375 + 0.375 = 1.75 bit
  • 效率 η = 1.75 / 1.75 × 100% = 100%

这个例子刚好达到了极限。但现实中很少这么完美。香农编码的效率通常比哈夫曼编码低一点,因为它的码长是向上取整的,会浪费一些比特。

注意: 香农编码不一定是唯一可译的。如果累积概率的二进制小数截断后出现前缀冲突,需要手动调整。我在实际项目中就踩过这个坑——解码时发现两个符号的码字一样,结果数据全乱了。

3.4 我当年手算香农编码的趣事

说到手算,我想起大学时的一件糗事。

那时候刚学香农编码,老师布置了一道题:给 8 个符号,概率分别是 0.4、0.2、0.1、0.1、0.05、0.05、0.05、0.05。要求手算码字并验证唯一可译性。

我算到第三个符号就卡住了。累积概率 0.6 的二进制是 0.100110011...,无限循环。码长是 3 位,取前三位是 100。但下一个符号的累积概率 0.7,二进制是 0.101100110...,取前三位也是 100。两个符号码字冲突了!

我当时急得满头汗,翻书查资料,最后发现香农编码的原始论文里提到:如果冲突,就把后一个符号的码长加 1 位。于是我把第二个符号的码长从 3 改成 4,码字变成 1011,这才解了围。

那次之后我养成了一个习惯:手算完一定要检查前缀条件。后来做嵌入式系统时,有个同事用香农编码压缩传感器数据,没检查前缀,结果解压出来的数据全是乱码。我帮他排查了一下午,最后发现是码字冲突。嗯,从那以后我再也不敢跳过这步了。

3.5 香农编码的完整流程

下面这张图总结了香农编码的完整流程,从输入符号概率到输出码字:

香农编码完整流程图 输入符号及概率 按概率降序排列 计算累积概率和码长 累积概率转二进制取前l(i)位 检查前缀唯一性 输出码字 注意点 • 码长 l(i)=⌈-log₂p(i)⌉ • 二进制小数反复乘2取整 • 冲突时码长+1位 • 最终检查前缀条件

3.6 香农编码 vs 其他编码

香农编码虽然理论意义重大,但实际应用中用得不多。我整理了一个对比表:

特性 香农编码 哈夫曼编码 算术编码
编码方式 基于累积概率 基于二叉树 基于区间划分
码长 整数,向上取整 整数,最优 分数,接近熵
效率 中等 高 极高
实现复杂度 低 中 高
前缀条件 需手动检查 自动满足 不适用

你看,香农编码的优势在于实现简单,适合教学和理解。但真要上生产环境,我建议用哈夫曼或算术编码。不过话说回来,没有香农编码打底,你很难理解后面那些更复杂的算法。

我的建议: 学香农编码时,一定要手算至少 3 个例子。别偷懒用程序算。手算能让你真正理解「累积概率」「二进制小数」「前缀冲突」这些概念。我当年手算了一整晚,虽然累,但之后看任何编码算法都觉得通透。