Huffman(哈夫曼)解/压缩算法实现

news2025/7/13 9:17:14

一、文件压缩

        哈夫曼压缩算法需要对输入的文件,逐字节扫描,统计出不同字节出现的数量(频率),根据的得到的频率生成一组叶子节点,这些节点存储着<字节信息>和<频率>,通常需要按频率排序后存储在数组中,更好的做法是存储在小顶堆中;只要堆/数组的大小大于1,每次新建一个节点,取出频率最小的两个节点作为新节点的左右子节点(不必在意谁是左右),两个节点的频率和作为新节点的频率,将新节点放入堆/数组中;最后剩余的节点便是哈夫曼树的根节点。哈夫曼树的数据结构如下所示:

class Node {
public:
	using Ptr = std::shared_ptr<Node>;
	void SetLeft(Node::Ptr node);
	void SetRight(Node::Ptr node);
	Node::Ptr Left();
	Node::Ptr Right();
	void SetChar(uint8_t character);
	uint8_t Char();
	void SetCount(size_t count);
	size_t Count();
	bool HasLeft();
	bool HasRight();
private:
	Node::Ptr _left{ nullptr };//左节点
	Node::Ptr _right{ nullptr };//右节点
	size_t _count{ 0 };//数量/频率
	uint8_t _character{ 0x00 };//字符/字节
};
struct NodeCompare {
	bool operator()(const Node::Ptr& left, const Node::Ptr& right) {
		return left->Count() > right->Count();
	}
};
//小顶堆
std::priority_queue<Node::Ptr, std::vector<Node::Ptr>, NodeCompare> pending;

std::unordered_map<uint8_t, size_t> mapper;
for (uint8_t character : input) {
	mapper[character]++;
}

for (const auto& pair : mapper) {
	pending.push(Node::MakeNode(pair.first, pair.second));
}

if (pending.empty()) return nullptr;

if (pending.size() == 1) {
	return pending.top();
}

while (pending.size() > 1) {
	auto left = pending.top();
	pending.pop();
	auto right = pending.top();
	pending.pop();
	pending.push(Node::MakeNode(0, left->Count() + right->Count(), left, right));
}

         得到哈夫曼树后,需要重新扫描输入数据,针对每个字节数据到哈夫曼树中查找编码;为避免查询时频繁搜索整棵树,需要先遍历一次树,将字符作为键,生成的编码作为值,存储在map中;为了存储二进制的编码,需要引入新的数据结构:

class BitSet {
public:
	void Set(size_t pos);
	bool Test(size_t pos) const;
	void Reset(size_t pos);
private:
	std::vector<uint8_t> data;
	size_t count{ 0 };
};

         针对这个数据结构,BitSet::Set用于将指定bit设置为1,BitSet::Reset将bit设置为0,BitSet::Test用于检测bit是0/1;由于uint8_t(char)是我们能操作的最小单位,因此BitSet的数据data使用动态uint8_t数组或std::vector<uint8_t>,即使只存储一个bit也必须申请一个字节的空间,因此必须使用count记录实际的bit数(如果使用的是动态数组而不是std::vector,还需要记录实际的字节数byte)。

        使用BitSet存储编码数据时,对应的原始字符时8bit的uint8_t,最大可存储256中字符,也就是说极端情况下我们得到的编码长度最大为256,但是极限编码通常事字符频率呈现指数分布,因此最大长度16bit(2 uint8_t)的编码即可满足要求。为了操作方便,即使编码的长度不足16,我们也预分配16bit的空间,避免超过一个字节时重新分配空间。

        遍历哈夫曼树,开始时将根节点和空BitSet置于栈中;栈不为空时,每次循环取出节点和BitSet A,如果节点有右子节点,复制A得到B并将其当前位设为1,将右子节点和新的BitSet B压入栈中;如果又左子节点,复制A得到C并将其当前位设为0,将左子节点和新的BitSet C压入栈中;如果没有子节点,则将节点存储的字符和当前的BitSet存入map中。(此时可以将字符频率乘以BitSet的bit数,就得到压缩后数据的总bit数)

