【项目】基于 Huffman 算法实现文件压缩

摘要:记录通过学习Huffman算法自主实现简单的文件压缩程序的过程。

什么是文件压缩

在古诗词中,这种信息的高度浓缩体现得淋漓尽致。例如王维的《使至塞上》中的名句大漠孤烟直,长河落日圆 。仅仅十个字,却描绘出了一幅极为宏大且细致的画面。它不仅包含了丰富的视觉信息,还营造出了一种苍茫、孤寂而又壮丽的意境。

 

从这些诗词中我们可以发现,诗词就像一个神奇的 “压缩包”,能够把大量的信息、复杂的画面以及深刻的情感压缩在简短的文字之中。而在计算机领域中,文件压缩的概念与之类似。文件压缩就是把文件中的数据看作是丰富的信息源,通过特定的算法,就如同诗人遣词造句的技巧一般,去除其中的冗余信息,对数据进行重新编码和组织。例如一个包含大量重复数据或者可以用更简洁方式表示的数据文件,经过压缩算法处理后,能够在不丢失关键信息的情况下,使文件的体积大大减小。这就像诗人把一幅复杂的画面和细腻的情感压缩成几句诗词一样,让文件在存储和传输过程中更加高效、便捷
 

文件压缩的本质是去除文件中的冗余信息以及对数据编码进行优化


压缩的基本原理

去除冗余信息

  • 重复数据消除许多文件中存在大量重复出现的数据。例如,在一个文本文件中,如果多次出现相同的单词或短语,压缩算法会识别这些重复部分,并在存储时只记录一次该重复数据,然后通过标记来表示其在文件中的重复出现位置。
    压缩前的数据djfakjfd777777777qeajivod777777777fkjsdorfjvnvdvfla777777777
    压缩过程重复出现的字段为"777777777",因此可以用"~"代替该内容
    压缩后的数据djfakjfd~qeajivod~fkjsdorfjvnvdvfla~

    (ps.上面表格中所展示的是一个非常简化的例子,只是为了方便理解)

  • 相似数据处理对于一些相似但不完全相同的数据,如图像文件中颜色相近的像素区域,压缩算法会利用数学模型来找出它们之间的规律,用更简洁的方式表示这些相似数据。

编码优化

  • 霍夫曼(Huffman)编码这是一种常用的编码方式。它根据字符在文件中出现的频率来分配不同长度的编码。出现频率高的字符被分配较短的编码,而出现频率低的字符则被分配较长的编码。这样可以使文件整体的编码长度更短。例如,在一个特定文件中,字符 “A” 出现的频率非常高,那么它可能被分配一个较短的二进制编码,如 “0”,而其他不常出现的字符则会被分配相对较长的编码。

【详细举例 - Huffman是如何实现优化的】

  • 数据内容:aaaabbbccd
  • 数据所占内存空间大小分析:每个字符占8bit,一共10个字符,即80bit

Huffman编码优化:

  1. 首先统计字符的频率:

    a 出现了 4 次。
    b 出现了 3 次。
    c 出现了 2 次。
    d 出现了 1 次。
  2. 构建哈夫曼树,得到每个字符对应的Huffman编码

  3. 分配编码:

    d:111
    c:110
    b:10
    a:0
  4. 编码结果:

    aaaabbbccd 编码为 0000101010110111 👉 16 bit < 80 bit

Huffman 编码如何实现文件压缩

一、Huffman 算法基础

Huffman 树是一种 带权路径长度最短 的二叉树.

👉带权路径:带权路径是指在树结构中从根节点到某一节点的路径长度与该节点权值的乘积。

既然 Huffman 树是一种 带权路径长度最短 的二叉树. 想要带权路径最短就要让权值大的数处在更短的路径上。接下来讲解如何创建 Huffman 🌳

