1. 程式人生 > >哈夫曼編碼實現文字壓縮和解壓(C++)

哈夫曼編碼實現文字壓縮和解壓(C++)

哈弗曼樹:又稱最優二叉樹,是帶權路徑長度最短的樹。

哈夫曼編碼:是一種字首編碼,即同一字符集中任何一個字元的編碼都不是另外一個字元編碼的字首(最左子串)。

        在哈弗曼樹中,若用‘0’表示左子樹,‘1’表示右子樹,那麼每當從根遍歷到一個葉子節點時都會形成一個01串,即該葉子節點的編碼,由於各個葉子節點已經是樹的最末梢了,因此他們之間的編碼不會有包含關係,這樣就生成了字首編碼集。

那麼,如何構建哈弗曼樹呢?

    首先,我們需要一串字元,並且得到其中每個字元出現的頻次(作為權值),接著我們將這些權值放在一個vector中,並將其從小到大進行排序,之後取前兩個(權值最小的兩個)作為葉子節點構建一棵二叉樹,將權值相加賦給新生成的根節點,之後將根節點加入到vector中

,重複以上操作,直到vector中僅剩一個數據為止,該資料則是哈弗曼樹最終的根節點。程式碼實現如下:

#include <iostream>
#include <algorithm>
#include <string>
#include <vector>
#include <map>
using namespace std;

typedef struct a
{
    char key = '\0';  //字元
    int weight = 0;   //權值(出現頻次)
    struct a *LeftChild = nullptr;
    struct a *RightChild = nullptr;
}Node;

bool cmp(Node *data1,Node *data2)
{
    return data1 -> weight < data2 -> weight;
}

class HuffManTree
{
public:
    Node *root;
    explicit HuffManTree(map<char,int> d)  //傳入一個map,其中儲存了所有的字元及其出現的頻次
    {
        vector<Node *> weight_order;
        // 遍歷map,將其中的字元以及權值儲存到各個葉子節點(node)中,將這些節點放在一個vector裡面
        for(auto iter : d)
        {
            auto tmp = new Node;
            tmp -> key = iter.first;
            tmp -> weight = iter.second;
            weight_order.push_back(tmp);
        }
        // 根據先前的思想,我們至少應該同時獲取兩個節點的資訊才能構建哈弗曼樹,那麼假如傳入只有一個節點,就要進行特殊化處理
        if(weight_order.size() == 1)
        {
            auto tmp = new Node;
            auto t = new Node;
            tmp -> weight = weight_order[0] -> weight;
            Node_Copy(t,weight_order[0]);
            weight_order.erase(weight_order.begin());
            tmp -> LeftChild = t;
            tmp -> RightChild = nullptr;
            weight_order.push_back(tmp);
        }
        //正常情況下,節點數大於等於2,此時則需要不斷地排序,建樹,刪除舊的兩個節點,加入一個新的節點,直到最終vector中僅剩餘一個節點(根節點)
        while(weight_order.size() != 1)
        {
            sort(weight_order.begin(),weight_order.end(),cmp);  //排序
            auto tmp = new Node;
            tmp -> weight = weight_order[0] -> weight + weight_order[1] -> weight;
            auto carbon1 = new Node;
            auto carbon2 = new Node;
            Node_Copy(carbon1,weight_order[0]);
            Node_Copy(carbon2,weight_order[1]);
            tmp -> LeftChild = carbon1;  //建樹
            tmp -> RightChild = carbon2;
            weight_order.erase(weight_order.begin(),weight_order.begin()+2); //刪點
            weight_order.push_back(tmp);  //加點
        }
        this -> root = weight_order[0]; //最終讓vector中的節點作為哈弗曼樹的根節點
    }
    void Node_Copy(Node *a,Node *b)
    {
        a->key=b->key;
        a->weight=b->weight;
        a->LeftChild=b->LeftChild;
        a->RightChild=b->RightChild;
    }
//    void PreOrder(Node *root)  //先序遍歷
//    {
//        if(root == nullptr)
//        {
//            return;
//        }
//        cout << root -> key << " " << root -> weight << endl;
//        PreOrder(root -> LeftChild);
//        PreOrder(root -> RightChild);
//    }
};

剛剛我們已經將哈弗曼樹構建好了,接下來該怎樣獲取每個字元的編碼呢?我的方法是利用二叉樹先序遍歷的順序,首先設定一個vector<pair<char,string>>物件準備儲存每個字元以及即將得到的編碼,接著設定一個空的string物件tmp,從根節點開始若遇到左子樹則tmp+=‘0’,若遇到右子樹則tmp+='1’,若遇到葉子節點即該葉子中的字元編碼已經形成,便將其複製給vector<pair<char,string>>物件中與該字元對應的string,然後將tmp中最後一個字元刪掉,返回上一層,遍歷其右子樹,當右子樹也遍歷完時,它將逐層返回,它要訪問的下一個節點將是當前子樹的根節點的兄弟節點,因此在每返回一層時都需要刪除tmp的最後一個字元,否則編碼就會出錯。

