Qt实现哈夫曼编码解压缩软件详解

目录

    • 一、概要设计
    • 二:设计效果展示:
    • 三、源代码
      • 1°MainWindow.h
      • 2°MainWindow.cpp
      • 3°Compression.h
      • 4°Compression.cpp
    • 四、软件分析

诸位既然点开了本帖,相信对此问题已有初步了解,哈夫曼树的原理不再赘述,我们开门见山,直入主题。

一、概要设计

问题拆解:设计一个基于哈夫曼编码的解压缩软件,这个问题我认为可以分解为以下几个子问题:

  • 读取传入文件,进行字符权重统计
  • 将出现的字符放入哈夫曼树结点,构建哈夫曼树,获取哈夫曼编码
  • 将编码相关信息写入压缩后的文件,再将传入文件的每个字符按照哈夫曼编码转换,每8个二进制位作为一个字节传入压缩后的文件
  • 解压部分:将传入的已压缩文件进行文件流读取,获取编码信息进行还原

根据这几个子问题的思路顺序,我们逐个击破,寻找解决方案:

  • 读取传入文件,需要用到文件流。这里推荐使用Qt自带的QFile文件流配以QDatastram辅助。因为,C++的文件流fstream无法识别中文路径(一说起这个,就想起Debug时的辛酸)。权重统计我用的是map记录:mapWeightmap
  • 哈夫曼结点的构造,和二叉树没有什么区别,只是在封装的struct里面加了unsigned char 型的字符,int型的权重,string型的哈夫曼编码,和是否是叶子结点的bool型标记(哈夫曼树叶子结点才是我们需要的编码)。将前面我们获取到的字符和权重加进去。
    我们将建立好的结点放入到一个vector内(理论上什么容器都可以),进入循环:根据哈夫曼结点的权重比较进行排序(直接调用sort),每次将连个最小的结点权重取出,相加得到权重和,以权重和建立一个新的结点,新结点的左右孩子结点就是这两个结点,将新结点加入到vector中,那两个结点删除。循环结束条件为vector内只剩下一个结点。这个结点便是哈夫曼树的根结点,保存一根结点足矣。
    我们从根结点出发遍历,左子树的string+“0”,右子树的string+“1”。并将unsigned char型的字符与这个stringmap PasswordMap 记录。
  • 传入辅助信息阶段:我们传入的信息有,PasswordMap.size(),循环传入PasswordMap->first(字符),PasswordMap->second.size()(记录编码长度),PasswordMap->second(这个字符串有可能超过8位,每个字节不足8位的部分补0)直到完全传入。另外一个方法是将所有字符与字符权重传入,在解压缩时再次构建哈夫曼树,两种思路我觉得都可以。这里我采用的第一种方法。
    传入哈夫曼编码阶段:遍历先前在读取文件时获取的字符串,将字符串的每一位转化为哈夫曼编码,用另一个字符串储存。这里不妨称这个字符串为二进制字符串,这个二进制字符串按8分割到最后可能会有不足8位的部分,对其进行补0操作。 最后再传入一个字节,记录补0数。
  • 解压部分:对应上部分的操作进行对应读取,主要思路是将压缩时传入的辅助信息提取,建立一个新的Map映射,根据这个映射,将原文件还原。

二:设计效果展示:

压缩前:
Qt实现哈夫曼编码解压缩软件详解_第1张图片
压缩中:
Qt实现哈夫曼编码解压缩软件详解_第2张图片
压缩后:
Qt实现哈夫曼编码解压缩软件详解_第3张图片
解压中:
Qt实现哈夫曼编码解压缩软件详解_第4张图片
解压后:
Qt实现哈夫曼编码解压缩软件详解_第5张图片

三、源代码

前面分析得很清晰,不过写代码的过程异常艰辛,充满了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 
#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

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的最大值设为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界面“解压”按钮的槽函数,思想与前者相同

3°Compression.h

在这一块定义哈夫曼树节点,定义了压缩类,具体见注释

#ifndef COMPRESSION_H
#define COMPRESSION_H

#include 
#include
#include
#include
#include
#include
#include
#include
#include
#include

using namespace std;

struct HuffmanTreeNode
{
    pairWeight;  //记录字符和其权重
    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掉申请空间的哈夫曼树指针,采用递归遍历进行delete
    
    void BinaryString_Init();
    //二进制字符串的初始化,即从传入文件得到的字符串按照哈夫曼编码获取二进制字符串
    
    void Weightmap_Init(QFile& in);
    //参数是QFile型,是Qt的文件操作,该函数打开传入文件,遍历,形成一个字符串,并且初始化WeightMap
    
    HuffmanTreeNode* MakehuffmanTreeNode(int i);
    //该函数的功能是创建单个哈夫曼树结点
    
    void HuffmanTreeVector_Init();
    //将创建的哈夫曼结点放入Vector中
    
    void HuffmanTree_Init();
    //建立哈夫曼树的函数
    
    void ZipPassword_Init(HuffmanTreeNode* &root, string& password);
    //对建成的哈夫曼树进行遍历,更新每个结点的zippassword,即哈夫曼编码
    
private:
    map weightmap;  //权重映射
    mappasswordmap;  //哈夫曼编码映射
    string source_string;       //读取传入文件形成的字符串
    string binary_string;      //通过哈夫曼编码映射形成的二进制字符串
    vector container;  //哈夫曼结点储存的容器
    map 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();
            //转换为我们亲切的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();
 }

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

四、软件分析

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

文件类型 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,即哈夫曼树叶子结点几乎全在最底层,使压缩效率大打折扣。

你可能感兴趣的:(数据结构)