数据结构05:树与二叉树[C++][哈夫曼树HuffmanTree]

news2024/12/26 4:50:22

图源:文心一言

考研笔记整理6k+字,小白友好、代码可跑,请小伙伴放心食用~~🥝🥝

第1版:查资料、画导图、画配图~🧩🧩

参考用书:王道考研《2024年 数据结构考研复习指导》

参考用书配套视频:7.3_1 二叉排序树_哔哩哔哩_bilibili

特别感谢: Chat GPT老师、文心一言老师~


📇目录

📇目录

🦮思维导图 

🧵基本概念

⏲️哈夫曼树简介

🌰构造举栗

⌨️代码实现

🧵分段代码

 🔯P0:调用库文件

 🔯P1:定义结点与指针

 🔯P2:用于优先队列中的比较函数

 🔯P3:构造哈夫曼树

 🔯P4:打印哈夫曼树编码

 🔯P5:计算哈夫曼树的权值路径长度(WPL)

 🔯P6:main函数

🧵完整代码

 🔯P0:完整代码

 🔯P1:执行结果

🔚结语


🦮思维导图 

备注:

  • 思维导图为整理王道教材第5章 查找的所有内容;
  • 本篇仅涉及到哈夫曼树HuffmanTree的代码;
  • 本章节往期博文,涉及到树与二叉树的内容如下~
    • 🌸[树:双亲、孩子、兄弟表示法][二叉树:先序、中序、后序遍历]
    • 🌸数据结构05:树与二叉树[C++][线索二叉树:先序、中序、后序]

🧵基本概念

⏲️哈夫曼树简介

哈夫曼树的起源:

哈夫曼树是由一位美国数学家David A. Huffman在1952年发明的,它的设计灵感来源于信息的编码与传输。在计算机科学中,我们经常需要将字符或数据编码为二进制形式以便传输和存储。哈夫曼树就是一种通过将出现频率高的字符赋予较短编码,从而实现高效编码的数据结构。

哈夫曼树的用途:

首先,哈夫曼树在数据压缩领域中被广泛应用。在我们的日常生活中,常常会遇到需要传输或存储大量数据的情况,比如发送电子邮件、观看在线视频等。而传输或存储数据需要消耗带宽或存储空间,因此我们希望尽可能减少数据的体积。

哈夫曼树通过根据字符的出现频率构建一种最优的编码方式,使得频率高的字符使用较短的二进制编码,而频率低的字符使用较长的二进制编码。这样一来,我们可以在不损失数据的情况下,显著减小数据的体积,从而实现高效的数据压缩,我们可以在网络传输中更高效地传输数据,提升通信的质量和速度。

🌰构造举栗

哈夫曼树是带权路径最小路径长度的二叉树。

带权路径的公式为 = 求和(结点的权值 x 路径长度)

例如以下三棵树,结点均为“数据a(权值7)、数据b(权值5)、数据c(权值2)、数据d(权值4)”构成:

其构造方式不同,树的权值计算也有差异~ 

  • 树a的WPL:(7+5+2+4)x2=36
  • 树b的WPL:2x1+4x2+(7+5)x3=46
  • 树c的WPL:7x1+5x2+(2+4)x3=35

这要怎么理解这棵树的含义呢?

例如在《天才枪手》中,我们需要利用时差蹲在厕所向队友传递一串选择题答案,这个答案包含“7个a、5个b、2个c、4个d”;并且约定短信内容一定要保密,防止被他人一眼偷窥到短信内容就是答案,我们约定“a、b、c、d”这4个选择由“0”和“1”这两个数字编码加密组成。

对应哈夫曼树,其左子树为0,右子树为1,那么:

  • 树a的编码:a(00)、b(01)、c(10)、d(11),根据权值公式计算的结果,我们传递选择题答案需要在厕所偷偷摁36个数字;
  • 树b的编码:a(010)、b(011)、c(1)、d(00),根据权值公式计算的结果,我们传递选择题答案需要在厕所偷偷摁46个数字;
  • 树c的编码:a(0)、b(10)、c(110)、d(111),根据权值公式计算的结果,我们传递选择题答案需要在厕所偷偷摁35个数字;