1. 统计字符频率

  • 首先,对需要压缩的文件进行字符统计。例如,对于一个文本文件,统计文件中每个字符出现的次数。假设文件内容是 “ aaaabbbccd ”,那么字符‘a’出现 4 次,‘b’出现 3 次,‘c’出现 2 次,‘d’出现 1 次。
  • 这些频率信息将作为构建 Huffman 树的基础

2. 构建 Huffman 树

  • 将每个字符及其频率看作一个节点。例如,有节点(a,4)、(b,3)、(c,2)、(d,1)。
  • 从这些节点中选取频率最小的两个节点,创建一个新的父节点,其频率为这两个子节点频率之和。比如先选取(d,1)和(c,2),创建一个新节点(新节点_1,3)。
  • 重复这个过程,直到所有节点都合并成一棵树。接着刚才的例子,再将(新节点_1,3)和(a,4)合并为(新节点_2,7),最后将(新节点_2,7)和(b,3)合并为根节点(根,10)。

二、生成 Huffman 编码

从根节点开始编码

  • 从构建好的 Huffman 树的根节点开始,向左分支标记为 0,向右分支标记为 1(即左1右0,或者相反)。
  • 对于每个叶子节点(即原始字符节点),从根节点到该叶子节点的路径上的 0 和 1 组成的二进制串就是该字符的 Huffman 编码。

Huffman🌳是压缩和解压缩的关键,而围绕Huffman🌳有两个重点。①构建Huffman🌳需要什么;②Huffman🌳可以得到什么
--->即把一个关键转化为两个方面:①字符出现的频次信息;②字符对应的Huffman编码


文件压缩
读取要压缩的文件 👉 获取构建Huffman树的必要信息 👉 成功构建Huffman树 👉 获取字符对应的Huffman编码 👉 替换字符编码 👉 存储编码后的文件


文件解压缩

读取要解压缩的文件 👉 读取构建huffm树的必要信息 👉 成功构建Huffman树 👉 获取字符对应的Huffman编码 👉 替换编码字符 👉 存储解码后的文件

【class HuffmanTree🌳】

可以看到文件压缩和解压缩的准备动作都和Huffman🌳有关,因此有必要封装一个Huffman的类来实现这些统一的动作。接下来我们将实现一个HuffmanTree类。

首先。老习惯,先来理清框架

类之间的依赖关系
class HuffmanTree                 》》----------depend on---------》》                 class HuffmanNode
class HuffmanNode
TypeVariable Name
待实例化(W)_weight(权重值)
pointer->HuffmanNode(指针指向的变量类型)_pleft
pointer->HuffmanNode(指针指向的变量类型)_pright

实现的重点在于 👉 根据给定的数据创建huffman树 👉 实现HuffmanTree的构造函数

构造思路:1. 总是先要选出最小的两个值 → 则把这些数据放在一个 小堆 中存储,这里用到了优先级队列。
                  2. 按 Huffman🌳的构造流程一步步实现即可
                  ❓------这里我遇到一个问题:优先级队列中是应该存储 指针 还是 结点值 呢?(如图)

解决方式:最后还是认为存储指针更方便,对于比较大小的问题,可以用仿函数来控制。

(💡提醒:一开始用了 lambda 表达式 来进行大小比较的控制 👉 priority_queue<pNode, vector<pNode>, lambda 表达式> HuffmanForest  但是,lambda 是一种匿名函数对象!是不能作为一个模板参数的!模板参数只能传递类型!)

示例代码: 