std::unordered_map<uint8_t, BitSet> mapper;
struct StagingItem
{
	Node::Ptr node{ nullptr };
	BitSet code{};
};
std::stack<StagingItem> pending;
pending.emplace(root, BitSet(0,2));
while (!pending.empty()) {
	auto node = pending.top().node;
	auto code = pending.top().code;
	pending.pop();
	if (node->HasRight()) {
		auto current = code;
		current.Push(true);
		pending.emplace(node->Right(),current);
	}
	if (node->HasLeft()) {
		auto current = code;
		current.Push(false);
		pending.emplace(node->Left(),current);
	}
	if (!node->HasChildren()) {
		mapper.emplace(node->Char(), code);
		dataBits += (node->Count() * code.Count());
	}
}

        得到std::unordered_map<uint8_t, BitSet> map,就可以逐个字节扫描需要压缩的数据,在map中查找对应二进制编码。由于上一步已经计算压缩后的总bit数,可以预分配一个BitSet encode_data(count = 总bit数) ,将查找到的二进制编码逐个写入encode_data;

BitSet encode_data(dataBits);
size_t index = 0;
for (const auto& character : input) {
	auto& code = mapper[character];
	for (size_t i = 0;i < code.Count();++i) {
		if (code.Test(i)) encode_data.Set(index);
		++index;
	}
}

        至此,我们完成了数据的压缩。但是,如果直接将数据写入文件,便无法再将文件解压成源文件。因此必须将哈夫曼树(字符、二进制编码、编码长度)也写入文件中。只有数据和哈夫曼树也还不能将文件恢复,因为无法区分哈夫曼树和文件数据的位置、大小。所以还需要一个新的数据结构(文件头)来描述这些信息:

struct Header
{
	size_t dataSize{ 0 };//原文件的字节数,用于解压时预分配空间
	size_t dataBits{ 0 };//文件编码的bit数,用于解压时限定数据的边界/长度
	uint32_t codeCount{ 0 };//哈夫曼编码的数量
	uint16_t reserve{ 0 };//预留/填充
	uint8_t label[2] = { 0x52,0x48 };//文件标签
};

        在网络通信中通常会用报文头标识信息,文件也一样,通常包含文件的 数据区的字节大小、压缩数据的比特数、编码区的字节大小、标签等信息。将文件头,哈夫曼编码和编码后的数据整合到数组中(如下图所示),即可将数据写入文件中,解压时便可根据文件头信息解压。

                由于哈夫曼编码的长度固定(如下图),因此头中只需要指定数量即可。编码长度最大为16bit,用一个字节即可存储编码长度(code size)。

        申请一个StagingItem的数组,大小为哈夫曼编码的数量,StagingItem::item的大小为4(上图数据结构的长度)。逐个将哈夫曼树的数据转移至数组中,最后将文件头,编码数组和编码得到的数据写入文件中。

Header header{};
header.dataSize = input.size();
header.dataBits = dataBits;
header.codeCount = static_cast<uint32_t>(mapper.size());

struct StagingItem {
	uint8_t item[2 + CODE_WIDTH];//CODE_WIDTH = 2;
};
size_t code_zone_size = mapper.size() * sizeof(StagingItem);
std::vector<StagingItem> staging(mapper.size());
size_t i = 0;
for (const auto& pair : mapper) {
	staging[i].item[0] = ~pair.first;
	pair.second.ToBytes(&staging[i].item[1], CODE_WIDTH);
	staging[i].item[3] = static_cast<uint8_t>(pair.second.Count());
	++i;
}

size_t total = sizeof(Header) + code_zone_size + encode_data.DataSize();
std::vector<uint8_t> data(total);
memcpy(data.data(), &header, sizeof(Header));
memcpy(data.data() + sizeof(Header), staging.data(), code_zone_size);
memcpy(data.data() + sizeof(Header)+ code_zone_size, encode_data.DataPtr(), encode_data.DataSize());