这就体现出选择树c编码的好处了~如果不幸选择树b编码的话,会把答案出现频率最高a、b的答案排最长的编码010、011,真的要摁到手麻才能出门~

那如何构成树c呢?

如上图,文字阐述具体步骤如下:

  • 首先,我们需要统计字符的出现频率。这可以通过扫描待编码的数据来实现,统计每个字符出现的次数。
  • 接下来,我们将统计得到的每个字符及其对应的频率作为叶节点,构建一个优先队列
  • 然后,我们从优先队列中选取频率最小的两个节点,创建一个新的节点作为它们的父节点,并将父节点插入到优先队列中。重复上述步骤,直到优先队列中只剩下一个节点,这个节点就是哈夫曼树的根节点。
  • 最后,我们可以通过遍历哈夫曼树,为每个字符生成对应的编码。从根节点开始,左子树路径表示编码位"0",右子树路径表示编码位"1",直到达到叶节点。通过遍历路径,我们可以为每个字符生成唯一的哈夫曼编码

下面我们以图中的小树为例,简单列出哈夫曼树的构造代码{这次的代码是GPT老师生成的}~


图源:文心一言

⌨️代码实现

🧵分段代码

 🔯P0:调用库文件

  • 输入输出流文件iostream{本代码用于输入与输出};
  • 动态数组的向量文件vector{本代码用于比较队列中结点的大小};
  • 队列函数文件queue{本代码用于创建哈夫曼树}~
#include <iostream>
#include <queue>
#include <vector>

 🔯P1:定义结点与指针

struct HuffmanNode {
    char data;       //定义字符
    int frequency;   //定义频率
    HuffmanNode *left, *right;  //定义左指针、右指针

    HuffmanNode(char data, int frequency) { //初始化
        this->data = data;
        this->frequency = frequency;
        left = right = nullptr;
    }
};

 🔯P2:用于优先队列中的比较函数

创建结点的步骤在调整<优先队列>时重复出现,因此使用函数封装~

思路:比较指针指向的两个函数,权值高的结点优先度降低。

struct Compare {
    bool operator()(HuffmanNode* a, HuffmanNode* b) {   //接受两个HuffmanNode对象的指针作为参数,即HuffmanNode* a和HuffmanNode* b
        return a->frequency > b->frequency;              //判断a的频率是否大于b的频率,频率高的结点优先度更低
    }
};

 🔯P3:构造哈夫曼树

传入main函数中的数据动态数组data和频度动态数组frequency~

本步骤的构建过程在博文上面已有图文解释,此处不再赘述~

HuffmanNode* buildHuffmanTree(const vector<char>& data, const vector<int>& frequency) {
    priority_queue<HuffmanNode*, vector<HuffmanNode*>, Compare> pq; //声明了一个优先队列(priority_queue),其中存储的是HuffmanNode类型的对象指针。这个优先队列使用了一个比较函数(Compare)来定义元素的优先级

    // 创建叶结点并将它们插入优先队列
    for (int i = 0; i < data.size(); i++) {
        pq.push(new HuffmanNode(data[i], frequency[i]));
    }

    // 构建哈夫曼树
    while (pq.size() > 1) {
        HuffmanNode* left = pq.top();   //队首元素记录并出列,即左子结点(left)
        pq.pop();
        HuffmanNode* right = pq.top();  //队首元素记录并出列,即右子结点(right)
        pq.pop();

        HuffmanNode* newNode = new HuffmanNode('$', left->frequency + right->frequency);    //创建了一个新的HuffmanNode对象,它的频率是左子节点和右子节点的频率之和
        newNode->left = left;   //在树中链接新节点和左子结点
        newNode->right = right; //在树中链接新节点和右子结点

        pq.push(newNode);       //将新的结点插回队列
    }

    return pq.top();    //返回根结点
}

 🔯P4:打印哈夫曼树编码

传入树的根结点内存地址,编码由空值开始,以左子树+0,右子树+1的方式遍历树中的结点,如果是叶结点则打印编码~

