目录

  • 一、概要设计
  • 二:设计效果展示:
  • 三、源代码
    • 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型的字符与这个stringmap<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();}

感谢您把本弱鸡的代码看完了,如果方便的话,可以点个赞吗

四、软件分析

测试了各种类型的文件,每种文件压缩效率是不同的。

文件类型 pdf 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实现哈夫曼编码解压缩软件详解相关推荐

  1. Python+Qt 使用哈夫曼编码对文本文件进行压缩,解压缩

    from Huffman_Ui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui i ...

  2. 哈夫曼编码解压缩文件 - Java实现

    文章目录 前言 一.文件压缩 二.文件解压 结语 前言 不了解哈夫曼树的可以移步查看我的另一篇博客:哈夫曼树(最优二叉树) 使用哈夫曼编码压缩文件,其实就是将每个字符的哈夫曼编码得到,每8位转为一个字 ...

  3. JPEG霍夫曼编码教程

    转译自:https://www.impulseadventure.com/photo/jpeg-huffman-coding.html 量化后,霍夫曼/熵编码是JPEG压缩文件大小节省的重要因素之一. ...

  4. 【Java数据结构与算法】第十二章 哈夫曼树和哈夫曼编码

    第十二章 哈夫曼树和哈夫曼编码 文章目录 第十二章 哈夫曼树和哈夫曼编码 一.哈夫曼树 1.基本术语 2.构建思路 3.代码实现 三.哈夫曼编码 1.引入 2.介绍 3.代码实现哈夫曼编码综合案例 一 ...

  5. 【C语言-数据结构与算法】-哈夫曼压缩解压缩-终局-如何做一个自己独有的压缩软件

    哈夫曼压缩&解压缩 Ⅰ 前言 Ⅱ 需求分析&主函数带参的应用 A. 需求分析 B. 压缩部分 B. 解压缩部分 Ⅲ 哈夫曼压缩 A. 代码分析 B. 从文件中读取内容生成频度表 C. ...

  6. [源码和文档分享]C语言实现的基于Huffman哈夫曼编码的数据压缩与解压缩

    一.实验题目 用哈夫曼编码实现文件压缩 二.实验目的 了解文件的概念 掌握线性链表的插入.删除等算法 掌握Huffman树的概念及构造方法 掌握二叉树的存储结构及遍历算法 利用Huffman树及Huf ...

  7. 数据结构编程实践(七)创建哈夫曼树、生成哈夫曼编码、完成图片的压缩与解压缩

    一.对图片的压缩与解压缩,涉及以下内容: 1.文件读写 2.创建Huffman树 3.生成Huffman编码 4.压缩图片文件 5 .  解压缩图片文件 二.将项目分成三个小任务,下一任务是在上一任务 ...

  8. C#,哈夫曼编码(Huffman Code)压缩(Compress )与解压缩(Decompress)算法与源代码

    David A. Huffman 哈夫曼编码简史(Huffman code) 1951年,哈夫曼和他在MIT信息论的同学需要选择是完成学期报告还是期末考试.导师Robert M. Fano给他们的学期 ...

  9. 【C语言->数据结构与算法】->哈夫曼压缩解压缩->第一阶段->哈夫曼编码解码的实现

    文章目录 Ⅰ 前言 Ⅱ 代码实现哈夫曼编码&解码 A. 构造哈夫曼树 a. 频度统计 b. 生成哈夫曼树 ① 初始化节点 ② 查找频度最小节点 ③ 哈夫曼树的构造 B. 哈夫曼编码 a. 得到 ...

最新文章

  1. 从传统到深度学习:浅谈点云分割中的图结构
  2. BZOJ 1084: [SCOI2005]最大子矩阵
  3. python知识:numpy.geomspace()
  4. Log4j详细介绍(七)----日志格式化器Layout
  5. VMware QueryPerformanceCounter/GetTickCount 悬案
  6. Hulu直播服务难点解析(二):系统设计与实现
  7. 【javascript权威指南】类型转换
  8. Mybatis 高级结果映射 ResultMap Association Collection
  9. 如何使用 C# 扩展方法
  10. 2021-10-28嵌入式人工智能
  11. 清空了回收站怎么找回?你没用过的方法
  12. StyleCop学习笔记——默认的规则
  13. 网站服务器和空间大小,网站服务器和空间大小
  14. 高德地图 API 搜索服务 搜索定位 用不了
  15. c语言作业 分解质因数,分解质因数(C语言)
  16. 谷歌浏览器 performance 详解
  17. 带你走进缓存世界(1):漫谈缓存
  18. 用python进行列联表卡方检验
  19. Qt使用 QProcess来检测 ip 设备是否在线(Ping)
  20. 5800日常操作使用小技巧

热门文章

  1. php if else 语句怎么写,PHP if…else 语句
  2. 第127届广交会闭幕,建立线上贸易新机制
  3. 万众达机械携手中企动力制霸高空作业平台市场
  4. WiFi6模块W80
  5. Kinect驱动出现黄色感叹号问题解决
  6. RL(Chapter 3): GridWorld
  7. Ubuntu 14.04 安装 QQ2013
  8. 创业故事,终生受用 之 创业必须抓住人性弱点及需求
  9. 关于thymeleaf的#strings.abbreivate(str,10)
  10. 关于QQ空间相册功能的构想与简单实现