二、文件解压

        将压缩文件读入内存中,本文将数据存储在std::vector<uint8_t>中,通过std::vector<uint8_t>::data()即可访问文件数据。下列代码意思是将uint8_t指针转为Header类型指针,这样就能直接通过->访问成员的属性,而不需要创建Header对象。通过标签校验文件是否为该算法压缩。除了标签,还可以在压缩时写入校验码、加密密钥等关键信息,通过校验头文件,可以提高操作的安全性。由于文件头最先写于,此时指针正指向文件头的其实位置。

auto header = reinterpret_cast<const Header*>(input.data());
if (header->label[0] != 0x52 && header->label[1] != 0x48) {
}

        文件头校验成功后,根据文件信息预分配编码空间items;代码auto codeptr = input.data() + sizeof(Header);表示跳过sizeof(Header)字节的位置,将指针指向此处,也就是哈夫曼编码存储的起始位置。用memcpy将数据写回StagingItem数组中;根据编码将哈夫曼树恢复:

        1.从根节点开始遍历编码,如果bit==1,前往右子节点,如果没有就先创建在前往;否则前往左子节点,没有也创建。

        2.如果遍历到最后一位编码,将编码字符存储在当前的节点中。

        3.重复1、2知道所有编码否恢复成树中的节点。

struct StagingItem {
	uint8_t item[2 + CODE_WIDTH];
};
auto codeptr = input.data() + sizeof(Header);
std::vector<StagingItem> items(header->codeCount);
size_t code_zone_size = header->codeCount * sizeof(StagingItem);
memcpy(items.data(), codeptr, code_zone_size);

auto root = Node::MakeNode(0, 0);
for (const auto& staging: items) {
	auto current = root;
	for (size_t i = 0;i < staging.item[3];++i) {
		if (BitSet::Test(&staging.item[1], i, staging.item[3])) {
			if (!current->HasRight()) current->SetRight(Node::MakeNode(0, 0));
			current = current->Right();
		}
		else {
			if (!current->HasLeft()) current->SetLeft(Node::MakeNode(0, 0));
			current = current->Left();
		}
	}
	current->SetChar(~staging.item[0]);
}

        将指针指向编码数据的起始位置,逐个bit访问,遇到1前往问右子节点,遇到0就前往左子节点,没有子节点则将数据节点存储的字符写到data中,并将节点重置为根节点。当编码数据的所有bit都处理完成,数据也就解压完成,此时将data写回文件中即可。

auto dataptr = codeptr + code_zone_size;
size_t index = 0;
std::vector<uint8_t> data(header->dataSize);
for (size_t i = 0;i < header->dataBits;) {
	auto current = root;
	while (current != nullptr) {
		if (!current->HasChildren()) {
			data[index] = current->Char();
			++index;
			break;
		}
		if (BitSet::Test(dataptr, i, header->dataBits)) {
			if (!current->HasRight()) {
				throw std::runtime_error("invalid right node");
			}
			current = current->Right();
		}
		else {
			if (!current->HasLeft()) {
				throw std::runtime_error("invalid left node");
			}
			current = current->Left();
		}
		++i;
	}
}
if (index != header->dataSize) {
	throw std::runtime_error("decompress failed,file has been broken");
}

    


    经过测试,debug模式下对于小文件压缩速率通常为10MB/s,解压速率为2MB/s;release模式下,压缩速率在55MB/s,解压速率10MB/s。下图是release模式下压缩1.1MB点云的数据。

        

        对于文本信息压缩比率较高,但是对音视频、图片直接压缩几乎没有效果;大文件的压缩效果也是优于小文件的,文件过小还可能导致叫上编码信息后压缩体积远大于源文件体积。

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

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

相关文章

迭代器模式:统一数据遍历方式的设计模式

迭代器模式&#xff1a;统一数据遍历方式的设计模式 一、模式核心&#xff1a;将数据遍历逻辑与数据结构解耦 在软件开发中&#xff0c;不同的数据结构&#xff08;如数组、链表、集合&#xff09;有不同的遍历方式。如果客户端直接依赖这些数据结构的内部实现来遍历元素&…

