Skip to content

哈夫曼树

为什么需要哈夫曼树

在数据通信和文件压缩中,不同字符出现的频率往往差别很大。如果对所有字符都采用等长编码,会造成大量空间浪费。哈夫曼树(最优二叉树)的思想是:让出现频率高的字符使用短编码,频率低的字符使用长编码,从而使总编码长度最短。

408 考研中,哈夫曼树是树与二叉树章节的重点,涉及 WPL 计算、构建算法和哈夫曼编码,选择题和应用题均高频出现。

核心思想

带权路径长度(WPL, Weighted Path Length):树中所有叶结点的权值与其到根结点路径长度的乘积之和。

$$WPL = \sum_{i=1}^{n} w_i \times l_i$$

其中 $w_i$ 是第 i 个叶结点的权值,$l_i$ 是该叶结点到根的路径长度(经过的边数)。

哈夫曼树的核心特点:

  • WPL 最小:在所有含 n 个叶结点、权值相同的二叉树中,哈夫曼树的 WPL 最小
  • 贪心策略:每次选取权值最小的两棵树合并,保证权值大的叶结点离根更近
  • 没有度为 1 的结点:哈夫曼树中只存在度为 0(叶结点)和度为 2 的结点
  • 结点总数:n 个叶结点的哈夫曼树共有 2n - 1 个结点(n - 1 个内部结点)

交互可视化

通过下方的交互动画,你可以逐步观察哈夫曼树的执行过程:

加载可视化中...

操作详解

构建过程

哈夫曼树的构建采用贪心算法,步骤如下:

  1. 将 n 个权值分别作为 n 棵只有根结点的二叉树,构成森林 F
  2. 从 F 中选取权值最小的两棵树作为左右子树,合并为一棵新树,新树根的权值 = 左右子树权值之和
  3. 从 F 中删除被选中的两棵树,将新树加入 F
  4. 重复步骤 2-3,直到 F 中只剩一棵树,即为哈夫曼树

示例:给定权值集合 {2, 3, 5, 7},构建过程如下:

第1步: 选最小的 2 和 3,合并为 5
       5
      / \
     2   3

第2步: 选最小的 5(新) 和 5(原),合并为 10
        10
       /  \
      5    5
     / \
    2   3

第3步: 选 10 和 7,合并为 17
          17
         /  \
        10   7
       /  \
      5    5
     / \
    2   3

构建算法的 C 语言实现

c
typedef struct {
    int weight;       // 权值
    int parent;       // 父结点下标
    int lchild;       // 左孩子下标
    int rchild;       // 右孩子下标
} HTNode;

// 构建哈夫曼树,w[] 为权值数组,n 为叶结点数
void CreateHuffmanTree(HTNode ht[], int w[], int n) {
    int total = 2 * n - 1;  // 总结点数
    // 初始化所有结点
    for (int i = 0; i < total; i++) {
        ht[i].parent = ht[i].lchild = ht[i].rchild = -1;
        ht[i].weight = (i < n) ? w[i] : 0;
    }
    // 进行 n-1 次合并
    for (int i = n; i < total; i++) {
        int min1 = -1, min2 = -1;
        // 在没有父结点的结点中,选权值最小的两个
        for (int j = 0; j < i; j++) {
            if (ht[j].parent != -1) continue;
            if (min1 == -1 || ht[j].weight < ht[min1].weight) {
                min2 = min1;
                min1 = j;
            } else if (min2 == -1 || ht[j].weight < ht[min2].weight) {
                min2 = j;
            }
        }
        // 合并 min1 和 min2
        ht[i].weight = ht[min1].weight + ht[min2].weight;
        ht[i].lchild = min1;
        ht[i].rchild = min2;
        ht[min1].parent = ht[min2].parent = i;
    }
}

哈夫曼编码

哈夫曼编码是一种前缀编码——任何一个字符的编码都不是另一个字符编码的前缀,因此解码时不会产生歧义。

编码规则:从根到叶结点的路径上,走左分支记 0,走右分支记 1(也可反过来,但需保持一致),路径上的 0/1 序列即为该叶结点对应字符的编码。

示例:权值 {2, 3, 5, 7} 对应字符 {D, C, B, A},由上面的哈夫曼树得到编码:

字符权值编码编码长度
A711
B5012
C30013
D20003

前缀编码的性质:哈夫曼编码中,每个编码对应哈夫曼树的一个叶结点。由于叶结点不可能是其他叶结点到根路径上的中间结点,所以天然满足前缀编码的要求。

WPL 计算

WPL 等于所有叶结点的 "权值 × 路径长度" 之和,也等于所有非叶结点权值之和(这是一个常用的等价计算技巧)。

以上面的例子为例:

WPL = 7×1 + 5×2 + 3×3 + 2×3
    = 7 + 10 + 9 + 6
    = 32

等价计算: 非叶结点权值之和 = 17 + 10 + 5 = 32 ✓

WPL 的实际含义:若将权值视为字符出现次数,WPL 就是用该编码方案编码所有字符所需的总比特数。哈夫曼树保证这个总数最小。

复杂度分析

操作时间复杂度说明
构建哈夫曼树O(n²)n-1 次合并,每次线性查找最小值
构建哈夫曼树(优化)O(n log n)使用最小堆(优先队列)选最小值
生成哈夫曼编码O(n log n)每个叶结点回溯到根,平均路径长度 O(log n)

空间复杂度:O(n),需要 2n - 1 个结点的存储空间。

考研高频考点

  • ⭐ 给定权值,画出哈夫曼树并求 WPL(应用题必考)
  • ⭐ 哈夫曼编码的编码/解码过程(选择题、应用题高频)
  • ⭐ 哈夫曼树的性质:无度为 1 的结点,n 个叶结点共 2n-1 个结点(选择题高频)
  • ⭐ 前缀编码的概念及判定方法(选择题)
  • WPL 的定义与计算(填空题、选择题)
  • 哈夫曼树不唯一,但 WPL 唯一(易错点)
  • 哈夫曼编码是最优前缀编码,但不是唯一的最优编码(概念辨析)