#pragma once#include<iostream>
#include<vector>
#include<queue>
using std::vector;
using std::priority_queue;template<class W>
struct HuffmanNode
{HuffmanNode(const W& weight = W()):_weight(weight), _pleft(nullptr), _pright(nullptr){}W _weight;HuffmanNode<W>* _pleft;HuffmanNode<W>* _pright;
};template<class W>
class HuffmanTree
{typedef HuffmanNode<W>* pNode;typedef HuffmanNode<W> Node;struct _greater{bool operator()(pNode left, pNode right){return left->_weight > right->_weight;}};public:HuffmanTree():_proot(nullptr){}HuffmanTree(const vector<W>& DataSource){priority_queue<pNode, vector<pNode>, _greater> HuffmanForest;for (auto e : DataSource){pNode pNewNode = new Node(e);HuffmanForest.push(pNewNode);}while (!HuffmanForest.empty()){pNode pleft = HuffmanForest.top();HuffmanForest.pop();pNode pright = HuffmanForest.top();HuffmanForest.pop();pNode pNewNode = new Node(pleft->_weight + pright->_weight);pNewNode->_pleft = pleft;pleft->_pparent = pNewNode;pNewNode->_pright = pright;pright->_pparent = pNewNode;if (HuffmanForest.empty()){_proot = pNewNode;break;}HuffmanForest.push(pNewNode);}}pNode GetProot(){return _proot;}~HuffmanTree(){Destroy(_proot);}private:void Destroy(pNode& proot){if (proot == nullptr)return;Destroy(proot->_pleft);Destroy(proot->_pright);delete proot;proot = nullptr;}pNode _proot = nullptr;
};

三、文件压缩过程

封装一个类,实现文件压缩和解压缩的方法。

根据上述分析可知,文件压缩要通过Huffman🌳实现,而Huffman🌳构建的关键在于提供的频次频次信息。
👉因此,频次信息就是文件压缩与Huffman🌳之间的桥梁
👉因此,需要一个存储频次信息的变量。💡频次信息不单单只有频率这么简单:字符 + 出现次数 + 对应Huffman编码  这三项都是围绕Huffman树的重要数据

根据上面的分析,要把 字符 + 出现次数 + 对应Huffman编码 封装起来。

先来理清框架

类之间的依赖关系
class FileCompress                 》》----------depend on---------》》                 class DataINFO
class DataINFO
TypeVariable Name
unsigned char_ch(字符)
size_t_frequency(出现频率/次数)
string_code(对应Huffman编码)

 对应代码:

#pragma once
#include"HuffmanTree.h"
#include<string>
using std::string;struct DataINFO
{DataINFO(char ch = 0, size_t frequency = 0, const string& code = ""):_ch(ch), _frequency(frequency), _code(code){}unsigned char _ch;size_t _frequency;string _code;
};class FileCompress
{vector<DataINFO> fileDate;public:FileCompress();void Compress(const string& FilePath); // 对给定的FilePath的文件进行压缩操作,并生成压缩文件void UnCompress(const string& FilePath);// 对给定的FilePath的文件进行解压缩操作,并生成解压缩文件
};

class FileCompress 初始化:

将变量 fileDate 的下标值看成ASCII码值 👉 下标即字符(这也是为什么要用 unsigned char 而不是 char 的原因)。同时,下标对应的位置存储的数据代表该字符的出现次数。

 

FileCompress::FileCompress()
{fileDate.resize(256);for (size_t i = 0; i < 256; ++i){fileDate[i]._ch = i;}
}

1. 替换字符为编码

  • 对文件中的每个字符,用其对应的 Huffman 编码进行替换。
  • 由于 Huffman 编码是变长编码,而且出现频率高的字符编码较短,所以整体编码后的二进制串长度通常比原始文件的二进制表示长度要短。

2. 存储编码后的文件

  • 将编码后的二进制串存储为压缩文件。同时,为了能够正确解压缩文件,还需要存储 Huffman 树的信息(可以在文件开头或者其他特定位置存储),以便在解压缩时重建 Huffman 树并还原字符编码。

四、解压缩过程

1. 读取 Huffman 树信息

  • 当解压缩文件时,首先读取存储的 Huffman 树信息,重建 Huffman 树。

2. 解码二进制串

  • 从压缩文件中读取编码后的二进制串,从 Huffman 树的根节点开始,根据二进制串中的 0 和 1 沿着树的分支向下走,直到到达叶子节点,叶子节点对应的字符就是解码出的字符。
  • 例如,遇到编码 00,就沿着根节点的左分支再左分支找到叶子节点‘d’,以此类推,将整个二进制串解码还原出原始文件内容。