LeetCode每日一题4.23

题目 问题分析 计算每个数字的数位和&#xff1a;对于从 1 到 n 的每个整数&#xff0c;计算其十进制表示下的数位和。 分组&#xff1a;将数位和相等的数字放到同一个组中。 统计每个组的数字数目&#xff1a;统计每个组中有多少个数字。 找到并列最多的组&#xff1a;返回数…

RunnerGo API性能测试实战与高并发调优

API 性能测试通过模拟不同负载场景&#xff0c;量化评估 API 的响应时间、吞吐量、稳定性、可扩展性等性能指标&#xff0c;关注其在正常、高峰甚至极限负载下的表现。这有助于确保 API 稳定高效地运行&#xff0c;为调用者提供优质服务。 接下来&#xff0c;我们借助 RunnerG…

STM32——相关软件安装

本文是根据江协科技提供的教学视频所写&#xff0c;旨在便于日后复习&#xff0c;同时供学习嵌入式的朋友们参考&#xff0c;文中涉及到的所有资料也均来源于江协科技&#xff08;资料下载&#xff09;。 Keil5 MDK安装 1.安装Keil5 MDK2.安装器件支持包方法一&#xff1a;离线…

数据结构入门【算法复杂度】超详解深度解析

&#x1f31f; 复杂度分析的底层逻辑 复杂度是算法的"DNA"&#xff0c;它揭示了两个核心问题&#xff1a; 数据规模(n)增长时&#xff0c;资源消耗如何变化&#xff1f; 不同算法在极端情况下的性能差异有多大&#xff1f; 数学本质解析 复杂度函数 T(n)O(f(n))…

java多线程(7.0)

目录 ​编辑 定时器 定时器的使用 三.定时器的实现 MyTimer 3.1 分析思路 1. 创建执行任务的类。 2. 管理任务 3. 执行任务 3.2 线程安全问题 定时器 定时器是软件开发中的一个重要组件. 类似于一个 "闹钟". 达到一个设定的时间之后, 就执行某个指定好的…

Long类型封装Json传输时精度丢失问题

在信息做传输时&#xff0c;经常会使用到类型转换&#xff0c;这个时候因为一些问题会导致精度的丢失。在支付业务中这种问题更为致命。 这里我主动生成一个支付订单并将相关信息使用base64编码为一个二维码返回给前端进行支付&#xff0c;前端进行支付时我通过回调方法发现回调…

《从GPT崛起,看AI重塑世界》

《从GPT崛起,看AI重塑世界》 GPT 诞生:AI 领域的震撼弹 2022 年 11 月 30 日,OpenAI 发布了一款名为 ChatGPT 的人工智能聊天机器人程序,宛如一颗重磅炸弹投入了平静的湖面,迅速在全球范围内引发了轩然大波,成为了科技领域乃至大众舆论场中最热门的话题之一。一时间,无…

系统架构-安全架构设计

概述 对于信息系统来说&#xff0c;威胁有&#xff1a;物理环境&#xff08;最基础&#xff09;、通信链路、网络系统、操作系统、应用系统、管理系统 物理安全&#xff1a;系统所用设备的威胁&#xff0c;如自然灾害、电源故障通信链路安全&#xff1a;在传输线路上安装窃听…

鼠标指定范围内随机点击

鼠标指定范围内随机点击 点赞神器 将鼠标移动到相应位置后按F5 F6键&#xff0c;设置点击范围&#xff0c; F8开始&#xff0c;ESC中止。 有些直播有点赞限制&#xff0c;例如某音&#xff0c;每小时限制3千次&#xff0c;可以设置1200毫秒&#xff0c;3000次。 软件截图&#…

HashSet 概述

