【数据结构】哈夫曼树的原理、实现与考研真题解析

news2026/4/6 12:46:25
一、什么是哈夫曼树哈夫曼树Huffman Tree也叫最优二叉树是一种带权路径长度WPL最小的二叉树。节点带有权值比如字符出现的频率树的带权路径长度 WPL 所有叶子节点的权值 × 该节点到根的路径长度 之和哈夫曼树的特点权值越大的节点离根越近只有度为 0叶子和度为 2的节点没有度为 1 的节点n个叶子节点的哈夫曼树总共有2n−1 个节点二、哈夫曼编码与前缀码哈夫曼树最经典的应用就是哈夫曼编码它是一种无损数据压缩算法核心是利用字符出现的频率权值为频率高的字符分配更短的二进制编码频率低的字符分配更长的编码从而大幅减少数据传输的总长度。前缀码的定义前缀码任意一个字符的编码都不是其他字符编码的前缀。前缀码的核心作用是保证解码的唯一性在没有分隔符的二进制串中不会出现歧义能唯一还原出原始字符。哈夫曼编码天然是前缀码因为哈夫曼树中所有叶子节点的编码对应根到叶子的唯一路径不存在一个叶子节点的路径是另一个叶子节点路径的前缀。三、哈夫曼树构造步骤哈夫曼树就是每次选两个最小权值节点合并直到只剩一棵树最终让整棵树的带权路径长度最小。核心思想权值越大的节点离根越近使带权路径长度WPL最小。构造步骤步骤操作1. 初始化将n个带权叶子节点各自作为独立的树森林2. 选最小每次从森林中选取两个根节点权值最小的树3. 合并以这两棵树为左右子树新建根节点权值两者之和4. 重复将新树放回森林重复步骤2-3直到只剩一棵树以 6 个字母的频率表为例完整演示哈夫曼编码的生成过程字母ABCDEF频率2781515305步骤 1构造哈夫曼树按照哈夫曼树的构造规则以频率为权值构造出对应的哈夫曼树并约定左分支记为 0右分支记为 1。步骤 2生成哈夫曼编码从根节点出发沿着到每个叶子节点的路径记录 0/1 序列即为该字母的哈夫曼编码A根→0→1 → 编码01B根→1→0→0→1 → 编码1001C根→1→0→1 → 编码101D根→0→0 → 编码00E根→1→1 → 编码11F根→1→0→0→0 → 编码1000最终编码表如下字母ABCDEF等长二进制编码000001010011100101哈夫曼编码01100110100111000步骤 3编码效果对比以字符串BADCADFEED为例对比两种编码的总长度原等长编码二进制串001 000 011 010 000 011 101 100 100 011总长度为 3×1030 位哈夫曼编码二进制串1001 01 00 101 01 00 1000 11 11 00总长度为 422322422225 位可以看到哈夫曼编码通过为高频字符分配短编码成功压缩了数据长度同时因为是前缀码解码时不会出现歧义。四、哈夫曼树的 C 语言实现结合不同成绩等级占比的场景不及格 5%、及格 15%、中等 40%、良好 30%、优秀 10%用 C 语言实现哈夫曼树的构建与结构打印代码遵循哈夫曼树的构造规则注释清晰且易理解。1. 数据结构设计定义哈夫曼树的节点结构体包含权值、父节点下标、左孩子下标、右孩子下标四个核心字段通过数组存储哈夫曼树的所有节点顺序存储结构便于节点的查找与合并。#include stdio.h #include stdlib.h #define MAX_N 100 // 定义最大节点数满足2n-1的需求 // 哈夫曼树节点结构体 typedef struct { int weight; // 节点权值如成绩等级占比 int parent; // 父节点下标-1表示无父节点 int lchild; // 左孩子下标-1表示无左孩子 int rchild; // 右孩子下标-1表示无右孩子 } HTNode; typedef HTNode HuffmanTree[MAX_N]; // 数组形式存储哈夫曼树2. 核心辅助函数选择两个最小权值节点实现selectTwoMin函数在未被合并的节点父节点为 - 1中选出权值最小的两个节点返回其下标这是哈夫曼树构造的核心步骤直接体现贪心策略。// 在 HT[0...n-1] 中选出两个无父节点的最小权值节点下标存入 s1 和 s2 void selectTwoMin(HuffmanTree HT, int n, int *s1, int *s2) { int i; *s1 *s2 -1; // 初始化下标为-1表示未找到 for (i 0; i n; i) { if (HT[i].parent -1) { // 仅选择未被合并的节点 if (*s1 -1 || HT[i].weight HT[*s1].weight) { *s2 *s1; *s1 i; } else if (*s2 -1 || HT[i].weight HT[*s2].weight) { *s2 i; } } } }3. 哈夫曼树的构建函数实现createHuffmanTree函数遵循构造规则完成哈夫曼树的构建核心步骤为初始化节点和循环合并最小权值节点最终生成总节点数为2n−1的哈夫曼树。// 创建哈夫曼树weights为权值数组n为叶子节点个数 void createHuffmanTree(HuffmanTree HT, int weights[], int n) { int i; int m 2 * n - 1; // 哈夫曼树总节点数2n-1 // 初始化所有节点 for (i 0; i m; i) { HT[i].weight (i n) ? weights[i] : 0; // 前n个为叶子节点赋权值其余初始化为0 HT[i].parent -1; HT[i].lchild -1; HT[i].rchild -1; } // 构造内部节点从n到m-1 for (i n; i m; i) { int s1, s2; selectTwoMin(HT, i, s1, s2); // 找到两个最小权值的未合并节点 // 合并s1和s2创建新节点i HT[i].weight HT[s1].weight HT[s2].weight; HT[i].lchild s1; HT[i].rchild s2; HT[s1].parent i; // 给s1和s2设置父节点 HT[s2].parent i; } }4. 打印哈夫曼树结构实现printHuffmanTree函数打印哈夫曼树所有节点的索引、权值、双亲、左子、右子信息便于验证哈夫曼树的构造结果。// 打印哈夫曼树结构直观查看节点关系 void printHuffmanTree(HuffmanTree HT, int n) { int i; int total 2 * n - 1; printf(索引 权值 双亲 左子 右子\n); for (i 0; i total; i) { printf(%3d %4d %4d %4d %4d\n, i, HT[i].weight, HT[i].parent, HT[i].lchild, HT[i].rchild); } }5. 主函数测试以成绩等级占比为权值调用上述函数完成哈夫曼树的构建与打印验证代码的正确性。int main() { // 权值数组不及格5、及格15、中等40、良好30、优秀10 int weights[] {5, 15, 40, 30, 10}; int n sizeof(weights) / sizeof(weights[0]); // 叶子节点个数n5 HuffmanTree HT; createHuffmanTree(HT, weights, n); // 构建哈夫曼树 printHuffmanTree(HT, n); // 输出节点信息 return 0; }6.完整可运行代码如下#include stdio.h #include stdlib.h #include string.h #define MAXSIZE 100 // 最大节点数 #define MAXCODE 100 // 编码最大长度 // 哈夫曼树节点结构体定义 typedef struct { int weight; // 节点权值 int parent, lchild, rchild; // 双亲、左孩子、右孩子下标 } HTNode, *HuffmanTree; // 哈夫曼编码结构体存储每个叶子节点的编码字符串 typedef char **HuffmanCode; // 核心函数1挑选两个权值最小的节点 // 在HT[0]~HT[n-1]中选parent为-1未合并的两个最小权值节点下标存入s1、s2 void Select(HuffmanTree HT, int n, int *s1, int *s2) { int i, min1, min2; // min1最小权值min2次小权值 min1 min2 9999; // 初始化一个大数 *s1 *s2 -1; // 遍历所有节点找最小的两个 for (i 0; i n; i) { // 只处理没有双亲的节点 if (HT[i].parent -1) { // 当前节点权值比最小还小 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; } } } } // 核心函数2构建哈夫曼树 // HT哈夫曼树数组 // w权值数组 // n叶子节点个数 void CreateHuffmanTree(HuffmanTree *HT, int w[], int n) { int i, s1, s2, m; // 特殊情况只有1个叶子节点 if (n 1) return; // 哈夫曼树总结点数2n-1 m 2 * n - 1; // 分配内存 *HT (HTNode *)malloc((m) * sizeof(HTNode)); // 初始化所有节点 for (i 0; i m; i) { (*HT)[i].weight 0; (*HT)[i].parent -1; // 双亲初始-1 (*HT)[i].lchild -1; // 左孩子初始-1 (*HT)[i].rchild -1; // 右孩子初始-1 } // 给前n个叶子节点赋权值 for (i 0; i n; i) { (*HT)[i].weight w[i]; } // 开始构建哈夫曼树 // 循环创建 n-1 个内部节点从n到m-1 for (i n; i m; i) { // 挑选两个最小权值节点 Select(*HT, i, s1, s2); // 合并两个节点生成新节点i (*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; } } // 核心函数3生成哈夫曼编码前缀码 // HT哈夫曼树 // HC存储哈夫曼编码 // n叶子节点数 void CreateHuffmanCode(HuffmanTree HT, HuffmanCode *HC, int n) { int i, start, c, p; char *cd; // 分配编码存储空间 *HC (HuffmanCode)malloc(n * sizeof(char *)); // 临时存放编码的数组 cd (char *)malloc(n * sizeof(char)); // 编码结束符 cd[n - 1] \0; // 为每个叶子节点求编码 for (i 0; i n; i) { // 编码起始位置从后往前存 start n - 1; // c当前节点 c i; // pc的双亲节点 p HT[c].parent; // 向上遍历到根节点根节点parent-1 while (p ! -1) { // 如果c是左孩子编码为0 if (HT[p].lchild c) cd[--start] 0; // 如果c是右孩子编码为1 else cd[--start] 1; // 继续向上 c p; p HT[c].parent; } // 为第i个编码分配空间 (*HC)[i] (char *)malloc((n - start) * sizeof(char)); // 复制编码 strcpy((*HC)[i], cd[start]); } // 释放临时空间 free(cd); } // 计算带权路径长度WPL int GetWPL(HuffmanTree HT, int n) { int i, wpl 0; int c, p, len; // 遍历每个叶子节点 for (i 0; i n; i) { len 0; c i; p HT[c].parent; // 统计路径长度 while (p ! -1) { len; c p; p HT[c].parent; } // 累加 权值×路径长度 wpl HT[i].weight * len; } return wpl; } // 打印哈夫曼树结构 void PrintHT(HuffmanTree HT, int n) { int i, m 2 * n - 1; printf(\n 哈夫曼树节点信息 \n); printf(下标\t权值\t双亲\t左孩子\t右孩子\n); for (i 0; i m; i) { printf(%d\t%d\t%d\t%d\t%d\n, i, HT[i].weight, HT[i].parent, HT[i].lchild, HT[i].rchild); } } // 主函数测试 int main() { // 叶子节点权值示例不及格5、及格15、中等40、良好30、优秀10 int w[] {5, 15, 40, 30, 10}; // 叶子节点名称对应上面权值 char name[][20] {不及格, 及格, 中等, 良好, 优秀}; int n sizeof(w) / sizeof(w[0]); // 叶子节点数量 n5 HuffmanTree HT; // 哈夫曼树 HuffmanCode HC; // 哈夫曼编码 int i, wpl; // 1. 创建哈夫曼树 CreateHuffmanTree(HT, w, n); // 2. 打印哈夫曼树结构 PrintHT(HT, n); // 3. 生成哈夫曼编码 CreateHuffmanCode(HT, HC, n); // 4. 打印哈夫曼编码前缀码 printf(\n 哈夫曼编码前缀码 \n); for (i 0; i n; i) { printf(%s\t(权值:%d)\t编码%s\n, name[i], w[i], HC[i]); } // 5. 计算并打印WPL wpl GetWPL(HT, n); printf(\n带权路径长度 WPL %d\n, wpl); // 6. 前缀码说明 printf(\n 前缀码特性说明 \n); printf(1. 哈夫曼编码是前缀码任意编码都不是其他编码的前缀\n); printf(2. 解码无歧义可唯一还原原始数据\n); // 释放内存 for (i 0; i n; i) { free(HC[i]); } free(HC); free(HT); return 0; }代码运行结果结果分析总节点数为2×5−19根节点为索引 8权值 100无父节点所有叶子节点0-4无孩子节点内部节点均有左右两个孩子符合哈夫曼树的特性。五、哈夫曼树考研真题解析哈夫曼树是数据结构考研的高频考点题型主要包括哈夫曼树的构造、带权路径长度WPL的计算、哈夫曼编码的生成、前缀码的判断等以下结合考研经典真题讲解解题思路与步骤。真题 12010 年统考真题答案A解析A 错误哈夫曼树是带权路径长度最短的二叉树不是完全二叉树完全二叉树是按层序编号的结构哈夫曼树不满足该特性。B 正确哈夫曼树的构造规则是每次合并两个节点因此不存在度为 1 的节点。C 正确构造哈夫曼树时权值最小的两个节点会被优先合并为兄弟节点。D 正确非叶节点的权值是子节点权值之和因此一定大于等于子节点的权值也一定不小于下一层的任意节点权值。真题 22014 年统考真题答案D解析前缀码的核心要求是任意编码都不是其他编码的前缀。D 选项中110是1100的前缀因此不是前缀编码其余选项均满足前缀码要求。真题 32021 年统考真题答案B解析最小带权路径长度即哈夫曼树的 WPL构造哈夫曼树后计算合并 10 和 12 → 22合并 22 和 16 → 38合并 38 和 21 → 59合并 59 和 30 → 89WPL 16×2 21×2 30×2 10×3 12×3 32 42 60 30 36 200也可通过累加内部节点权值计算22385989200真题 42019 年统考真题答案C解析哈夫曼树的总节点数公式为 m2n−1代入 m1152n−1115→2n116→n58五、哈夫曼树的应用场景哈夫曼树的核心价值是带权路径长度最短基于这一特性在实际开发和算法设计中有着广泛应用数据压缩哈夫曼编码是无损数据压缩的经典算法如 JPEG、ZIP、GZIP 等格式均采用了哈夫曼编码对出现频率高的字符赋予短编码频率低的字符赋予长编码大幅减少数据存储体积。权重决策在任务调度、资源分配等场景中对优先级权值高的任务赋予更短的处理路径提高处理效率。最优判定在判定树设计中如成绩等级判定、商品等级判定哈夫曼树可使平均判定次数最少优化判定逻辑。通信编码在远距离通信中哈夫曼编码可减少数据传输的总长度降低传输成本同时保证解码的唯一性。六、总结哈夫曼树是数据结构中贪心算法的典型应用其核心是通过 “每次合并最小权值节点” 的策略构造出带权路径长度最短的二叉树。本文从基础概念出发结合成绩占比场景实现了哈夫曼树的 C 语言代码详解了哈夫曼编码与前缀码的原理又通过考研真题讲解了构造、WPL 计算、编码生成等高频考点核心要点总结哈夫曼树的结构特性2n−1个总节点无度为 1 的节点是严格二叉树。构造的核心是贪心策略考研中需遵循固定的左右孩子约定。WPL 的简便计算方法为累加所有内部节点的权值可快速解题。哈夫曼编码是前缀编码由根到叶子的左右分支 0/1 序列生成保证解码唯一性。前缀码的判断核心任意编码都不是其他编码的前缀是哈夫曼编码的核心特性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2484379.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…