以上是对Huffman算法的讲解和实现,以及整体框架的搭建和梳理。下篇文章将详细讲解文件压缩和解压缩的过程。

END

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

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

相关文章

MoveIt控制机械臂的运动实现——机器人抓取系统基础系列(二)

文章目录 概要1 用户接口和代码案例2 不同的规划类型2.1 关节空间规划2.2 工作空间规划2.3 笛卡尔空间规划 3 MoveIt运行实操4 相关资料推荐小结 概要 MoveIt为开发者提供了针对机械臂的集成化开发平台&#xff0c;由一系列操作相关的功能包组成&#xff0c;包括运动规划、操作…

从 Affine Particle-In-Cell (APIC) 到 Material Point Method (MPM 物质点法)

APIC与MPM Particle-In-Cell (PIC)Affine Particle-In-Cell (APIC)Material Point Method (MPM)关于边界投影等额外操作 Material Point Method (MPM 物质点法)是一种混合欧拉-拉格朗日视角物理仿真方法。 欧拉视角即网格视角&#xff0c;将空间划分为网格&#xff0c;通过表示…

从一到无穷大 #35 Velox Parquet Reader 能力边界

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 本作品 (李兆龙 博文, 由 李兆龙 创作)&#xff0c;由 李兆龙 确认&#xff0c;转载请注明版权。 文章目录 引言源码分析功能描述功能展望 引言 InfluxDB IOX这样完全不使用索引&#xff0c;只…

JavaEE: 深入探索TCP网络编程的奇妙世界(四)

文章目录 TCP核心机制TCP核心机制四: 滑动窗口为啥要使用滑动窗口?滑动窗口介绍滑动窗口出现丢包咋办? TCP核心机制五: 流量控制 TCP核心机制 书接上文~ TCP核心机制四: 滑动窗口 为啥要使用滑动窗口? 之前我们讨论了确认应答策略,对每一个发送的数据段,都要给一个ACK确…

centos7下openssh升级方法(编译安装)

注意&#xff1a; 首先打开两个或以上的shell连接&#xff0c;因为在升级过程中如果升级失败会导致不发新建shell连接&#xff1b;升级后使用xshell6,7连接&#xff0c;openssh版本对应修改&#xff0c;下载地址&#xff1a; https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/por…

Servlet day2(概念理解)

Servlet体系结构 Servlet相关配置 HTTP协议内容

leetcode746. 使用最小花费爬楼梯,动态规划

leetcode746. 使用最小花费爬楼梯 给你一个整数数组 cost &#xff0c;其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用&#xff0c;即可选择向上爬一个或者两个台阶。 你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。 请你计算并返回达到楼梯顶…

计算机毕业设计 基于SpringBoot的小区运动中心预约管理系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

Java笔试面试题AI答之设计模式(4)

文章目录 16. 简述什么是观察者模式&#xff1f;基本概念主要特点实现方式应用场景优缺点 17. 请列举观察者模式应用场景 &#xff1f;18. 请用Java代码实现观察者模式的案例 &#xff1f;19. 什么是装饰模式&#xff1f;定义与特点结构与角色工作原理优点应用场景示例 20. 请用…

基于大数据的电子产品需求数据分析系统的设计与实现(Python Vue Flask Mysql)

&#x1f497;博主介绍&#x1f497;&#xff1a;✌在职Java研发工程师、专注于程序设计、源码分享、技术交流、专注于Java技术领域和毕业设计✌ 温馨提示&#xff1a;文末有 CSDN 平台官方提供的老师 Wechat / QQ 名片 :) Java精品实战案例《700套》 2025最新毕业设计选题推荐…

【GlobalMapper精品教程】088:按点线面空间位置选择案例

按点线面空间位置选择的原则为:点线面的排列组合。 文章目录 一、选择线要素附近的点二、选择相交或触碰所选线的区和线三、选择包含点的区要素四、选择选定区域内的点要素一、选择线要素附近的点 启动该工具之前,首先要选择线,例如,选择某一段铁路5km范围之内的县城驻地。…

