Qt实现哈夫曼编码解压缩软件详解
目录
- 一、概要设计
- 二:设计效果展示:
- 三、源代码
- 1°MainWindow.h
- 2°MainWindow.cpp
- 3°Compression.h
- 4°Compression.cpp
- 四、软件分析
诸位既然点开了本帖,相信对此问题已有初步了解,哈夫曼树的原理不再赘述,我们开门见山,直入主题。
一、概要设计
问题拆解:设计一个基于哈夫曼编码的解压缩软件,这个问题我认为可以分解为以下几个子问题:
- 读取传入文件,进行字符权重统计
- 将出现的字符放入哈夫曼树结点,构建哈夫曼树,获取哈夫曼编码
- 将编码相关信息写入压缩后的文件,再将传入文件的每个字符按照哈夫曼编码转换,每8个二进制位作为一个字节传入压缩后的文件
- 解压部分:将传入的已压缩文件进行文件流读取,获取编码信息进行还原
根据这几个子问题的思路顺序,我们逐个击破,寻找解决方案:
- 读取传入文件,需要用到文件流。这里推荐使用Qt自带的
QFile
文件流配以QDatastram
辅助。因为,C++的文件流fstream
无法识别中文路径(一说起这个,就想起Debug时的辛酸)。权重统计我用的是map
记录:map<unsigned char ,int>Weightmap
。 - 哈夫曼结点的构造,和二叉树没有什么区别,只是在封装的
struct
里面加了unsigned char
型的字符,int
型的权重,string
型的哈夫曼编码,和是否是叶子结点的bool
型标记(哈夫曼树叶子结点才是我们需要的编码)。将前面我们获取到的字符和权重加进去。
我们将建立好的结点放入到一个vector
内(理论上什么容器都可以),进入循环:根据哈夫曼结点的权重比较进行排序(直接调用sort
),每次将连个最小的结点权重取出,相加得到权重和,以权重和建立一个新的结点,新结点的左右孩子结点就是这两个结点,将新结点加入到vector
中,那两个结点删除。循环结束条件为vector
内只剩下一个结点。这个结点便是哈夫曼树的根结点,保存一根结点足矣。
我们从根结点出发遍历,左子树的string+“0”
,右子树的string+“1”
。并将unsigned char
型的字符与这个string
用map<unsigned char,string> PasswordMap
记录。 - 传入辅助信息阶段:我们传入的信息有,
PasswordMap.size()
,循环传入PasswordMap->first
(字符),PasswordMap->second.size()
(记录编码长度),PasswordMap->second
(这个字符串有可能超过8位,每个字节不足8位的部分补0)直到完全传入。另外一个方法是将所有字符与字符权重传入,在解压缩时再次构建哈夫曼树,两种思路我觉得都可以。这里我采用的第一种方法。
传入哈夫曼编码阶段:遍历先前在读取文件时获取的字符串,将字符串的每一位转化为哈夫曼编码,用另一个字符串储存。这里不妨称这个字符串为二进制字符串,这个二进制字符串按8分割到最后可能会有不足8位的部分,对其进行补0操作。 最后再传入一个字节,记录补0数。 - 解压部分:对应上部分的操作进行对应读取,主要思路是将压缩时传入的辅助信息提取,建立一个新的
Map
映射,根据这个映射,将原文件还原。
二:设计效果展示:
压缩前:
压缩中:
压缩后:
解压中:
解压后:
三、源代码
前面分析得很清晰,不过写代码的过程异常艰辛,充满了Debug时的汗水和泪水。
完整代码与封装好的可执行文件下载链接:https://gitee.com/sherlocknovitch/Qt_Compression
(可执行文件有58M,是因为封装了Qt的核心控件,去掉这些估计只有几百Kb的大小)
上代码:
1°MainWindow.h
先从窗口头文件看起,这块没有什么实质性的内容,只有几个槽函数(3个按钮的Click
槽函数加一个让QprogressBar
动起来的槽函数和一个出现文件打开错误情况时让QLineEdit
清空的槽函数)
私有成员中QStirng path
的作用是读取文件路径,作为压缩函数和解压缩函数的参数。
Compression* com
是一个类指针,这个类是压缩类,在后面会提及。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>
#include"compression.h"
#include<QFileDialog>
#include<QMenu>
#include<QAction>namespace Ui {
class MainWindow;
}class MainWindow : public QMainWindow
{Q_OBJECTpublic:explicit MainWindow(QWidget *parent = 0);~MainWindow();void clear();
private slots:void on_pushButton_open_clicked();void on_pushButton_compression_clicked();void on_pushButton_decompression_clicked();void myslot(double per);private:Ui::MainWindow *ui;Compression* com;QString path;
};#endif // MAINWINDOW_H
2°MainWindow.cpp
这一块主要含义见注释即可,QProgressBar
是Qt的一个控件,可以理解为进度条
#include "mainwindow.h"
#include "ui_mainwindow.h"MainWindow::MainWindow(QWidget *parent) :QMainWindow(parent),ui(new Ui::MainWindow)
{ui->setupUi(this);resize(600,600);//设置窗口大小setWindowTitle("Sherlock's Compression");/设置窗口名称com=new Compression();//初始化压缩类指针connect(com,&Compression::error,this,&MainWindow::clear);//异常情况的信号接收connect(com,SIGNAL(mysignal(double)),this,SLOT(myslot(double)));//让QProgressBar随着压缩/解压缩进度更新ui->progressBar->setMaximum(100);//QProgressBar的最大值设为100ui->progressBar->setMinimum(0);//QProgressBar的最小值设为0ui->progressBar->setValue(0);//QProgressBar显示的值初始化为0QMenu* pFile=ui->menuBar->addMenu("帮助");//添加菜单栏QAction* pNew=pFile->addAction("关于此压缩软件");//添加菜单栏下的一个动作connect(pNew,&QAction::triggered,[=](){QMessageBox::about(this,"关于此压缩软件","""此乃数据结构大作业,余制之于庚子年辛巳月丙子日。该压缩软件可能存在一些Bug(运行过程中可能会卡顿,不要点击,否则可能强退),时间原因无法全面排查,如您在使用过程有所发现,欢迎反馈。");//点击时弹出的窗口});
}MainWindow::~MainWindow()
{delete ui;delete com;
}void MainWindow::clear()
{ui->lineEdit->clear();path.clear();
}
//在文件打开失败的情况下清空所有void MainWindow::myslot(double per)
{if(per>ui->progressBar->value())ui->progressBar->setValue(per);
}
//per是进度,信号是由ComPreesion类指针发出,这是槽函数void MainWindow::on_pushButton_open_clicked()
{path=QFileDialog::getOpenFileName(this,QString("选择文件"));ui->lineEdit->setText(path);
}
//UI界面“选择”按钮的槽函数,更新path和lineEdit内容void MainWindow::on_pushButton_compression_clicked()
{com->zip(path);ui->lineEdit->clear();this->path.clear();ui->progressBar->setValue(0);
}
//UI界面“压缩”按钮的槽函数,将path作为参数传给压缩类的压缩函数,压缩完毕后,清除lineEdit,path,progressBarvoid MainWindow::on_pushButton_decompression_clicked()
{com->unzip(path);ui->lineEdit->clear();this->path.clear();ui->progressBar->setValue(0);
}
// UI界面“解压”按钮的槽函数,思想与前者相同
3°Compression.h
在这一块定义哈夫曼树节点,定义了压缩类,具体见注释
#ifndef COMPRESSION_H
#define COMPRESSION_H#include <QMainWindow>
#include<QMessageBox>
#include<QFile>
#include<QDataStream>
#include<QString>
#include<map>
#include<string>
#include<vector>
#include<algorithm>
#include<ctime>using namespace std;struct HuffmanTreeNode
{pair<unsigned char, int>Weight; //记录字符和其权重string zippassword; //等待获取的哈夫曼编码HuffmanTreeNode* leftson; //左儿子指针HuffmanTreeNode* rightson; //右儿子指针bool tag; //是否为叶子结点的标记,true为非叶子结点
};
//哈夫曼树结点class Compression : public QMainWindow
{Q_OBJECT
public:explicit Compression(QWidget *parent = nullptr);void zip(QString path); //压缩函数void unzip(QString path); //解压函数
protected:void DEL(HuffmanTreeNode*& root); //在每次压缩后需要delete掉申请空间的哈夫曼树指针,采用递归遍历进行deletevoid BinaryString_Init();//二进制字符串的初始化,即从传入文件得到的字符串按照哈夫曼编码获取二进制字符串void Weightmap_Init(QFile& in);//参数是QFile型,是Qt的文件操作,该函数打开传入文件,遍历,形成一个字符串,并且初始化WeightMapHuffmanTreeNode* MakehuffmanTreeNode(int i);//该函数的功能是创建单个哈夫曼树结点void HuffmanTreeVector_Init();//将创建的哈夫曼结点放入Vector中void HuffmanTree_Init();//建立哈夫曼树的函数void ZipPassword_Init(HuffmanTreeNode* &root, string& password);//对建成的哈夫曼树进行遍历,更新每个结点的zippassword,即哈夫曼编码private:map<unsigned char, int> weightmap; //权重映射map<unsigned char, string>passwordmap; //哈夫曼编码映射string source_string; //读取传入文件形成的字符串string binary_string; //通过哈夫曼编码映射形成的二进制字符串vector<HuffmanTreeNode* > container; //哈夫曼结点储存的容器map<string, int> zippassword; //解压时建立的解码映射
signals:void error(); //信号void mysignal(double per); //信号
public slots:
};#endif // COMPRESSION_H
4°Compression.cpp
进入重头戏。
该函数将二进制串转为int型,即将压缩时每8个二进制位的字符串转化为ASCII码,传入压缩后的文件
int binarystringtoint(string binarystring)
{int sum = 0;for (int i = 0; i < binarystring.size(); i++){if (binarystring[i] == '1'){int j = pow(2, binarystring.size() - i - 1);sum += j;}}return sum;
}
int转化为string,是上面那个函数的反操作,在解压时使用
string inttobinarystring(int value)
{string binarystring;while (value > 0){int r = value % 2;if (r == 1)binarystring.insert(0, 1, '1');else binarystring.insert(0, 1, '0');value = value / 2;}if (binarystring.size() < 8){binarystring.insert(0, 8 - binarystring.size(), '0');}return binarystring;
}
这个函数是哈夫曼结点间比较的依据,作为sort
函数的参数
bool compare(HuffmanTreeNode* node1, HuffmanTreeNode* node2)
{return node1->Weight.second < node2->Weight.second;
}
这是删除整颗哈夫曼树的函数
void Compression::DEL(HuffmanTreeNode*& root)
{if(root==NULL) return;DEL(root->leftson);DEL(root->rightson);delete root;
}
此函数功能为sourcestring
按照哈夫曼编码映射初始化类中的binarystring
void Compression:: BinaryString_Init()
{for (int i = 0; i < source_string.size(); i++){binary_string+= passwordmap[source_string[i]];}
}
权重映射的初始化,涉及到QFile
的操作,实际上和C++的fstream
差不多,不过功能比其强大.
void Compression::Weightmap_Init(QFile& in)
{QByteArray a;while (!in.atEnd()){a=in.read(1024); //一次性读取1024个字节,不足1024个字节则读取全部string b=a.toStdString();//转换为我们亲切的stringfor(int i=0;i<b.size();i++){unsigned char ch=b[i];source_string += ch;weightmap[ch]++;}}
}
建立哈夫曼结点函数:进行初始化操作
HuffmanTreeNode* Compression::MakehuffmanTreeNode(int i)
{HuffmanTreeNode* huffman = new HuffmanTreeNode;huffman->Weight.first = i;huffman->Weight.second = weightmap[i];huffman->tag = 0;huffman->leftson = NULL;huffman->rightson = NULL;return huffman;
}
初始化结点容器:
void Compression::HuffmanTreeVector_Init(){for (map<unsigned char, int>::iterator it = weightmap.begin(); it != weightmap.end(); it++){HuffmanTreeNode* huffman = MakehuffmanTreeNode(it->first);container.push_back(huffman);}}
建立哈夫曼树的函数,基本上就是原理的实现
void Compression::HuffmanTree_Init(){while (container.size() != 1){sort(container.begin(), container.end(), compare);int sum = container[0]->Weight.second + container[1]->Weight.second;HuffmanTreeNode* newhuffman = new HuffmanTreeNode;newhuffman->Weight.second = sum;newhuffman->Weight.first = 0;newhuffman->tag = 1;//标记为非叶子结点newhuffman->leftson = container[0];newhuffman->rightson = container[1];container.erase(container.begin());//删除后迭代器会顺移到下一位container.erase(container.begin());container.push_back(newhuffman);}}
递归更新哈夫曼编码函数:这个函数的关键是每次递归返回时password
进行一次pop
操作。
void Compression::ZipPassword_Init(HuffmanTreeNode* &root, string& password){if (root != NULL && !root->tag){root->zippassword = password;passwordmap[root->Weight.first] = password;}if (root->leftson != NULL){ZipPassword_Init(root->leftson, password += "0");password.pop_back();}if (root->rightson != NULL){ZipPassword_Init(root->rightson, password+="1");password.pop_back();}}
压缩函数:
void Compression::zip(QString path){clock_t begin=clock(); //记录开始时间QFile openfile(path); //创建QFileif(!openfile.open(QIODevice::ReadOnly)) //打开文件,若不成功,发射信号,结束。{QMessageBox::information(NULL,QString("警告"),QString("文件打开失败"));emit error();return;}Weightmap_Init(openfile); //权重映射初始化emit mysignal(10); //更新进度条HuffmanTreeVector_Init(); //哈夫曼结点容器初始化emit mysignal(20);HuffmanTree_Init(); //构建哈夫曼树emit mysignal(30); string empty=""; ZipPassword_Init(container[0],empty); //哈夫曼编码映射初始化emit mysignal(40);BinaryString_Init(); //获取二进制串emit mysignal(50);path+=".HuffmanZip"; //压缩后的文件格式后缀openfile.close(); //关闭openfileQFile savefile(path); //创建新的Qfile进行压缩文件的写入savefile.open(QIODevice::WriteOnly); //打开QDataStream out(&savefile); //利用QdataStream进行接下来的操作,这就类似于fstream了int size = passwordmap.size();if (size == 256) size = 0; //因为256无法用一个字节保存int length = 0; //这个length没用的,忘记删除了out<<size; //传入哈夫曼编码映射数量length++;double k=1;for (map<unsigned char, string>::iterator it = passwordmap.begin(); it != passwordmap.end(); it++){emit mysignal(50+double(25*k++)/passwordmap.size());int first = it->first;out<<first; //传入key值length++;string second = it->second;int size = second.size();out<<size; //传入value值的长度length++;int n = 8 - second.size() % 8;if (n){second.append(n, '0'); //补0}for (int i = 0; i < second.size(); i += 8){string k = second.substr(i, 8);int temp = binarystringtoint(k);unsigned char ch = temp;out<<ch; //传入value值length++;}}int n = 8 - binary_string.size() % 8;if (n){binary_string.append(n, '0');}length++;int totalbitsize = binary_string.size() / 8;for (int i = 0; i < binary_string.size(); i += 8){emit mysignal(75+double(25*i)/binary_string.size());string k = binary_string.substr(i, 8);int temp = binarystringtoint(k);unsigned char ch = temp;out<<ch; //分8个字节依次传入length++;}unsigned char temp=n;out<<temp; //传入补0数量length++;emit mysignal(100);int newlength=savefile.size();savefile.close();clock_t end=clock(); //结束时间QString tip("理论压缩比:");tip+=QString::number(double(totalbitsize * 100) / source_string.size());tip+="%,实际压缩比:";tip+=QString::number(double(newlength * 100) / source_string.size());tip+="%,压缩用时:";tip+=QString::number(double(end-begin)/CLOCKS_PER_SEC);tip+="s";QMessageBox::about(this,"压缩说明",tip);//弹窗提示本次压缩情况weightmap.clear();passwordmap.clear();source_string.clear();binary_string.clear();DEL(container[0]);container.clear();//结束后的处理}
解压函数:思路同压缩函数类似
void Compression::unzip(QString path){clock_t begin=clock();if(path.right(11)!=".HuffmanZip"){QMessageBox::information(NULL,QString("警告"),QString("此文件非哈夫曼压缩文件,打开失败"));emit error();return;}QFile openfile(path);if(!openfile.open(QIODevice::ReadOnly)){QMessageBox::information(NULL,QString("警告"),QString("文件打开失败"));emit error();return;}QDataStream in(&openfile);int zipmapsize;in>>zipmapsize;if (zipmapsize == 0) zipmapsize = 256;for (int i = 1; i <= zipmapsize; i++){int zipkey;in>>zipkey;int valuelength;in>>valuelength;string valuestring;for (int i = 1; i <= valuelength / 8 + 1; i++){emit mysignal(double(20*i)/(valuelength/8 +1));unsigned char ch;in>>ch;int zipvalue=ch;valuestring += inttobinarystring(zipvalue);}valuestring.erase(valuelength, valuestring.size() - valuelength + 1);zippassword[valuestring] = zipkey;}string zipstring;while (!in.atEnd()){unsigned char ch;in>>ch;int bit=ch;zipstring += inttobinarystring(bit);}emit mysignal(50);int zerosize = binarystringtoint(zipstring.substr(zipstring.size() - 8));zipstring.erase(zipstring.size() - zerosize - 8, zerosize + 8);openfile.close();path.chop(11);path.insert(path.lastIndexOf('.'),"(New)");QFile savefile(path);savefile.open(QIODevice::WriteOnly);string str;for (int i = 0; i < zipstring.size(); i++){emit mysignal(50+double(50*i)/zipstring.size());str += zipstring[i];map<string, int>::iterator it = zippassword.find(str);if (it != zippassword.end()){unsigned char temp=it->second;savefile.write(reinterpret_cast<char*>(&temp),1);str.clear();}}emit mysignal(100);savefile.close();clock_t end=clock();QString tip="解压用时:";tip+=QString::number(double(end-begin)/CLOCKS_PER_SEC);tip+="s";QMessageBox::about(this,"解压说明",tip);zippassword.clear();}
感谢您把本弱鸡的代码看完了,如果方便的话,可以点个赞吗
四、软件分析
测试了各种类型的文件,每种文件压缩效率是不同的。
文件类型 | doc | docx | txt | cpp | py | exe | xlsx | png | bmp | jpg | gif | ppt | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
文件大小 | 6.26M | 124KB | 432KB | 679KB | 6.43KB | 5.01KB | 73.5KB | 12.3KB | 41.9KB | 308KB | 29KB | 34.3KB | 2.45M |
压缩后大小 | 6.26M | 98.8KB | 435KB | 493KB | 5.25KB | 4.43KB | 59KB | 14.4KB | 44KB | 170KB | 31.4KB | 36.7KB | 2.23M |
理论压缩比 | 99.9085% | 77.4442% | 100% | 72.5954% | 66.2418% | 62.028% | 76.9279% | 97.5236% | 99.1657% | 54.4023% | 99.6274% | 99.7214% | 90.794% |
实际压缩比 | 99.9474% | 79.431% | 100.579% | 72.6815% | 81.6568% | 88.4196% | 80.296% | 117.747% | 105.122% | 55.2083% | 108.188% | 106.985% | 90.893% |
压缩时间 | 4.683s | 0.093s | 0.375s | 0.457s | 0.014s | 0.015s | 0.068s | 0.025s | 0.042s | 0.178s | 0.036s | 0.039s | 2.114s |
解压时间 | 19.647s | 0.273s | 1.295s | 1.297s | 0.015s | 0.009s | 0.2s | 0.044s | 0.141s | 0.471s | 0.096s | 0.105s | 7.224s |
从这个表格可以得出,哈夫曼编码的压缩比取决于文件类型以及文件大小。doc,bmp,txt,cpp,py,exe文件压缩效果相对较好,其中bmp最优。
我认为一些文件压缩效率较差的原因为:
- 文件规模较小,传入的辅助信息量相对较大
- 文件中每个字符都出现且频率分布集中,导致了哈夫曼编码长度基本上全部趋近于8,即哈夫曼树叶子结点几乎全在最底层,使压缩效率大打折扣。
Qt实现哈夫曼编码解压缩软件详解相关推荐
- Python+Qt 使用哈夫曼编码对文本文件进行压缩,解压缩
from Huffman_Ui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui i ...
- 哈夫曼编码解压缩文件 - Java实现
文章目录 前言 一.文件压缩 二.文件解压 结语 前言 不了解哈夫曼树的可以移步查看我的另一篇博客:哈夫曼树(最优二叉树) 使用哈夫曼编码压缩文件,其实就是将每个字符的哈夫曼编码得到,每8位转为一个字 ...
- JPEG霍夫曼编码教程
转译自:https://www.impulseadventure.com/photo/jpeg-huffman-coding.html 量化后,霍夫曼/熵编码是JPEG压缩文件大小节省的重要因素之一. ...
- 【Java数据结构与算法】第十二章 哈夫曼树和哈夫曼编码
第十二章 哈夫曼树和哈夫曼编码 文章目录 第十二章 哈夫曼树和哈夫曼编码 一.哈夫曼树 1.基本术语 2.构建思路 3.代码实现 三.哈夫曼编码 1.引入 2.介绍 3.代码实现哈夫曼编码综合案例 一 ...
- 【C语言-数据结构与算法】-哈夫曼压缩解压缩-终局-如何做一个自己独有的压缩软件
哈夫曼压缩&解压缩 Ⅰ 前言 Ⅱ 需求分析&主函数带参的应用 A. 需求分析 B. 压缩部分 B. 解压缩部分 Ⅲ 哈夫曼压缩 A. 代码分析 B. 从文件中读取内容生成频度表 C. ...
- [源码和文档分享]C语言实现的基于Huffman哈夫曼编码的数据压缩与解压缩
一.实验题目 用哈夫曼编码实现文件压缩 二.实验目的 了解文件的概念 掌握线性链表的插入.删除等算法 掌握Huffman树的概念及构造方法 掌握二叉树的存储结构及遍历算法 利用Huffman树及Huf ...
- 数据结构编程实践(七)创建哈夫曼树、生成哈夫曼编码、完成图片的压缩与解压缩
一.对图片的压缩与解压缩,涉及以下内容: 1.文件读写 2.创建Huffman树 3.生成Huffman编码 4.压缩图片文件 5 . 解压缩图片文件 二.将项目分成三个小任务,下一任务是在上一任务 ...
- C#,哈夫曼编码(Huffman Code)压缩(Compress )与解压缩(Decompress)算法与源代码
David A. Huffman 哈夫曼编码简史(Huffman code) 1951年,哈夫曼和他在MIT信息论的同学需要选择是完成学期报告还是期末考试.导师Robert M. Fano给他们的学期 ...
- 【C语言->数据结构与算法】->哈夫曼压缩解压缩->第一阶段->哈夫曼编码解码的实现
文章目录 Ⅰ 前言 Ⅱ 代码实现哈夫曼编码&解码 A. 构造哈夫曼树 a. 频度统计 b. 生成哈夫曼树 ① 初始化节点 ② 查找频度最小节点 ③ 哈夫曼树的构造 B. 哈夫曼编码 a. 得到 ...
最新文章
- 从传统到深度学习:浅谈点云分割中的图结构
- BZOJ 1084: [SCOI2005]最大子矩阵
- python知识:numpy.geomspace()
- Log4j详细介绍(七)----日志格式化器Layout
- VMware QueryPerformanceCounter/GetTickCount 悬案
- Hulu直播服务难点解析(二):系统设计与实现
- 【javascript权威指南】类型转换
- Mybatis 高级结果映射 ResultMap Association Collection
- 如何使用 C# 扩展方法
- 2021-10-28嵌入式人工智能
- 清空了回收站怎么找回?你没用过的方法
- StyleCop学习笔记——默认的规则
- 网站服务器和空间大小,网站服务器和空间大小
- 高德地图 API 搜索服务 搜索定位 用不了
- c语言作业 分解质因数,分解质因数(C语言)
- 谷歌浏览器 performance 详解
- 带你走进缓存世界(1):漫谈缓存
- 用python进行列联表卡方检验
- Qt使用 QProcess来检测 ip 设备是否在线(Ping)
- 5800日常操作使用小技巧