void printHuffmanCodes(HuffmanNode* root, string code) {
    if (root == nullptr) {
        return;
    }

    // 如果是叶结点,则打印字符和对应的编码
    if (!root->left && !root->right) {
        cout << root->data << " : " << code << endl;
    }

    // 递归打印左子树和右子树
    printHuffmanCodes(root->left, code + "0");
    printHuffmanCodes(root->right, code + "1");
}

 🔯P5:计算哈夫曼树的权值路径长度(WPL)

传入树的根结点内存地址,树高由0开始(根结点那一行不算权值),先序遍历树中的结点,如果遇到叶子结点则计算权值(频度x权值),并返回加和~

先序遍历的内容可以看这里:🌸[树:双亲、孩子、兄弟表示法][二叉树:先序、中序、后序遍历]

int calculateWPL(HuffmanNode* root, int depth) {
    if (root == nullptr) {
        return 0;
    }

    // 如果是叶结点,返回权值乘以深度
    if (!root->left && !root->right) {
        return root->frequency * depth;
    }

    // 递归计算左子树和右子树的WPL
    int leftWPL = calculateWPL(root->left, depth + 1);
    int rightWPL = calculateWPL(root->right, depth + 1);

    return leftWPL + rightWPL;
}

 🔯P6:main函数

main函数除了P0~P5的函数调用,就创建了频度与结点值,以及示意性地增加了结果输出~

int main() {
    // 示例数据
    vector<char> data = {'A', 'B', 'C', 'D'};
    vector<int> frequency = {7, 5, 2, 4};

    // 构建哈夫曼树
    HuffmanNode* root = buildHuffmanTree(data, frequency);

    // 打印哈夫曼树的编码
    cout << "Huffman Codes:" << endl;
    printHuffmanCodes(root, "");

    // 计算并打印哈夫曼树的权值路径长度(WPL)
    int wpl = calculateWPL(root, 0);
    cout << "Weighted Path Length (WPL): " << wpl << endl;

    return 0;
}

🧵完整代码

 🔯P0:完整代码

为了凑本文的字数,我这里贴一下整体的代码,删掉了细部注释~

#include <iostream>
#include <queue>
#include <vector>
using namespace std;

// 哈夫曼树的结点定义
struct HuffmanNode {
    char data;
    int frequency;
    HuffmanNode *left, *right;

    HuffmanNode(char data, int frequency) {
        this->data = data;
        this->frequency = frequency;
        left = right = nullptr;
    }
};

// 用于优先队列中的比较函数
struct Compare {
    bool operator()(HuffmanNode* a, HuffmanNode* b) {
        return a->frequency > b->frequency;
    }
};

// 生成哈夫曼树
HuffmanNode* buildHuffmanTree(const vector<char>& data, const vector<int>& frequency) {
    priority_queue<HuffmanNode*, vector<HuffmanNode*>, Compare> pq;

    for (int i = 0; i < data.size(); i++) {
        pq.push(new HuffmanNode(data[i], frequency[i]));
    }

    while (pq.size() > 1) {
        HuffmanNode* left = pq.top();
        pq.pop();
        HuffmanNode* right = pq.top();
        pq.pop();

        HuffmanNode* newNode = new HuffmanNode('$', left->frequency + right->frequency);
        newNode->left = left;
        newNode->right = right;

        pq.push(newNode);
    }

    return pq.top();
}

// 打印哈夫曼树中的编码
void printHuffmanCodes(HuffmanNode* root, string code) {
    if (root == nullptr) {
        return;
    }

    if (!root->left && !root->right) {
        cout << root->data << " : " << code << endl;
    }

    printHuffmanCodes(root->left, code + "0");
    printHuffmanCodes(root->right, code + "1");
}

// 计算哈夫曼树的权值路径长度(WPL)
int calculateWPL(HuffmanNode* root, int depth) {
    if (root == nullptr) {
        return 0;
    }

    if (!root->left && !root->right) {
        return root->frequency * depth;
    }

    int leftWPL = calculateWPL(root->left, depth + 1);
    int rightWPL = calculateWPL(root->right, depth + 1);

    return leftWPL + rightWPL;
}

