什么是赫夫曼树(哈夫曼树 / Huffman Tree)

1. 核心定义

带权路径长度 WPL 最小的二叉树,称为赫夫曼树

关键概念拆解

  1. 权值 weight:每个叶子节点的数值(比如字符出现频次);
  2. 节点路径长度:从根节点到当前叶子节点经过的边数;
  3. 带权路径长度 WPL = Σ(叶子权值 × 路径长度)
  4. 赫夫曼树特点:所有原始带权节点只能是叶子节点,没有度为 1 的节点

2. 构建赫夫曼树标准算法思路

  1. 把所有带权叶子节点放入最小优先队列(小顶堆)
  2. 循环取出堆中权值最小的两个节点
  3. 创建一个新父节点,权值 = 两个子节点权值之和;
  4. 将新父节点放回优先队列;
  5. 重复 2~4,直到队列中只剩一个节点,该节点就是赫夫曼树根节点。

3. 赫夫曼树核心用途

  1. 数据压缩(赫夫曼编码):高频字符路径短(编码位数少),低频字符路径长,整体压缩文件体积;
  2. 通信传输、文件压缩工具(zip、gzip 底层都用到赫夫曼编码)。

4. 赫夫曼编码规则

  • 向左走记为 0,向右走记为 1
  • 从根走到叶子节点,沿途 0/1 拼接就是该叶子的赫夫曼编码;
  • 性质:任意字符编码不会是另一个字符编码的前缀(前缀编码),解码无歧义。

二、完整 Java 代码实现(赫夫曼树 + 生成赫夫曼编码)

思路总览

  1. 定义赫夫曼树节点:权值、左右孩子、存储对应字符;
  2. 构建小顶堆优先队列,存放所有待处理节点;
  3. 循环合并最小两个节点,生成赫夫曼树;
  4. 递归遍历赫夫曼树,从根到叶子记录路径,生成每个字符的赫夫曼编码;
  5. 测试案例:字符频次 a:5, b:9, c:12, d:13, e:16, f:45

java

运行

复制代码
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;

/**
 * 赫夫曼树节点
 */
class HuffmanNode implements Comparable<HuffmanNode> {
    // 存储字符,只有叶子节点有值
    Character data;
    // 权值(字符出现频次)
    int weight;
    HuffmanNode left;
    HuffmanNode right;

    // 叶子节点构造
    public HuffmanNode(Character data, int weight) {
        this.data = data;
        this.weight = weight;
    }

    // 非叶子节点(合并生成的父节点)构造
    public HuffmanNode(int weight) {
        this.weight = weight;
    }

    // 优先队列按权值升序排序(小顶堆)
    @Override
    public int compareTo(HuffmanNode o) {
        return this.weight - o.weight;
    }
}

/**
 * 赫夫曼树工具类
 */
public class HuffmanTree {
    // 存储每个字符对应的赫夫曼编码
    private static Map<Character, String> codeMap = new HashMap<>();

    /**
     * 1. 构建赫夫曼树
     * @param charWeightMap 字符-权值映射表
     * @return 赫夫曼树根节点
     */
    public static HuffmanNode createHuffmanTree(Map<Character, Integer> charWeightMap) {
        // 步骤1:初始化小顶优先队列
        PriorityQueue<HuffmanNode> minHeap = new PriorityQueue<>();

        // 步骤2:将所有叶子节点入堆
        for (Map.Entry<Character, Integer> entry : charWeightMap.entrySet()) {
            minHeap.add(new HuffmanNode(entry.getKey(), entry.getValue()));
        }

        // 步骤3:循环合并最小两个节点,直到堆只剩一个根节点
        while (minHeap.size() > 1) {
            // 取出权值最小节点1
            HuffmanNode leftNode = minHeap.poll();
            // 取出权值最小节点2
            HuffmanNode rightNode = minHeap.poll();

            // 新建父节点,权值为两个子节点之和
            HuffmanNode parentNode = new HuffmanNode(leftNode.weight + rightNode.weight);
            parentNode.left = leftNode;
            parentNode.right = rightNode;

            // 父节点重新入堆参与下一轮合并
            minHeap.add(parentNode);
        }

        // 堆中最后一个节点就是赫夫曼树根
        return minHeap.poll();
    }

    /**
     * 2. 递归遍历赫夫曼树,生成赫夫曼编码
     * @param root 当前遍历节点
     * @param code 递归累积的0/1编码字符串
     */
    public static void generateHuffmanCode(HuffmanNode root, String code) {
        if (root == null) {
            return;
        }
        // 到达叶子节点,记录当前字符的编码
        if (root.data != null) {
            codeMap.put(root.data, code);
            return;
        }
        // 左子树走0
        generateHuffmanCode(root.left, code + "0");
        // 右子树走1
        generateHuffmanCode(root.right, code + "1");
    }

