数据结构 哈夫曼树(HuffmanTree) 优先队列实现

哈夫曼树(HuffmanTree)

给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近

简介

在计算机数据处理中,哈夫曼编码使用变长编码表对源符号(如文件中的一个字母)进行编码,其中变长编码表是通过一种评估来源符号出现机率的方法得到的,出现机率高的字母使用较短的编码,反之出现机率低的则使用较长的编码,这便使编码之后的字符串的平均长度、期望值降低,从而达到无损压缩数据的目的。
例如,在英文中,e的出现机率最高,而z的出现概率则最低。当利用哈夫曼编码对一篇英文进行压缩时,e极有可能用一个比特来表示,而z则可能花去25个比特(不是26)。用普通的表示方法时,每个英文字母均占用一个字节,即8个比特。二者相比,e使用了一般编码的1/8的长度,z则使用了3倍多。倘若我们能实现对于英文中各个字母出现概率的较准确的估算,就可以大幅度提高无损压缩的比例。
哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树。所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数)。树的路径长度是从树根到每一结点的路径长度之和,记为WPL=(W1L1+W2L2+W3L3+…+WnLn),N个权值Wi(i=1,2,…n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径长度为Li(i=1,2,…n)。可以证明哈夫曼树的WPL是最小的。

术语

哈夫曼树又称为最优树.

  1. 路径和路径长度
    在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1。
  2. 结点的权及带权路径长度
    若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
  3. 树的带权路径长度
    树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。

构建哈夫曼树

  1. 创建一个包含所有字符的节点的优先队列(或最小堆),其中每个节点的权重是对应字符的频率。
  2. 从队列中选择两个具有最小权重的节点,并创建一个新节点作为它们的父节点。新节点的权重是这两个节点的权重之和。
  3. 将新节点插入队列。
  4. 重复步骤2和步骤3,直到队列中只剩下一个节点,这个节点就是哈夫曼树的根节点。
  5. 通过遍历哈夫曼树,给每个字符分配一个唯一的编码。通常,向左走表示添加一个"0",向右走表示添加一个"1"。

多叉哈夫曼树

哈夫曼树也可以是k叉的,只是在构造k叉哈夫曼树时需要先进行一些调整。构造哈夫曼树的思想是每次选k个权重最小的元素来合成一个新的元素,该元素权重为k个元素权重之和。但是当k大于2时,按照这个步骤做下去可能到最后剩下的元素少于k个。解决这个问题的办法是假设已经有了一棵哈夫曼树(且为一棵满k叉树),则可以计算出其叶节点数目为(k-1)nk+1,式子中的nk表示子节点数目为k的节点数目。于是对给定的n个权值构造k叉哈夫曼树时,可以先考虑增加一些权值为0的叶子节点,使得叶子节点总数为(k-1)nk+1这种形式,然后再按照哈夫曼树的方法进行构造即可。

实现

实现哈夫曼树的方式有很多种,可以使用优先队列(Priority Queue)简单达成这个过程,给与权重较低的符号较高的优先级(Priority),算法如下:

  1. 把n个终端节点加入优先队列,则n个节点都有一个优先权Pi,1 ≤ i ≤ n
  2. 如果队列内的节点数>1,则:
    ⑴从队列中移除两个最小的Pi节点,即连续做两次remove(min(Pi), Priority_Queue)
    ⑵产生一个新节点,此节点为(1)之移除节点之父节点,而此节点的权重值为(1)两节点之权重和
    ⑶把(2)产生之节点加入优先队列中
  3. 最后在优先队列里的点为树的根节点(root)
    而此算法的时间复杂度(Time Complexity)为O(n log n);因为有n个终端节点,所以树总共有2n-1个节点,使用优先队列每个循环须O(log n)。
    实现代码:(以cpp为例)
#include <iostream>
#include <queue>
#include <unordered_map>using namespace std;// 哈夫曼树节点的定义
struct Node {char data;int frequency;Node* left;Node* right;Node(char data, int frequency) : data(data), frequency(frequency), left(nullptr), right(nullptr) {}// 用于 priority_queue 中比较节点的大小bool operator>(const Node& other) const {return frequency > other.frequency;}
};// 构建哈夫曼树的函数
Node* buildHuffmanTree(const unordered_map<char, int>& frequencies) {// 优先队列,用于存储节点,并按照频率从小到大排列priority_queue<Node, vector<Node>, greater<Node>> pq;// 将字符频率转换为节点,并加入优先队列for (const auto& entry : frequencies) {pq.push(Node(entry.first, entry.second));}// 构建哈夫曼树while (pq.size() > 1) {// 取出两个最小频率的节点Node* left = new Node(pq.top().data, pq.top().frequency);pq.pop();Node* right = new Node(pq.top().data, pq.top().frequency);pq.pop();// 创建一个新节点作为它们的父节点,并将新节点加入优先队列Node* internalNode = new Node('\0', left->frequency + right->frequency);internalNode->left = left;internalNode->right = right;pq.push(*internalNode);}// 返回哈夫曼树的根节点return new Node('\0', pq.top().frequency);
}// 生成哈夫曼编码的递归辅助函数
void generateHuffmanCodes(Node* root, string currentCode, unordered_map<char, string>& codes) {if (root) {if (root->data != '\0') {codes[root->data] = currentCode;}generateHuffmanCodes(root->left, currentCode + "0", codes);generateHuffmanCodes(root->right, currentCode + "1", codes);}
}// 生成哈夫曼编码的函数
unordered_map<char, string> getHuffmanCodes(Node* root) {unordered_map<char, string> codes;generateHuffmanCodes(root, "", codes);return codes;
}int main() {// 示例字符频率字典unordered_map<char, int> frequencies = {{'a', 5}, {'b', 9}, {'c', 12}, {'d', 13}, {'e', 16}, {'f', 45}};// 构建哈夫曼树Node* root = buildHuffmanTree(frequencies);// 生成哈夫曼编码unordered_map<char, string> codes = getHuffmanCodes(root);// 打印字符和对应的哈夫曼编码for (const auto& entry : codes) {cout << entry.first << ": " << entry.second << endl;}return 0;
}

此外,有一个更快的方式使时间复杂度降至线性时间(Linear Time)O(n),就是使用两个队列(Queue)创建哈夫曼树。第一个队列用来存储n个符号(即n个终端节点)的权重,第二个队列用来存储两两权重的合(即非终端节点)。此法可保证第二个队列的前端(Front)权重永远都是最小值,且方法如下:
4. 把n个终端节点加入第一个队列(依照权重大小排列,最小在前端)
5. 如果队列内的节点数>1,则:
⑴从队列前端移除两个最低权重的节点
⑵将(1)中移除的两个节点权重相加合成一个新节点
⑶加入第二个队列
6. 最后在第一个队列的节点为根节点

虽然使用此方法比使用优先队列的时间复杂度还低,但是注意此法的第1项,节点必须依照权重大小加入队列中,如果节点加入顺序不按大小,则需要经过排序,则至少花了O(n log n)的时间复杂度计算。
但是在不同的状况考量下,时间复杂度并非是最重要的,如果我们考虑英文字母的出现频率,变量n就是英文字母的26个字母,则使用哪一种算法时间复杂度都不会影响很大,因为n不是一笔庞大的数字。

实现代码:(以cpp为例)

#include <iostream>
#include <queue>
#include <vector>using namespace std;// 哈夫曼树节点的定义
struct Node {char data;int frequency;Node* left;Node* right;Node(char data, int frequency) : data(data), frequency(frequency), left(nullptr), right(nullptr) {}// 用于 priority_queue 中比较节点的大小bool operator>(const Node& other) const {return frequency > other.frequency;}
};// 构建哈夫曼树的函数
Node* buildHuffmanTree(const vector<int>& weights) {priority_queue<Node*, vector<Node*>, greater<Node*>> minHeap; // 存储最小权重的节点for (int i = 0; i < weights.size(); ++i) {minHeap.push(new Node(char('a' + i), weights[i]));}while (minHeap.size() > 1) {// 取出两个最小权重的节点Node* left = minHeap.top();minHeap.pop();Node* right = minHeap.top();minHeap.pop();// 创建一个新节点作为它们的父节点,并将新节点加入最小堆Node* internalNode = new Node('\0', left->frequency + right->frequency);internalNode->left = left;internalNode->right = right;minHeap.push(internalNode);}// 返回哈夫曼树的根节点return minHeap.top();
}// 生成哈夫曼编码的递归辅助函数
void generateHuffmanCodes(Node* root, string currentCode, unordered_map<char, string>& codes) {if (root) {if (root->data != '\0') {codes[root->data] = currentCode;}generateHuffmanCodes(root->left, currentCode + "0", codes);generateHuffmanCodes(root->right, currentCode + "1", codes);}
}// 生成哈夫曼编码的函数
unordered_map<char, string> getHuffmanCodes(Node* root) {unordered_map<char, string> codes;generateHuffmanCodes(root, "", codes);return codes;
}int main() {// 示例权重数组vector<int> weights = {5, 9, 12, 13, 16, 45};// 构建哈夫曼树Node* root = buildHuffmanTree(weights);// 生成哈夫曼编码unordered_map<char, string> codes = getHuffmanCodes(root);// 打印字符和对应的哈夫曼编码for (const auto& entry : codes) {cout << entry.first << ": " << entry.second << endl;}return 0;
}

综合题

【问题描述】在数据压缩问题中,需要将数据文件转换成由二进制字符0、1组成的二进制串,称之为编码,已知待压缩的数据中包含若干字母(A-Z),为获得更好的空间效率,请设计有效的用于数据压缩的二进制编码,使数据文件压缩后编码总长度最小,并输出这个最小长度值。

【输入形式】待压缩的数据(长度不大于100的大写字母)

【输出形式】编码的最小总长度值

【样例输入】ABACCDA

【样例输出】13

【样例说明】A编码0,B编码110,C编码10,D编码111,ABACCDA的编码为0110010101110

代码实现

#include <iostream>
#include <queue>
#include <vector>using namespace std;const int MAX_CHAR = 26;  // 大写字母的个数// 定义节点结构
struct Node {char data;unsigned freq;Node* left, *right;Node(char data, unsigned freq) : data(data), freq(freq), left(nullptr), right(nullptr) {}
};// 比较节点的频率
struct compare {bool operator()(Node* left, Node* right) {return (left->freq > right->freq);}
};
//构建最小堆时用作比较函数// 生成哈夫曼树
Node* buildHuffmanTree(const string& data) {priority_queue<Node*, vector<Node*>, compare> minHeap;// 统计字符频率int freq[MAX_CHAR] = {0};for (char c : data) {freq[c - 'A']++;}// 创建节点并加入最小堆for (int i = 0; i < MAX_CHAR; ++i) {if (freq[i] > 0) {minHeap.push(new Node('A' + i, freq[i]));}}// 构建哈夫曼树while (minHeap.size() > 1) {Node* left = minHeap.top();minHeap.pop();Node* right = minHeap.top();minHeap.pop();Node* newNode = new Node('$', left->freq + right->freq);//'$'用作内部节点数据,没有实际意义newNode->left = left;newNode->right = right;minHeap.push(newNode);}return minHeap.top();
}// 计算哈夫曼编码长度
unsigned calculateHuffmanCodeLength(Node* root, unsigned depth = 0) {if (!root)return 0;if (!root->left && !root->right)return root->freq * depth;
//递归计算哈夫曼树中每个叶子节点的编码长度,即路径长度乘以叶子节点的频率。return calculateHuffmanCodeLength(root->left, depth + 1) + calculateHuffmanCodeLength(root->right, depth + 1);
}// 主函数
int main() {string input;cin >> input;if (input.size() > 100) {return 1;}// 生成哈夫曼树Node* root = buildHuffmanTree(input);// 计算最小编码长度unsigned minLength = calculateHuffmanCodeLength(root);cout << minLength << endl;return 0;
}

上面代码使用了优先队列实现,使用数组统计频率,实现基于哈夫曼编码的数据压缩。

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

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

相关文章

LeetCode51. N-Queens

文章目录 一、题目二、题解 一、题目 The n-queens puzzle is the problem of placing n queens on an n x n chessboard such that no two queens attack each other. Given an integer n, return all distinct solutions to the n-queens puzzle. You may return the answe…

PTA:输入一个合数n,将n进行质因数分解

题目 输入一个正整数n&#xff0c;如果n为合数除了1和本身&#xff0c;还有因数的称为合数)&#xff0c;将n进行质因数分解。例如&#xff0c;输入100&#xff0c;输出2、2、5、5&#xff0c;当输入不为合数时&#xff0c;输出error 输入格式: 请在这里写输入格式。例如&…

计算机网络408

一&#xff1a;计算机网络体系结构 1.计网的概念&#xff0c;组成&#xff0c;功能和分类 一&#xff1a;计算机网络的发展 (3)从功能组成视觉看&#xff1a;分为资源子网和通信子网 2.计网性能指标

性能自动化测试?

一、思考❓❔ 1.什么是性能自动化测试? 性能 系统负载能力超负荷运行下的稳定性系统瓶颈 自动化测试 使用程序代替手工提升测试效率性能自动化 使用代码模拟大批量用户让用户并发请求多页面多用户并发请求采集参数&#xff0c;统计系统负载能力生成报告 2.Python中的性能…

CompletableFuture高并发和线程池使用

文章目录 概要 多线程CountDownLatch 线程池CompletableFuture CompletableFuture介绍 概要 在Java中&#xff0c;有些任务单线程执行时间长&#xff0c;一般会使用多线程增加并发提高运行速率。但很多情况下&#xff0c;需要所有子线程执行完&#xff0c;才能往下执行主线…

RabbitMQ工作模式2 整合springboot 和MQ高级特性

RabbitMQ工作模式 1.路由模式 创建交换机 , 连接队列 (生产者) public class MyTestExDirect {Testpublic void bbb() throws IOException, TimeoutException {ConnectionFactory connectionFactory new ConnectionFactory();//连接mqconnectionFactory.setUsername("…

可信区块链运行监测服务平台(TBM)发展研讨会在北京召开

2023年11月23日&#xff0c;由中国信息通信研究院、中国移动通信集团设计院有限公司、区块链服务网络&#xff08;BSN&#xff09;发展联盟共同主办的“可信区块链运行监测服务平台&#xff08;TBM&#xff09;发展研讨会”在北京成功举行。会议围绕区块链的监测与治理&#xf…

小程序如何实现下拉刷新?

一、全局下拉刷新 在app.json的window节点中&#xff0c;将enablePullDownRefresh设置为true&#xff1b; onPullDownRefresh: function () {console.log(下拉刷新);// 在这里编写数据更新的逻辑wx.stopPullDownRefresh(); // 数据更新完成后&#xff0c;调用该方法停止刷新}二…

vatee万腾的科技征途:Vatee数字化力量的新视野

在科技的浪潮中&#xff0c;Vatee万腾正展开一场引人注目的科技征途&#xff0c;以其独特的数字化力量描绘出一片新的视野。这不仅是一次技术的升级&#xff0c;更是一场对未来的全新探索&#xff0c;为我们带来了前所未有的数字化时代。 Vatee万腾以其卓越的技术实力和前瞻性的…

springboot实现数据脱敏

springboot实现数据脱敏 怎么说呢&#xff0c;写着写着发觉 ”这写的什么玩意“ 。 总的来说就是&#xff0c;这篇文章并不能解决数据脱敏问题&#xff0c;但以下链接可以。 SpringBoot中利用自定义注解优雅地实现隐私数据脱敏 然后回到本文&#xff0c;本来是想基于AOP代理&am…

PHP众筹系统源码+支持报名众筹+商品众筹+无偿众筹+市面上所有的众筹模式 附带完整的搭建教程

大家好啊&#xff0c;罗峰今天来给大家分好用的源码系统了。今天要给大家分享的是一款PHP众筹系统源码。众筹作为一种新型的融资方式&#xff0c;逐渐在市场上占据了重要的地位。从公益众筹到商品众筹&#xff0c;再到股权众筹&#xff0c;各种众筹模式层出不穷。然而&#xff…

ELK日志系统

&#xff08;一&#xff09;ELK 1、elk&#xff1a;是一套完整的日志集中处理方案&#xff0c;由三个开源的软件简称组成 2、E&#xff1a;ElasticSearch&#xff08;ES&#xff09;&#xff0c;是一个开源的&#xff0c;分布式的存储检索引擎&#xff08;索引型的非关系型数…

WebSocket--1.协议解析

目录 一.概念 二.建立流程 三.四大事件 五.js中建立ws链接 六.springboot中进行ws连接 1.首先&#xff0c;添加WebSocket的依赖到你的Spring Boot项目中。 2.接下来&#xff0c;创建一个WebSocket处理器 3.最后&#xff0c;创建一个配置类&#xff0c;注册该WebSocket处理…

后端整合Swagger+Knife4j接口文档

后端整合SwaggerKnife4j接口文档 接口文档介绍 什么是接口文档&#xff1a;写接口信息的文档&#xff0c;条接口包括&#xff1a; 请求参数响应参数 错误码 接口地址接口名称请求类型请求格式备注 为什么需要接口文档 who用&#xff1f;后端提供&#xff0c;前后端都需要使用…

python:类中静态方法,类方法和实例方法的使用与区别

python 类里面常用的方法有3个&#xff1a;静态方法(staticmethod)&#xff0c;类方法(classmethod)和实例方法(self) 1. 函数和方法 1.1 函数&#xff1a; 函数定义是由def()关键字定义 def fun():a "hello"return a# 函数调用 res fun() print(res)1.2 方法-…

ESXi 添加虚拟闪存 无可选设备问题排查

虚拟内存是计算机系统中的一种技术&#xff0c;它可以将计算机硬盘的一部分空间作为临时存储器来使用。当计算机的物理内存&#xff08;RAM&#xff09;不足时&#xff0c;操作系统可以将部分数据从内存移至硬盘的虚拟内存空间中&#xff0c;以释放内存供其他程序使用。虚拟内存…

uniapp基础-教程之HBuilderX配置篇-01

uniapp教程之HBuilderX配置篇-01 为什么要做这个教程的梳理&#xff0c;主要用于自己学习和总结&#xff0c;利于增加自己的积累和记忆。首先下载HBuilderX&#xff0c;并保证你的软件在C盘进行运行&#xff0c;最好使用英文或者拼音&#xff0c;这个操作是为了保证软件的稳定…

羊大师提示,别让坏习惯影响生活

羊大师提示&#xff0c;别让坏习惯影响生活 拖延是人们常常会遇到的一种坏习惯&#xff0c;它不仅浪费时间&#xff0c;还会对生活、工作和学习造成负面影响。为了改变这种坏习惯&#xff0c;我们需要采取一系列的措施&#xff0c;从根本上改变自己的生活方式。下面小编羊大师…

qt 5.15.2读取csv文件功能

qt 5.15.2读取csv文件功能 工程文件.pro 内容&#xff1a; QT core#添加网络模块 QT networkCONFIG c17 cmdline# You can make your code fail to compile if it uses deprecated APIs. # In order to do so, uncomment the following line. #DEFINES QT_DISABLE_DEPREC…

bat脚本执行py文件

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…