1. 核心定义
带权路径长度 WPL 最小的二叉树,称为赫夫曼树。
关键概念拆解
- 权值 weight:每个叶子节点的数值(比如字符出现频次);
- 节点路径长度:从根节点到当前叶子节点经过的边数;
- 带权路径长度 WPL =
Σ(叶子权值 × 路径长度); - 赫夫曼树特点:所有原始带权节点只能是叶子节点,没有度为 1 的节点。
2. 构建赫夫曼树标准算法思路
- 把所有带权叶子节点放入最小优先队列(小顶堆);
- 循环取出堆中权值最小的两个节点;
- 创建一个新父节点,权值 = 两个子节点权值之和;
- 将新父节点放回优先队列;
- 重复 2~4,直到队列中只剩一个节点,该节点就是赫夫曼树根节点。
3. 赫夫曼树核心用途
- 数据压缩(赫夫曼编码):高频字符路径短(编码位数少),低频字符路径长,整体压缩文件体积;
- 通信传输、文件压缩工具(zip、gzip 底层都用到赫夫曼编码)。
4. 赫夫曼编码规则
- 向左走记为
0,向右走记为1; - 从根走到叶子节点,沿途 0/1 拼接就是该叶子的赫夫曼编码;
- 性质:任意字符编码不会是另一个字符编码的前缀(前缀编码),解码无歧义。
二、完整 Java 代码实现(赫夫曼树 + 生成赫夫曼编码)
思路总览
- 定义赫夫曼树节点:权值、左右孩子、存储对应字符;
- 构建小顶堆优先队列,存放所有待处理节点;
- 循环合并最小两个节点,生成赫夫曼树;
- 递归遍历赫夫曼树,从根到叶子记录路径,生成每个字符的赫夫曼编码;
- 测试案例:字符频次
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 构建树核心逻辑
- 初始化
PriorityQueue小顶堆,所有原始字符封装成叶子节点入堆; - 循环取出堆里权值最小两个节点,创建新父节点,权值为两节点之和;
- 父节点放回堆,持续合并,直到堆只剩 1 个节点,就是赫夫曼树根;
- 关键点:赫夫曼树不存在度为 1 的节点,每次合并一定消耗 2 个节点,新增 1 个节点。
3. generateHuffmanCode 递归生成编码
- 递归终止条件:当前节点是叶子节点(
data != null),将累积的 0/1 字符串存入编码 map; - 向左递归,编码末尾拼接
0;向右递归,编码末尾拼接1; - 根节点初始传入空字符串,逐层向下拼接路径。
4. main 测试流程
- 构造测试字符 - 频次映射表;
- 调用方法生成完整赫夫曼树;
- 递归遍历树生成编码;
- 循环打印每个字符对应的二进制赫夫曼编码。
四、运行结果说明
输出示例(权重越大的字符编码越短,f权重 45 最大,编码最短):
plaintext
====== 赫夫曼编码结果 ======
字符:f 编码:0
字符:c 编码:100
字符:d 编码:101
字符:a 编码:1100
字符:b 编码:1101
字符:e 编码:111
五、面试高频问答
1. 为什么赫夫曼树 WPL 最小?
每次合并当前权值最小的两个节点,让低频节点(权值小)放在更深层,高频节点(权值大)放在浅层,加权路径总和最小。
2. 赫夫曼树为什么没有度为 1 的节点?
构建规则每次合并2 个节点生成 1 个父节点,节点增减平衡,不可能出现只有单个子节点的节点。
3. 赫夫曼编码为什么是前缀编码?
任意字符编码路径终止于叶子节点,不存在一个叶子节点是另一个叶子节点路径中间点,因此没有编码互为前缀,解码时不会歧义。
4. 赫夫曼树是唯一的吗?
不唯一。若存在多个权值相同节点,左右子树交换后会生成不同形态的赫夫曼树,但所有合法赫夫曼树 WPL 一定相等。