📡

哈夫曼编码

左 0 右 1 发"门牌号"——常用字母门牌短,报文又短又不会读串

💡 打个比方

上一课建好了哈夫曼树,这一课给它发门牌号:规定往左走记 0、往右走记 1,从根走到每个字母叶子的路上经过的方向,连起来就是这个字母的编码。就像小区地址:"3 号楼 → 左拐 → 右拐",走到门口刚好是 "3-左-右"。门牌越短的楼,是住得越"热闹"(出现越频繁)的楼。

更妙的是:哈夫曼树的每个字母都在叶子上,所以一个字母的门牌号绝不会是另一个字母门牌号的"开头"——送信的人读 01 串时永远不会读串!这叫前缀码。

哈夫曼编码:在哈夫曼树上,左 = 0、右 = 1,根到叶子的路径就是编码。频率高的字母路径短(码短),且所有码互不为前缀(前缀码),所以 01 串可以唯一地还原成原文。

🖼️ 大图看明白

沿用上一课的树(A=1、B=4、C=6、D=8、E=10)。边上标好左 0 右 1,每个字母的"门牌号"(编码)就出来了。

0 1 0 1 0 1 0 1 根 11 18 5 C D E A B 走法 = 编码:如 A = 左左左 = 000,C = 左右 = 01 每个字母的"门牌号" 字母 频率 编码 码长 A 1 000 3 B 4 001 3 C 6 01 2 D 8 10 2 E 10 11 2 发报文 "C E C": 01 11 01 → 011101(6 个比特) 定长码需 3×3=9 比特;哈夫曼码省了 3 比特 规律:频率越高(E=10)码越短(2 位)

🧩 子任务 1:左 0 右 1,发"门牌号"

🎯 小目标:会沿着树把每个字母的编码读出来

规则一句话:往左走记 0,往右走记 1,从根走到叶子,把路上的记号连起来就是编码。

逐个读(对照大图左边的树):

为什么常用字母码短:建哈夫曼树时,频率大的字母被"留"在浅处(它所在的小树很快就不再参与合并),所以路径短。E(10)只有 2 位,A(1)要 3 位——正是我们想要的效果。
✅ 小检查:如果 D 和 E 的位置互换(D 走右右、E 走右左),编码表变成什么样?WPL 变不变?
看看答案
D = 11、E = 10,其他不变。WPL 不变——每个字母的码长没变(都是 2 位),变的只是 01 串的具体写法。平手时谁左谁右不影响 WPL,只影响编码外观。

🧩 子任务 2:前缀码——为什么不会"读串"?

🎯 小目标:理解"前缀码",说清哈夫曼码为什么能唯一还原

先想一个反面例子。假如给字母发这样的编码:A = 0,B = 01,C = 1。收到 01 时,是"A + …"还是"B"?分不清!因为 A 的码 0 是 B 的码 01 的开头(前缀),读到 0 就不知道该停还是继续。

哈夫曼码没这个问题,因为所有编码都只在叶子上:任何一条"根到叶子"的路,中途不会经过另一个叶子。翻译成编码语言:没有一个编码是另一个编码的前缀——这叫前缀码(也叫前缀-free 码)。

验证上图编码表:000、001、01、10、11 互相看看,谁也不在谁的开头(01 不是 001 的前缀,因为 01 ≠ 00…)。所以读 011101 时:先 01(C)→ 再 11(E)→ 再 01(C),一路无歧义,还原出 "C E C"。

解码方法:不用背"每段几位",只要在树上走——读到 0 往左、读到 1 往右,一踩到叶子就输出这个字母,回到根继续。前缀码保证每次踩叶子都恰好是一个完整字母。
✅ 小检查:编码表 A=0、B=10、C=11 是不是前缀码?收到 0110 会还原成什么?
看看答案
是前缀码(0 不在 10、11 的开头,10、11 互不为前缀)。0110 = 0(A)+ 11(C)+ 0(A)→ "A C A"。

🧩 子任务 3:编解码实战

🎯 小目标:会手工把报文编成 01 串、再把 01 串译回报文,并会算省了多少位

编码(发报):把每个字母查表换成 01 串,首尾相接。例:发 "D E A B" → 10 11 000 001 → 1011000001(10 个比特)。

