问题拆解:设计一个基于哈夫曼编码的解压缩软件,这个问题我认为可以分解为以下几个子问题:
根据这几个子问题的思路顺序,我们逐个击破,寻找解决方案:
QFile
文件流配以QDatastram
辅助。因为,C++的文件流fstream
无法识别中文路径(一说起这个,就想起Debug时的辛酸)。权重统计我用的是map
记录:mapWeightmap
。struct
里面加了unsigned char
型的字符,int
型的权重,string
型的哈夫曼编码,和是否是叶子结点的bool
型标记(哈夫曼树叶子结点才是我们需要的编码)。将前面我们获取到的字符和权重加进去。vector
内(理论上什么容器都可以),进入循环:根据哈夫曼结点的权重比较进行排序(直接调用sort
),每次将连个最小的结点权重取出,相加得到权重和,以权重和建立一个新的结点,新结点的左右孩子结点就是这两个结点,将新结点加入到vector
中,那两个结点删除。循环结束条件为vector
内只剩下一个结点。这个结点便是哈夫曼树的根结点,保存一根结点足矣。string+“0”
,右子树的string+“1”
。并将unsigned char
型的字符与这个string
用map PasswordMap
记录。PasswordMap.size()
,循环传入PasswordMap->first
(字符),PasswordMap->second.size()
(记录编码长度),PasswordMap->second
(这个字符串有可能超过8位,每个字节不足8位的部分补0)直到完全传入。另外一个方法是将所有字符与字符权重传入,在解压缩时再次构建哈夫曼树,两种思路我觉得都可以。这里我采用的第一种方法。Map
映射,根据这个映射,将原文件还原。前面分析得很清晰,不过写代码的过程异常艰辛,充满了Debug时的汗水和泪水。
完整代码与封装好的可执行文件下载链接:https://gitee.com/sherlocknovitch/Qt_Compression
(可执行文件有58M,是因为封装了Qt的核心控件,去掉这些估计只有几百Kb的大小)
上代码:
先从窗口头文件看起,这块没有什么实质性的内容,只有几个槽函数(3个按钮的Click
槽函数加一个让QprogressBar
动起来的槽函数和一个出现文件打开错误情况时让QLineEdit
清空的槽函数)
私有成员中QStirng path
的作用是读取文件路径,作为压缩函数和解压缩函数的参数。
Compression* com
是一个类指针,这个类是压缩类,在后面会提及。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include"compression.h"
#include
#include
#include
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
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
这一块主要含义见注释即可,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的最大值设为100
ui->progressBar->setMinimum(0);
//QProgressBar的最小值设为0
ui->progressBar->setValue(0);
//QProgressBar显示的值初始化为0
QMenu* 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,progressBar
void MainWindow::on_pushButton_decompression_clicked()
{
com->unzip(path);
ui->lineEdit->clear();
this->path.clear();
ui->progressBar->setValue(0);
}
// UI界面“解压”按钮的槽函数,思想与前者相同
在这一块定义哈夫曼树节点,定义了压缩类,具体见注释
#ifndef COMPRESSION_H
#define COMPRESSION_H
#include
#include
#include
#include
#include
#include
进入重头戏。
该函数将二进制串转为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();
//转换为我们亲切的string
for(int i=0;i
建立哈夫曼结点函数:进行初始化操作
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::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); //创建QFile
if(!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(); //关闭openfile
QFile 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<::iterator it = passwordmap.begin(); it != passwordmap.end(); it++)
{
emit mysignal(50+double(25*k++)/passwordmap.size());
int first = it->first;
out<second;
int size = second.size();
out<
解压函数:思路同压缩函数类似
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::iterator it = zippassword.find(str);
if (it != zippassword.end())
{
unsigned char temp=it->second;
savefile.write(reinterpret_cast(&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最优。
我认为一些文件压缩效率较差的原因为: