【良师408】计算机考研408真题解析(2023-04 哈夫曼编码加权平均长度详解)
传播知识,做懂学生的好老师
1.【哔哩哔哩】(良师408)
2.【抖音】(良师408) goodteacher408
3.【小红书】(良师408)
4.【CSDN】(良师408) goodteacher408
5.【微信】(良师408) goodteacher408
特别提醒:【良师408】所收录真题根据考生回忆整理,命题版权归属教育部考试中心所有
摘要:本文以2023年计算机考研408数据结构真题为切入点,深入剖析哈夫曼编码的加权平均长度(WPL)计算方法。通过详细的哈夫曼树构建过程、两种WPL计算方法的推导,并提供完整的C语言实现代码及测试结果,旨在帮助读者透彻理解哈夫曼编码的原理与应用,有效应对相关考点。
在计算机考研408数据结构科目中,哈夫曼编码(Huffman Coding)及其加权平均长度(Weighted Path Length, WPL)的计算是每年必考的高频考点。这不仅考验考生对数据结构基本概念的理解,更侧重于实际操作和计算能力。我们来看一道2023年的典型真题:
【2023-04】 在由 6 个字符组成的字符集 S 中,各字符出现的频次分别为 3,4,5,6,8,10,为 S 构造的哈夫曼编码的加权平均长度为( )。
选项:A. 2.4 B. 2.5 C. 2.67 D. 2.75
本题的关键在于正确构建哈夫曼树,并准确计算其WPL,最终得出加权平均长度。
哈夫曼树,又称最优二叉树,是一种带权路径长度最小的二叉树。它的构建遵循贪心策略:每次从森林中选择权值最小的两棵树合并,生成一棵新树,新树的根节点权值为两棵子树权值之和。这个过程重复进行,直到森林中只剩下一棵树。
**带权路径长度(WPL)**是衡量一棵树优劣的重要指标。它定义为树中所有叶子节点的权值与其到根节点路径长度的乘积之和。路径长度通常指从根节点到该叶子节点所经过的边数。
加权平均长度,在哈夫曼编码中特指平均每个字符的编码位数。其计算公式为:
加权平均长度 = WPL / 总频次
其中,总频次是所有字符权值(频次)的总和。
给定字符频次(权值):[3, 4, 5, 6, 8, 10]。我们将逐步构建哈夫曼树:
初始状态:将所有字符的频次视为独立的树,构成一个森林:{3}, {4}, {5}, {6}, {8}, {10}
。
第一次合并:选择当前森林中权值最小的两个树 {3}
和 {4}
进行合并。新生成的树根节点权值为 3 + 4 = 7
。森林变为:{5}, {6}, {7}, {8}, {10}
。
第二次合并:选择当前森林中权值最小的两个树 {5}
和 {6}
进行合并。新树根节点权值为 5 + 6 = 11
。森林变为:{7}, {8}, {10}, {11}
。
第三次合并:选择当前森林中权值最小的两个树 {7}
和 {8}
进行合并。新树根节点权值为 7 + 8 = 15
。森林变为:{10}, {11}, {15}
。
第四次合并:选择当前森林中权值最小的两个树 {10}
和 {11}
进行合并。新树根节点权值为 10 + 11 = 21
。森林变为:{15}, {21}
。
第五次合并:最后,合并仅剩的两棵树 {15}
和 {21}
。新树根节点权值为 15 + 21 = 36
。至此,哈夫曼树构建完成,根节点权值为36。
最终的哈夫曼树结构示意图如下:
36
/ \
15 21
/ \ / \
7 8 10 11
/ \ / \
3 4 5 6
在哈夫曼树中,从根节点到任意叶子节点的路径长度(边数)即为该叶子节点所代表字符的编码长度。
现在,我们使用两种方法计算加权平均长度:
根据定义,WPL等于所有叶子节点的权值乘以其路径长度之和。总频次为 3+4+5+6+8+10 = 36
。
WPL = (3×3) + (4×3) + (5×3) + (6×3) + (8×2) + (10×2)
WPL = 9 + 12 + 15 + 18 + 16 + 20
WPL = 90
加权平均长度 = WPL / 总频次 = 90 / 36 = 2.5
这是一个重要的性质:一棵哈夫曼树的WPL等于所有非叶子节点(即内部节点)的权值之和。
根据构建过程,内部节点及其权值分别为:7, 11, 15, 21, 36
。
内部节点权值总和 = 7 + 11 + 15 + 21 + 36 = 90
加权平均长度 = 内部节点权值总和 / 总频次 = 90 / 36 = 2.5
两种方法计算结果一致,均为 2.5。因此,本题答案为B。
为了更好地理解哈夫曼树的构建和WPL的计算,下面提供一个C语言实现。该代码包括哈夫曼树的构建、编码长度的计算以及两种WPL计算方法的验证。
#include
#include
#include // For INT_MAX
#define MAX_SIZE 100 // 最大节点数
// 哈夫曼树节点结构
typedef struct {
int weight; // 权值
int parent, lchild, rchild; // 父节点、左子节点、右子节点
} HTNode, *HuffmanTree;
// 在HT[1...end]中选择parent为0且权值最小的两个节点
// 其序号分别存储在s1和s2中
void Select(HuffmanTree HT, int end, int *s1, int *s2) {
int i, min1 = INT_MAX, min2 = INT_MAX;
*s1 = *s2 = 0;
for (i = 1; i <= end; i++) {
if (HT[i].parent == 0) {
if (HT[i].weight < min1) {
min2 = min1;
*s2 = *s1;
min1 = HT[i].weight;
*s1 = i;
} else if (HT[i].weight < min2) {
min2 = HT[i].weight;
*s2 = i;
}
}
}
}
// 构建哈夫曼树
void CreateHuffmanTree(HuffmanTree *HT, int *weights, int n) {
int i, m, s1, s2;
// 哈夫曼树总节点数:n个叶子节点,n-1个内部节点
m = 2 * n - 1;
// 分配内存,0号位置不用,从1开始使用
*HT = (HuffmanTree)malloc((m + 1) * sizeof(HTNode));
if (*HT == NULL) {
fprintf(stderr, "内存分配失败!\n");
exit(EXIT_FAILURE);
}
// 初始化哈夫曼树所有节点
for (i = 1; i <= m; i++) {
(*HT)[i].parent = 0;
(*HT)[i].lchild = 0;
(*HT)[i].rchild = 0;
(*HT)[i].weight = 0; // 内部节点初始权值为0
}
// 设置前n个叶子节点的权值
for (i = 1; i <= n; i++) {
(*HT)[i].weight = weights[i - 1];
}
// 构建哈夫曼树:进行n-1次合并操作
for (i = n + 1; i <= m; i++) {
// 选择权值最小的两个节点进行合并
Select(*HT, i - 1, &s1, &s2);
// 更新父子关系
(*HT)[s1].parent = i;
(*HT)[s2].parent = i;
(*HT)[i].lchild = s1;
(*HT)[i].rchild = s2;
(*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight; // 新节点的权值为子节点权值之和
printf("合并节点: %d + %d = %d\n", (*HT)[s1].weight, (*HT)[s2].weight, (*HT)[i].weight);
}
}
// 计算每个叶子节点的编码长度(即其深度)
void CalculateCodeLengths(HuffmanTree HT, int n, int *lengths) {
int i, p, len;
for (i = 1; i <= n; i++) {
len = 0;
p = i;
// 从叶子节点向上追溯到根节点,每向上走一步,长度加1
while (HT[p].parent != 0) {
len++;
p = HT[p].parent;
}
lengths[i - 1] = len; // 存储对应字符的编码长度
}
}
// 方法一:直接计算加权平均长度
double CalculateWPL(int *weights, int *lengths, int n) {
int i, totalWeight = 0, totalWeightedLength = 0;
for (i = 0; i < n; i++) {
totalWeight += weights[i]; // 累加总频次
totalWeightedLength += weights[i] * lengths[i]; // 累加带权路径长度
}
return (double)totalWeightedLength / totalWeight; // 返回加权平均长度
}
// 方法二:通过内部节点权值和计算加权平均长度
double CalculateWPLFromInternalNodes(HuffmanTree HT, int n) {
int i, totalWeight = 0, internalNodesWeightSum = 0;
// 计算所有叶子节点的总权值
for (i = 1; i <= n; i++) {
totalWeight += HT[i].weight;
}
// 计算所有内部节点的权值之和 (从n+1到2*n-1)
for (i = n + 1; i <= 2 * n - 1; i++) {
internalNodesWeightSum += HT[i].weight;
}
return (double)internalNodesWeightSum / totalWeight; // 返回加权平均长度
}
// 主函数
int main() {
int weights[] = {3, 4, 5, 6, 8, 10}; // 字符频次
int n = sizeof(weights) / sizeof(weights[0]); // 字符数量
int lengths[n]; // 存储每个字符的编码长度
HuffmanTree HT; // 哈夫曼树
printf("=== 哈夫曼编码加权平均长度计算 ===\n\n");
printf("字符频次: ");
for (int i = 0; i < n; i++) {
printf("%d ", weights[i]);
}
printf("\n\n");
// 构建哈夫曼树
printf("【构建哈夫曼树】\n");
CreateHuffmanTree(&HT, weights, n);
printf("\n");
// 计算编码长度
printf("【计算编码长度】\n");
CalculateCodeLengths(HT, n, lengths);
for (int i = 0; i < n; i++) {
printf("字符%d (频次%d): 编码长度 = %d\n", i + 1, weights[i], lengths[i]);
}
printf("\n");
// 方法一:直接计算WPL并求加权平均长度
printf("【方法一:直接计算WPL】\n");
double wpl1 = CalculateWPL(weights, lengths, n);
printf("WPL计算: ");
int sum_wpl_direct = 0;
for (int i = 0; i < n; i++) {
printf("%d×%d", weights[i], lengths[i]);
sum_wpl_direct += weights[i] * lengths[i];
if (i < n - 1) printf(" + ");
}
printf(" = %d\n", sum_wpl_direct);
int totalWeight_direct = 0;
for (int i = 0; i < n; i++) {
totalWeight_direct += weights[i];
}
printf("总频次: %d\n", totalWeight_direct);
printf("加权平均长度: %.2f\n\n", wpl1);
// 方法二:通过内部节点权值和计算加权平均长度
printf("【方法二:内部节点权值和】\n");
double wpl2 = CalculateWPLFromInternalNodes(HT, n);
printf("所有内部节点权值和: ");
int internalSum_method2 = 0;
for (int i = n + 1; i <= 2 * n - 1; i++) {
printf("%d", HT[i].weight);
internalSum_method2 += HT[i].weight;
if (i < 2 * n - 1) printf(" + ");
}
printf(" = %d\n", internalSum_method2);
printf("总频次: %d\n", totalWeight_direct); // 总频次与方法一相同
printf("加权平均长度: %.2f\n", wpl2);
// 释放内存
free(HT);
return 0;
}
=== 哈夫曼编码加权平均长度计算 ===
字符频次: 3 4 5 6 8 10
【构建哈夫曼树】
合并节点: 3 + 4 = 7
合并节点: 5 + 6 = 11
合并节点: 7 + 8 = 15
合并节点: 10 + 11 = 21
合并节点: 15 + 21 = 36
【计算编码长度】
字符1 (频次3): 编码长度 = 3
字符2 (频次4): 编码长度 = 3
字符3 (频次5): 编码长度 = 3
字符4 (频次6): 编码长度 = 3
字符5 (频次8): 编码长度 = 2
字符6 (频次10): 编码长度 = 2
【方法一:直接计算WPL】
WPL计算: 3×3 + 4×3 + 5×3 + 6×3 + 8×2 + 10×2 = 90
总频次: 36
加权平均长度: 2.50
【方法二:内部节点权值和】
所有内部节点权值和: 7 + 11 + 15 + 21 + 36 = 90
总频次: 36
加权平均长度: 2.50
O(logN)
。共进行 N-1
次合并操作,因此构建时间复杂度为 O(N logN)
,其中 N
为字符数量。O(N)
。总共 N
个叶子节点,因此为 O(N^2)
。但如果树是平衡的,则为 O(N logN)
。O(N)
,需要遍历所有叶子节点或内部节点。综合来看,主要的时间开销在哈夫曼树的构建阶段,为 O(N logN)
。
2N-1
个节点,因此空间复杂度为 O(N)
。O(N)
。总空间复杂度为 O(N)
。
哈夫曼编码作为一种高效的无损数据压缩算法,在计算机科学领域有着广泛的应用:
zip
、gzip
等压缩工具中,哈夫曼编码常作为核心或辅助压缩算法,用于对文件内容进行编码,减少存储空间和传输带宽。本文详细解析了2023年计算机考研408真题中关于哈夫曼编码加权平均长度的计算问题。我们不仅回顾了哈夫曼树的构建原理和WPL的定义,还通过两种计算方法和C语言代码实现,验证了最终答案。掌握哈夫曼编码的贪心思想、构建过程以及WPL的计算方法,对于计算机考研和实际工程应用都至关重要。希望本文能帮助读者深入理解这一经典算法,并在未来的学习和工作中灵活运用。