int main() {

    vector<char> data = {'A', 'B', 'C', 'D'};
    vector<int> frequency = {7, 5, 2, 4};

    HuffmanNode* root = buildHuffmanTree(data, frequency);

    cout << "Huffman Codes:" << endl;
    printHuffmanCodes(root, "");

    int wpl = calculateWPL(root, 0);
    cout << "Weighted Path Length (WPL): " << wpl << endl;

    return 0;
}

 🔯P1:执行结果

运行结果如下图所示~


🔚结语

博文到此结束,写得模糊或者有误之处,欢迎小伙伴留言讨论与批评,督促博主优化内容,不限于以下内容~😶‍🌫️😶‍🌫️

  • 有错误:这段注释南辕北辙,理解错误,需要更改~
  • 难理解:这段代码雾里看花,需要更换排版、增加语法、逻辑注释或配图~
  • 不简洁:这段代码瘠义肥辞,好像一座尸米山,需要更改逻辑;如果是C++语言,调用某库某语法还可以简化~
  • 缺功能:这段代码败絮其中,能跑,然而不能用,想在实际运行或者通过考试需要增加功能~
  • 跑不动:这不可能——好吧,如果真不能跑,告诉我哪里不能跑我再回去试试...

博文若有帮助,欢迎小伙伴动动可爱的小手默默给个赞支持一下~🌟🌟

有兴趣可以看看博主其它的博文,说不定会遇到感兴趣的内容~🌟🌟

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

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

相关文章

一纸文书之MySQL的回忆录

MySQL要点学习&#xff1a;你可以在简历上说熟悉MySQL 什么是数据库&#xff1f;什么是数据库管理系统&#xff1f;什么是MySQL&#xff1f;什么是SQL&#xff1f;数据库数据库管理系统&#xff1a;SQL&#xff1a;结构化查询语言三者之间的关系 安装MySQL数据库管理系统MySQL常…

数据增强之裁剪、翻转与旋转

文章和代码已经归档至【Github仓库&#xff1a;https://github.com/timerring/dive-into-AI 】或者公众号【AIShareLab】回复 pytorch教程 也可获取。 文章目录 数据增强 Data Augmentation裁剪Croptransforms.CenterCroptransforms.RandomCroptransforms.RandomResizedCroptra…

【雕爷学编程】Arduino动手做(153)---2.4寸TFT液晶触摸屏模块7

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

TortoiseGit 入门指南02:创建和克隆仓库

图标 本节讲解如何使用 TortoiseGit 创建和克隆仓库。但在此之前&#xff0c;我们先来看下 TortoiseGit 软件的一个特色&#xff1a;图标。 TortoiseGit 会给 工作区 中的文件和文件夹叠加图标&#xff08;Icon Overlays&#xff09;&#xff0c;图标反应的是这些文件和文件夹…

跳表很难吗?手把手教你如何跳跃它!

文章目录 Ⅰ. 前言Ⅱ. 跳表&#xff08;skiplist&#xff09;1、什么是跳表2、跳表的发明历程3、跳表的搜索方式 Ⅲ. skiplist的算法性能分析1、理论准备2、性能分析&#xff08;了解即可&#xff0c;主要记结论&#xff09; Ⅳ. skiplist与平衡树、哈希表的比较Ⅴ. skiplist的…

【数字信号处理】线性调频Z(Chirp-Z,CZT)算法详解

CZT变换算法的引入 CZT算法的基本原理 注意:这里所要分析的复频谱点数为 M M M,这也是CZT变换之后的点数。

MySQL之DML和DDL

1、显示所有职工的基本信息&#xff1a; 2、查询所有职工所属部门的部门号&#xff0c;不显示重复的部门号。 3、求出所有职工的人数。 4、列出最高工和最低工资。 5、列出职工的平均工资和总工资。 6、创建一个只有职工号、姓名和参加工作的新表&#xff0c;名为工作日期表。 …

CentOS环境下的MYSQL8安装

MySQL 安装 参考连接&#xff1a;https://www.cnblogs.com/jasonx1an/p/16690866.html 下载 下载网址&#xff1a;https://dev.mysql.com/downloads/mysql/ 卸载 mariadb 查看 mariadb rpm -qa|grep mariadb卸载 mariadb rpm -e mariadb-libs-5.5.68-1.el7.x86_64 --nodeps再…