    public static void main(String[] args) {
        // 测试用:字符及对应出现频次
        Map<Character, Integer> charWeight = new HashMap<>();
        charWeight.put('a', 5);
        charWeight.put('b', 9);
        charWeight.put('c', 12);
        charWeight.put('d', 13);
        charWeight.put('e', 16);
        charWeight.put('f', 45);

        // 1. 创建赫夫曼树
        HuffmanNode root = createHuffmanTree(charWeight);

        // 2. 遍历树生成编码,初始空字符串
        generateHuffmanCode(root, "");

        // 3. 打印每个字符的赫夫曼编码
        System.out.println("====== 赫夫曼编码结果 ======");
        for (Map.Entry<Character, String> entry : codeMap.entrySet()) {
            System.out.println("字符:" + entry.getKey() + "  编码:" + entry.getValue());
        }
    }
}

三、代码分步思路详解

1. HuffmanNode 节点类

  • 叶子节点:存data字符 + weight频次;
  • 合并后的中间父节点:无data,仅存储左右孩子与总权值;
  • 实现Comparable接口,让优先队列按权值从小到大排序(小顶堆)。

2. createHuffmanTree 构建树核心逻辑

  1. 初始化PriorityQueue小顶堆,所有原始字符封装成叶子节点入堆;
  2. 循环取出堆里权值最小两个节点,创建新父节点,权值为两节点之和;
  3. 父节点放回堆,持续合并,直到堆只剩 1 个节点,就是赫夫曼树根;
  4. 关键点:赫夫曼树不存在度为 1 的节点,每次合并一定消耗 2 个节点,新增 1 个节点。

3. generateHuffmanCode 递归生成编码

  1. 递归终止条件:当前节点是叶子节点(data != null),将累积的 0/1 字符串存入编码 map;
  2. 向左递归,编码末尾拼接0;向右递归,编码末尾拼接1
  3. 根节点初始传入空字符串,逐层向下拼接路径。

4. main 测试流程

  1. 构造测试字符 - 频次映射表;
  2. 调用方法生成完整赫夫曼树;
  3. 递归遍历树生成编码;
  4. 循环打印每个字符对应的二进制赫夫曼编码。

四、运行结果说明

输出示例(权重越大的字符编码越短,f权重 45 最大,编码最短):

plaintext

复制代码
====== 赫夫曼编码结果 ======
字符:f  编码:0
字符:c  编码:100
字符:d  编码:101
字符:a  编码:1100
字符:b  编码:1101
字符:e  编码:111

五、面试高频问答

1. 为什么赫夫曼树 WPL 最小?

每次合并当前权值最小的两个节点,让低频节点(权值小)放在更深层,高频节点(权值大)放在浅层,加权路径总和最小。

2. 赫夫曼树为什么没有度为 1 的节点?

构建规则每次合并2 个节点生成 1 个父节点,节点增减平衡,不可能出现只有单个子节点的节点。

3. 赫夫曼编码为什么是前缀编码?

任意字符编码路径终止于叶子节点,不存在一个叶子节点是另一个叶子节点路径中间点,因此没有编码互为前缀,解码时不会歧义。

4. 赫夫曼树是唯一的吗?

不唯一。若存在多个权值相同节点,左右子树交换后会生成不同形态的赫夫曼树,但所有合法赫夫曼树 WPL 一定相等

相关推荐
魔镜er1 小时前
03-张量
人工智能·pytorch·python
37.2℃9952 小时前
Claude Design哪个公司技术好
python·设计模式
Warren2Lynch2 小时前
掌握 UML 构造型、标记定义与标记值:面向领域特定建模的 UML 扩展全面指南
大数据·算法·uml
157092511342 小时前
【无标题】
开发语言·python·算法
稚南城才子,乌衣巷风流2 小时前
换根法(Rerooting)算法详解
算法
晓子文集2 小时前
Tushare接口文档:期货交易日历(fut_trade_cal)
大数据·算法
AI工具人PM产品经理3 小时前
CSDN 自动发布全流程实战:从 CKEditor HTML 注入到创作页就地发布
python
_Jimmy_3 小时前
Tool Calling 与 Function Calling 区别
人工智能·python·langchain
逆风飞翔的小叔3 小时前
【Python基础】Python 流程控制语句使用详解
python·python 流程控制语句·python 流程控制·python 流程控制语句使用·python 流程控制语句详解