#include "HuffmanTree.h"
#include <iostream>
#include <string>
#include <vector>

void getcode(vector<pair<char,string>> *codeorder,string *tmp,Node *root)
{
    if(root -> LeftChild == nullptr && root -> RightChild == nullptr) //遇到葉子節點
    {
        pair<char,string> t;
        t.first = root -> key;
        t.second = *tmp;
        codeorder->push_back(t);
        tmp->pop_back();
        return;
    }
    if(root -> LeftChild)  //若左子樹,則tmp+='0'
    {
        *tmp+="0";
        getcode(codeorder,tmp,root -> LeftChild);
    }if(root -> RightChild)  //若右子樹,則tmp+='1'
    {
        *tmp+="1";
        getcode(codeorder,tmp,root -> RightChild);
    }
    tmp->pop_back();//逐層返回的時候一定要刪掉最後的一個字元
}
void StructuralCoding(vector<pair<char,string>> *codeorder,Node *root) //傳入待填充的編碼集以及哈弗曼樹的根節點
{
    string tmp;
    getcode(codeorder,&tmp,root);
}

文字的壓縮:

    到現在我們已經獲得了字元的編碼集,接下來我們只需要遍歷原本的字串並對照編碼集便可以得到待壓縮的01串了。那麼01串要怎麼儲存才能實現壓縮的功能呢?我們可以看到,原本的字串中每一個字元在記憶體中都佔用了一個位元組,也就是八位,但是那些ascii碼值很小的字元二進位制實際上是不需要八位來儲存的,這就造成的空間的浪費,而我們的哈弗曼編碼實際上就是通過自己編碼以實現空間的最大化利用。舉個例子,字串AB,單個儲存的話,它將耗費16位來儲存,而通過哈弗曼編碼,A~0,B~1,那麼AB串就可以用01來表示,這樣它在計算機中就只需要兩位便足以儲存。

    因此,我的做法便是,將已經得到的01串,八位並一位,用bitset將其轉化為十進位制數,又因為2^8-1=255,ascii碼的範圍剛好可以滿足,於是便可以將該十進位制數以對應的ascii字元儲存在檔案中,這樣便實現了壓縮的功能。

    應當注意的一點是,在壓縮時,應該把原文字的字元以及權值也同時儲存在壓縮檔案中,否則丟失了編碼規則將導致無法解壓,雖然這樣的做法在文字較短或字元種類較多時壓縮後的檔案可能會更大,但是在原始檔較大時,他的壓縮功能可以得到更好的體現。

#include "HuffmanTree.h"
#include <sstream>
#include <iostream>
#include <fstream>
#include <vector>
#include <bitset>
#include <map>
using namespace std;
#define TXT_NAME "a.sh"    //待壓縮檔案
#define BIN_NAME "text2.o"  //壓縮後的檔案

void OutToBinFile(vector<pair<char,string>> *codeorder,map<char,int> *data_b)
{
    fstream infile(TXT_NAME); //開啟待壓縮檔案
    if(!infile)
    {
        cout << "fail to open the file" << endl;
        exit(0);
    }
    char c;
    string codestream;  //01串
    //逐位讀取檔案中的字元,根據編碼集將其轉換為01串儲存在codestream中
    while(true)
    {
        if(infile.peek() == EOF)
            break;
        infile.read(&c,1);
        for(auto iter : *codeorder)
        {
            if(c == iter.first)
               codestream += iter.second;
        }
    }
    infile.close();
    fstream outfile;
    outfile.open(BIN_NAME,ios::out | ios::binary);  //開啟壓縮後文件
    for(auto iter : *data_b)  //字元及對應權值儲存進二進位制檔案
    {
        outfile << iter .first << "~" << iter . second << " ";
    }
    outfile << "*@#";  //編碼規則結束標記
    while(codestream.size() >= 8)   //01串轉換為字元並輸出
    {
        bitset<8> bits(codestream,0,8);
        codestream.erase(0,8);
        outfile << static_cast<char>(bits.to_ulong());
    }
    if(!codestream.empty())   //結尾長度不夠八位,用0補齊,記錄剩餘的長度將其放在檔案末尾,在讀取時轉換為下標位置還原本來的字串
    {
        ulong loc = codestream.size();
        bitset<8> bits(codestream,0,loc);
        outfile << static_cast<char>(bits.to_ulong());
        outfile << static_cast<char>(loc);
    }
    outfile.close();
}