解码(收报):拿 1011000001 在树上走:1→右、0→左 = 10(D),踩到叶子输出 D 回根;1→右、1→右 = 11(E),输出 E 回根;0→左、0→左、0→左 = 000(A),输出 A 回根;0→左、0→左、1→右 = 001(B),输出 B。得到 "D E A B",一个不错!

算省了多少:定长码给 5 个字母每人 3 位(2²=4 不够、2³=8 才够),发 4 个字母要 12 位;哈夫曼码只要 2+2+3+3 = 10 位,省 2 位。字母表越大、频率越悬殊,省得越多。

✅ 小检查:用上图编码表,把 0100010 译成字母。
看看答案
01(C)→ 000(A)→ 10(D)。译出 "C A D"。读法:从串头开始,沿树走,踩到叶子就切一段。

💻 完整代码

建树(复用上一课)→ DFS 生成每个字母的编码 → 编码报文 → 在树上走一遍解码,全流程一个文件搞定。

// 哈夫曼编码:建树 → 生成编码表 → 编/解码
#include <bits/stdc++.h>
using namespace std;

struct Node {
    int w, ch, l, r;   // 权重/字母编号/左右孩子(0 = 无)
    Node(int weight, int c) : w(weight), ch(c), l(0), r(0) {}
};

int main() {
    int freq[] = {1, 4, 6, 8, 10};   // A B C D E
    int n = 5;
    vector<Node> pool;
    priority_queue<pair<int,int>, vector<pair<int,int>>,
                   greater<pair<int,int>>> pq;
    for (int i = 0; i < n; i++) {
        pool.push_back(Node(freq[i], i));
        pq.push({freq[i], i});
    }
    while (pq.size() > 1) {
        int a = pq.top().second; pq.pop();   // 较轻放左
        int b = pq.top().second; pq.pop();
        pool.push_back(Node(pool[a].w + pool[b].w, -1));
        int id = (int)pool.size() - 1;
        pool[id].l = a;  pool[id].r = b;
        pq.push({pool[id].w, id});
    }
    int root = pq.top().second;

    // DFS 生成编码表:左 0 右 1
    vector<string> code(n);
    function<void(int, string&)> dfs = [&](int id, string &s) {
        if (pool[id].ch >= 0) { code[pool[id].ch] = s; return; }
        s.push_back('0'); dfs(pool[id].l, s); s.pop_back();
        s.push_back('1'); dfs(pool[id].r, s); s.pop_back();
    };
    string path;
    dfs(root, path);
    for (int i = 0; i < n; i++)
        cout << (char)('A'+i) << " = " << code[i] << endl;
    // A = 000 / B = 001 / C = 01 / D = 10 / E = 11

    // 编码报文 "CEC"
    string msg = "CEC", out;
    for (char c : msg) out += code[c - 'A'];
    cout << "encode: " << out << endl;   // 011101

    // 解码:沿树走,踩到叶子输出字母、回根
    string bits = out, plain;
    int cur = root;
    for (char b : bits) {
        cur = (b == '0') ? pool[cur].l : pool[cur].r;
        if (pool[cur].ch >= 0) {
            plain += (char)('A' + pool[cur].ch);
            cur = root;
        }
    }
    cout << "decode: " << plain << endl;  // CEC
    return 0;
}
易错点:① DFS 里进左孩子前 push_back('0')、回来 pop_back(),忘删就把 0 带进右子树了。② 解码时踩到叶子必须立刻回根,否则下一个字母会接着上一段路走。③ 平手谁左谁右影响编码外观、不影响码长和 WPL——题目若指定"轻左重右"要照做,否则答案可能对不上。

✋ 你来试试

第 1 题:用编码表(A=000 B=001 C=01 D=10 E=11)把 "E B D" 编成 01 串。
E=11、B=001、D=10 → 11 001 10 = 11001110(8 个比特)。
第 2 题:把 011001 译回字母。
01(C)→ 10(D)→ 01(C)→ "C D C"。
第 3 题:报文共 20 个字母,其中 15 个是 E、5 个是 A。定长码(3 位/字母)和哈夫曼码各需多少比特?省多少?
定长:20×3 = 60;哈夫曼:15×2 + 5×3 = 30 + 15 = 45;省 15 比特(25%)。E 用得越多,省得越狠。
🎯 一句话记住:左 0 右 1 读门牌,频率高码就短;码都在叶子上 = 前缀码,01 串永远读不串。