深度学习调参技巧

一、常用的网络模型训练技巧&#xff1f; 使用更大的 batch size。使用更大的 batch size 可以加快训练的进度。但是对于凸优化问题&#xff0c;收敛速度会随着 batch size 的增加而降低。所以在相同的 epoch 下&#xff0c;使用更大的 batch size 可能会导致验证集的 acc更低…

Unittest加载执行用例的方法总结

目录 前言 方式1 方式2 方式3 方式4 方式5 方式6 方式7 总结 前言 说到测试框架&#xff0c;unittest是我最先接触的自动化测试框架之一了&#xff0c; 而且也是用的时间最长的&#xff0c; unittest框架有很多方法加载用例&#xff0c;让我们针对不同的项目&#xff0…

23 | MySQL是怎么保证数据不丢的?

以下内容出自《MySQL 实战 45 讲》 23 | MySQL是怎么保证数据不丢的&#xff1f; binlog 的写入机制 1、事务执行过程中&#xff0c;先把日志写到 binlog cache&#xff0c;事务提交的时候&#xff0c;再把 binlog cache 写到 binlog 文件中。 2、一个事务的 binlog 是不能被…

rust学习-所有权

运行程序必须管理使用内存的方式 &#xff08;1&#xff09;一些语言中具有垃圾回收机制&#xff0c;程序运行时不断寻找不再使用的内存 &#xff08;2&#xff09;一些语言中&#xff0c;开发者必须亲自分配和释放内存 &#xff08;3&#xff09;Rust 通过所有权系统管理内存…

Windows操作系统安全加固

Windows操作系统安全加固 一、安全加固基本思路1.1、安全基线1.2、系统信息审查 二、Windows安全配置加固2.1、漏洞修复——补丁安装2.2、漏洞修复——端口封禁2.2.1、windows高危端口加固实践——封禁135端口对外开放 2.3、安全配置加固——账号口令 一、安全加固基本思路 1.…

10.20UEC++/代理,单播,多播

构建一个无参无返回值类型的函数&#xff08;也可以有参有返回值类型&#xff09; 相对应的构建一个无参无返回值类型的代理

【计算机组成与体系结构课程设计】上机考试

1 (1) 针对图中的MIPS处理器数据通路(不考虑I/O)&#xff0c;用红色或蓝色描出执行sw指令时的数据通路。&#xff08;将该图下载到电脑&#xff0c;并用画图完成描线&#xff09; (2) 写出执行sw指令时&#xff0c;各个元件控制端信号应该置什么值? 2 基于Minisys处理…

Qt保存代码

补全保存代码 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent) :QWidget(parent),ui(new Ui::Widget) {ui->setupUi(this); }Widget::~Widget() {delete ui; }//字体按钮对应的槽函数 void Widget::on_fontBtn_clicked() {…

使用常见的三个命令分析线程的信息

目录 jps jstack.exe jvisualvm.exe jmc.exe 这三个都在jdk\bin文件夹中&#xff01;&#xff01;&#xff01; 查看线程等待状态与信息可以采用3种常见命令。 本文中针对以下代码进行演示 package ChapterOne.test;public class Run3 {public static void main(String[…

Coggle 30 Days of ML(23年7月)任务四:线性模型训练与预测

Coggle 30 Days of ML&#xff08;23年7月&#xff09;任务四&#xff1a;线性模型训练与预测 任务四&#xff1a;使用TFIDF特征和线性模型完成训练和预测 说明&#xff1a;在这个任务中&#xff0c;你需要使用TFIDF特征和线性模型&#xff08;如逻辑回归&#xff09;完成训练…

图像处理-比特平面分层和重构

一、比特平面分层 像素是由比特组成的数字。例如在256级灰度图像中&#xff0c;每个像素的灰度是由8比特&#xff08;一个字节&#xff09;组成。如下图所示&#xff0c;一幅8比特图像由8个1比特平面组成&#xff0c;其中平面1包含图像中所有像素的最低阶比特&#xff0c;而平…