壓縮檔案的解壓:

    解壓時,我們應當先將其中儲存的編碼規則提取出來,之後根據規則將其還原,這裡有兩種方法可以實現,一,儲存的是剛才的編碼集,在解壓時一位一位的讀取字元轉化為01串,之後每新增一位便在編碼集中尋找是否有對應的字元,若有則轉換,沒有則繼續擴充01串,這樣的方法雖然可以實現解壓,但是經實測,效率相當低,因此不推薦。二,將原始檔的字元和對應頻次儲存在壓縮檔案中,解壓時先由此生成哈弗曼樹,之後再將01串逐位遍歷,遇0走左子樹,遇1走右子樹,遇到葉子節點則將其中的字元提取出來儲存在待存放解壓資訊的檔案中,該方法的效率大大提高,經檢驗,10Mb左右的檔案大小都可以秒解(更大的沒試過),下來看程式碼:

void OutToTxt(string *strline,string *codestream)   //構建map,建哈弗曼樹,遍歷01串輸出
{
    char a;
    int b;
    ulong t;
    string tmp;
    map<char,int> data_b;
    fstream outfile_t(TXT_NAME,ios::out);  //開啟壓縮檔案

    while(!strline->empty())   //讀取其中儲存的字元以及出現的頻次,由此生成哈弗曼樹
    {
        a = strline -> at(0);  
        strline -> erase(0,2);
        t = strline -> find(' ');
        tmp = strline -> substr(0,t);
        strline -> erase(0,t+1);
        b = stoi(tmp,nullptr,10);
        data_b.insert(pair<char,int>(a,b));
    }
    HuffManTree HTree(data_b);  //建樹
    Node *p = HTree.root;

    string end = codestream->substr(codestream->size()-16,16);  //處理末尾不夠八位的情況
    bitset<8> loc(end,8,16);  
    ulong add = loc.to_ulong();
    end = end.substr(8-add,add);
    codestream->erase(codestream->size()-16,codestream->size());
    *codestream += end;

    for(auto i : *codestream)  //用01串遍歷哈弗曼樹
    {
        if(i == '0')  //遇0,走左
            p = p -> LeftChild;
        if(i == '1')  //遇1,走右
            p = p -> RightChild;
        if(p -> LeftChild == nullptr && p -> RightChild == nullptr)  //遇葉子節點,提取字元並存儲
        {
            outfile_t << p->key;
            p = HTree.root;
        }
    }
}

最後將main中的操作直接粘出來,沒有什麼難度,都可以看得懂

#include <algorithm>
#include <iostream>
#include <fstream>
#include <cstring>
#include <string>
#include <queue>
#include <map>
#include "HuffmanTree.h"
#include "OutPut.h"
#include "Code.h"

using  namespace std;

void TxtZip()
{
    char key;  //txt檔案字元,map中的關鍵字
    map<char,int> data_b;  //檔案中的字元以及對應的出現次數
    pair<char,string> code; //字元及對應編碼
    vector<pair<char,string>> codeorder;  //字元編碼集
    fstream file(TXT_NAME);
    if(!file)
    {
        cout << "fail to open the file" << endl;
        exit(0);
    }
     while (true)  //填充map
     {
         file.read(&key, 1);
         if(file.peek() == EOF)
             break;
         data_b[key] += 1;
     }
     file.close();
    HuffManTree Htree(data_b);  //構建哈弗曼樹
//    Htree.PreOrder(Htree.root);
    StructuralCoding(&codeorder,Htree.root);  //構建字元編碼集
    OutToBinFile(&codeorder,&data_b);
    codeorder.clear();
}

void BinUnzip()  //獲取編碼流和01串
{
    char c,o,p,q;
    bitset<8> a;
    string strline,codestream;  //strline儲存二進位制檔案中的字元及對應權值,codestream儲存01串
    fstream infile_b(BIN_NAME,ios::in | ios::binary);
    while(true)         //從二進位制檔案中獲取字元及對應權值
    {
        if(infile_b.peek() == EOF)
            break;
        infile_b.read(&o,1);
        if(o == '*')
        {
            infile_b.read(&p,1);
            infile_b.read(&q,1);
            if(p=='@'&&q=='#')
                break;
            else infile_b.seekg(-2 , ios::cur);
        }
        strline += o;
    }
    while(true)       //讀取字元轉化為01串
    {
        if(infile_b.peek() == EOF)
            break;
        infile_b.read(&c,1);
            a = c;   //字元賦值給bitset<8>可直接轉化為二進位制
            codestream += a.to_string() ;
    }
    cout << "Decompression, please wait..." << endl;
    OutToTxt(&strline,&codestream);
}

int main(int argc , char *argv[]) {
    if(argc == 2 && !strcmp(argv[1],"-zip")) {
        TxtZip();
    }
    else if(argc == 2 && !strcmp(argv[1],"-unzip"))
        BinUnzip();
    else cout << "please input legal parameters!!";
    return 0;
}

以上就是我利用哈弗曼樹實現的檔案壓縮和解壓了,該程式在跑小檔案的時候還行,檔案大一點的話壓縮會比較慢,本人暫時沒有想到優化的方法,若有大佬指點,感激不盡!!