1. HashSet 概述 HashSet 是 Java 集合框架中 Set 接口的一个实现类&#xff0c;它存储唯一元素&#xff0c;即集合中不会有重复的元素。HashSet 基于哈希表&#xff08;实际上是 HashMap 实例&#xff09;来实现&#xff0c;不保证元素的顺序&#xff0c;并且允许存储 null 元…

遥测终端机,推动灌区流量监测向数据驱动跃迁

灌区范围那么大&#xff0c;每一滴水怎么流都关系到粮食够不够吃&#xff0c;还有生态能不能平衡。过去靠人工巡查、测量&#xff0c;就像拿着算盘想算明白大数据&#xff0c;根本满足不了现在水利管理的高要求。遥测终端机一出现&#xff0c;就像给灌区流量监测安上了智能感知…

蓝耘平台介绍:算力赋能AI创新的智算云平台

一、蓝耘平台是什么 蓝耘智算云&#xff08;LY Cloud&#xff09;是蓝耘科技打造的现代化GPU算力云服务平台&#xff0c;深度整合自研DS满血版大模型技术与分布式算力调度能力&#xff0c;形成"模型算力"双轮驱动的技术生态。平台核心优势如下&#xff1a; 平台定位…

QtDesigner中Button控件详解

一&#xff1a;Button控件 关于Button控件的主要作用就是作为触发开关&#xff0c;通过点击事件&#xff08;click&#xff09;执行代码逻辑&#xff0c;或者作为功能入口&#xff0c;跳转到其他界面或模块。 二&#xff1a;常见属性与配置 ①Button的enabled&#xff0c;大…

Flink 源码编译

打包命令 打包整个项目 mvn clean package -DskipTests -Drat.skiptrue打包单个模块 mvn clean package -DskipTests -Drat.skiptrue -pl flink-dist如果该模块依赖其他模块&#xff0c;可能需要先将其他模块 install 到本地&#xff0c;如果依赖的模块的源代码有修改&#…

docker的安装和简单使用(ubuntu环境)

环境准备 这里用的是linux的环境&#xff0c;如果没有云服务器的话&#xff0c;就是用虚拟环境吧。 虚拟环境的安装参考&#xff1a;vmware17的安装 linux镜像的安装 docker安装 我使用的是ubuntu&#xff0c;使用以下命令&#xff1a; 更新本地软件包索引 sudo apt u…

EasyRTC音视频实时通话在线教育解决方案:打造沉浸式互动教学新体验

一、方案概述 EasyRTC是一款基于WebRTC技术的实时音视频通信平台&#xff0c;为在线教育行业提供了高效、稳定、低延迟的互动教学解决方案。本方案将EasyRTC技术深度整合到在线教育场景中&#xff0c;实现师生间的实时音视频互动等核心功能&#xff0c;打造沉浸式的远程学习体…

【分布式系统中的“瑞士军刀”_ Zookeeper】一、Zookeeper 快速入门和核心概念

在分布式系统的复杂世界里&#xff0c;协调与同步是确保系统稳定运行的关键所在。Zookeeper 作为分布式协调服务的 “瑞士军刀”&#xff0c;为众多分布式项目提供了高效、可靠的协调解决方案。无论是在分布式锁的实现、配置管理&#xff0c;还是在服务注册与发现等场景中&…

Electron从入门到入门

项目说明 项目地址 项目地址&#xff1a;https://gitee.com/ruirui-study/electron-demo 本项目为示例项目&#xff0c;代码注释非常清晰&#xff0c;给大家当做入门项目吧。 其实很多东西都可以在我这基础上添加或修改、市面上有些已开源的项目&#xff0c;但是太臃肿了&am…

优化提示词方面可以使用的数学方法理论:信息熵,概率论 ,最优化理论

优化提示词方面可以使用的数学方法理论:信息熵,概率论 ,最优化理论 目录 优化提示词方面可以使用的数学方法理论:信息熵,概率论 ,最优化理论信息论信息熵明确问题主题提供具体细节限定回答方向规范语言表达概率论最优化理论信息论 原理:信息论中的熵可以衡量信息的不确定性。…