DeepSeek 2.5本地部署的实战教程

大家好,我是herosunly。985院校硕士毕业,现担任算法研究员一职,热衷于大模型算法的研究与应用。曾担任百度千帆大模型比赛、BPAA算法大赛评委,编写微软OpenAI考试认证指导手册。曾获得阿里云天池比赛第一名,CCF比赛第二名,科大讯飞比赛第三名。授权多项发明专利。对机器学…

[Meachines] [Medium] Sniper RFI包含远程SMB+ powershell用户横向+CHM武器化权限提升

信息收集 IP AddressOpening Ports10.10.10.151TCP:80,135,139,445,49667 $ nmap -p- 10.10.10.151 --min-rate 1000 -sC -sV -Pn PORT STATE SERVICE VERSION 80/tcp open http Microsoft IIS httpd 10.0 |_http-server-header: Microsoft-IIS/10.…

三阶魔方还原法 勾上回下 上右左左右

三阶魔方还原法&#xff1a; 1小白花 &#xff08;转3换1&#xff09; 2白十字架 (侧与中心同色 下下) 3第一层 &#xff08;找位置角块放顶点 勾上回下&#xff09; 4 第二层 &#xff08;颜色边 勾上回下 再单白边 勾上回下&#xff09; 5 黄十字架 &#xff08;无黄边 压 勾…

0.设计模式总览——设计模式入门系列

在现代软件开发中&#xff0c;设计模式为我们提供了优秀的解决方案&#xff0c;帮助我们更好地组织代码和架构。本系列专栏将对设计模式的基本思想、原则&#xff0c;以及常用的分类、实现方式&#xff0c;案例对比、以及使用建议&#xff0c;旨在提高开发者对设计模式的理解和…

【算法】BFS系列之 拓扑排序

【ps】本篇有 3 道 leetcode OJ。 目录 一、算法简介 二、相关例题 1&#xff09;课程表 .1- 题目解析 .2- 代码编写 2&#xff09;课程表 II .1- 题目解析 .2- 代码编写 3&#xff09;火星词典 .1- 题目解析 .2- 代码编写 一、算法简介 【补】图的基本概念 &#…

HTML翻牌器:用CSS和HTML元素创造动态数字展示

HTML翻牌器&#xff1a;用CSS和HTML元素创造动态数字展示 前言 翻牌器是一种数字动态展示形式&#xff0c;在生活中常见的例如翻牌计分、翻牌时钟等。 之所以以翻牌的形式是因为其物理设计的原因使其只能滚动翻牌展示数字&#xff0c;在电子显示设备不普及时&#xff0c;使用…

Leetcode - 139双周赛

目录 一&#xff0c;3285. 找到稳定山的下标 二&#xff0c;3286. 穿越网格图的安全路径 三&#xff0c;3287. 求出数组中最大序列值 四&#xff0c;3288. 最长上升路径的长度 一&#xff0c;3285. 找到稳定山的下标 本题就是找[0&#xff0c; n-2]中&#xff0c;height[i]…

C++入门12——详解多态2

上篇文章&#xff08;C入门12——详解多态1&#xff09;中&#xff0c;我们介绍了C多态的概念和用法&#xff0c;但是只知其然而不知其所以然是万万不行的&#xff0c;所以本篇文章将从探案的角度详细介绍多态的原理。 1. 虚函数表 想要弄懂多态的原理&#xff0c;首先要了解一…

数据结构与算法学习day22-回溯算法-分割回文串、复原IP地址、子集

一、分割回文串 1.题目 131. 分割回文串 - 力扣&#xff08;LeetCode&#xff09; 2.思路 分割回文串可以抽象为一棵树形结构。 递归用来纵向遍历&#xff0c;for循环用来横向遍历&#xff0c;切割线&#xff08;就是图中的红线&#xff09;切割到字符串的结尾位置&#xf…