哈夫曼树与哈夫曼编码的系统性解析,涵盖了数据结构定义、构建过程(createHTree 函数)、编码原理以及实际应用场景。以下是对此内容的进一步整理与补充说明:
1. 代码结构与功能解析
(1)数据结构定义
#define MAXLEAFNUM 50
- 定义最大叶子节点数为 50,用于限制输入字符数量。
struct node {
char ch; // 叶子节点对应的字符(仅叶子节点有效)
float weight; // 权值(频率或概率)
int parent; // 父节点索引,0 表示无父节点
int lchild; // 左孩子索引,0 表示无左孩子
int rchild; // 右孩子索引,0 表示无右孩子
} HuffmanTree[2 * MAXLEAFNUM - 1]; // 哈夫曼树存储数组,共 2n-1 个节点
typedef struct {
char code[MAXLEAFNUM]; // 存储每个字符的哈夫曼编码字符串
int start; // 编码起始位置(从后往前填,start 指向第一位)
} HuffmanCode[MAXLEAFNUM];
- 使用
HuffmanCode数组保存每个叶子节点的最终编码。
(2)createHTree 函数:构建哈夫曼树
步骤详解:
- 初始化叶子节点:
- 将给定的
n个字符及其权值填入前n个节点。 - 设置
parent = 0,表示初始状态均无父节点。
- 将给定的
- 初始化非叶子节点空间:
- 从第
n+1到2n-1的节点预留作合并用,初始权值设为 0,parent=0。
- 从第
- 循环构造内部节点(共 n-1 次):
- 在所有
parent == 0的节点中找出两个权值最小的节点s1和s2。 - 创建新节点(下标为
i),其:weight = s1.weight + s2.weightlchild = s1,rchild = s2- 并将
s1.parent = i,s2.parent = i
- 这样逐步向上构建,形成一棵带权路径长度最短的二叉树(即哈夫曼树)。
- 在所有
关键点:每次选择两个最小权值节点进行合并 —— 典型贪心策略。
2. 哈夫曼编码的概念
| 编码方式 | 特点 |
|---|---|
| 等长编码 | 每个字符使用相同位数编码(如 ASCII 用 8 位),实现简单但不压缩 |
| 哈夫曼编码 | 变长前缀编码,高频字符短码,低频字符长码,整体平均码长最小 |
编码规则:
- 从根到叶子的路径上,左分支记为
0,右分支记为1(或反之)。 - 所有编码均为“前缀码”:任一编码都不是其他编码的前缀 → 解码唯一。
生成编码方法:
void createHCode(HuffmanTree, H, n) {
for (int i = 1; i <= n; i++) {
int start = MAXLEAFNUM - 1;
int c = i, p = H[i].parent;
while (p != 0) {
if (H[p].lchild == c)
code[--start] = '0';
else
code[--start] = '1';
c = p;
p = H[p].parent;
}
HCode[i].start = start;
strcpy(HCode[i].code, &code[start]);
}
}
3. 应用场景
- ✅ 文件压缩:ZIP、GZIP、PNG 中的 DEFLATE 算法结合 LZ77 与哈夫曼编码。
- ✅ 图像压缩:JPEG 对量化后的 DCT 系数进行哈夫曼编码。
- ✅ 通信传输:在带宽受限环境下减少数据量。
- ✅ 无损压缩核心组件:保证信息完整的同时提升存储效率。
优势总结
- 最优前缀编码,使加权路径长度 WPL 最小。
- 实现简单,适合静态统计模型。
- 支持完全还原原始数据(无损)。
在构建哈夫曼树的过程中,每次需要从当前所有无父节点(即尚未被合并)的节点中选出两个权值最小的节点 s1 和 s2。这是哈夫曼算法的核心操作之一。
一、基础实现方法(适用于小规模数据)
使用简单的遍历查找法,在 C 语言中可如下实现:
void selectMin(struct node H[], int n, int *s1, int *s2) {
float min1 = FLT_MAX, min2 = FLT_MAX;
*s1 = *s2 = 0;
for (int i = 1; i <= n; i++) {
// 跳过已有父节点的节点
if (H[i].parent != 0) continue;
if (H[i].weight < min1) {
min2 = min1;
*s2 = *s1;
min1 = H[i].weight;
*s1 = i;
} else if (H[i].weight < min2) {
min2 = H[i].weight;
*s2 = i;
}
}
}
说明:
FLT_MAX来自<float.h>,表示浮点最大值。- 遍历一次即可找出两个最小值及其下标。
- 时间复杂度为 O(n),适合叶子数较少的情况(如 n ≤ 50)。
二、调用示例(在 createHTree 中)
for (int i = n + 1; i <= 2 * n - 1; i++) {
int s1 = 0, s2 = 0;
selectMin(H, i - 1, &s1, &s2); // 在前 i-1 个节点中找最小两个
// 设置新节点
H[i].lchild = s1;
H[i].rchild = s2;
H[i].weight = H[s1].weight + H[s2].weight;
H[s1].parent = i;
H[s2].parent = i;
}
三、优化方法
| 方法 | 描述 | 适用场景 |
|---|---|---|
| 优先队列(最小堆) | 使用最小堆维护所有可用节点,每次取 top 两次,合并后插入新节点 | 大规模数据或动态构造 |
| 排序 + 双指针归并 | 初始按权值排序,之后用两个队列分别保存原始节点和生成的内部节点,利用“单调性”在线性时间内选择最小值 | 高效静态哈夫曼编码 |
| 数组扫描优化 | 添加标记避免重复查找,或缓存候选列表 | 小规模但频繁调用 |
✅ 推荐优化方案:双队列法(高效且常用)
思路:
- Q1:存放初始叶子节点,按权值升序排列。
- Q2:存放合并产生的内部节点(按生成顺序,权值自然递增)。
- 每次从 Q1 和 Q2 的队首选两个最小的节点进行合并。
优点:总时间复杂度降至 O(n log n) → 实际运行接近 O(n)
四、注意事项
- 下标从 1 开始:便于父子关系计算(通常 H[0] 不使用)。
- 权值相等时任意选:不影响最终 WPL,但可能影响编码形式。
- 保证 s1 ≠ s2:虽然权值相同也可能来自不同节点,逻辑上应允许同权不同节点。


1021

被折叠的 条评论
为什么被折叠?



