1. 程式人生 > >用Huffman樹實現檔案壓縮與解壓

用Huffman樹實現檔案壓縮與解壓

用Huffman樹實現檔案的壓縮與解壓
我們先來了解一下什麼是Huffman樹?
我們平常所使用的Zip等壓縮工具都是藉助Huffman樹實現的,Huffman是一種特殊的二叉樹,它是一種加權路徑最短的二叉樹,
因此也稱為最優二叉樹。
(下面用一幅圖來說明)
這裡寫圖片描述
它們的帶權路徑長度分別為:

圖1: WPL=3*2+4*2+2*2+10*2=48
圖2: WPL=3*3+2*3+4*2+10*1=33
我們很容易可以看出圖2的帶權路徑長度較小,證明圖b就是哈夫曼樹(也稱為最優二叉樹)。

如何構建一個Huffman樹?
一般我們可以按下面步驟構建:
1)將所有左,右子樹都為空的作為根節點。
2)在森林中選出兩棵根節點的權值最小的樹作為一棵新樹的左,右子樹,且置新樹的附加根節點的權值為其左,右子樹上根節點的權值之和。注意,左子樹的權值應小於右子樹的權值。
3)從森林中刪除這兩棵樹,同時把新樹加入到森林中。
4)重複2,3步驟,直到森林中只有一棵樹為止,最終生成的樹便是哈夫曼樹
(Huffman的構建圖解如下):
這裡寫圖片描述


Huffman有什麼特點呢?

我們構建的Huffman樹,保證了所有的成員資料都在葉子節點上,最底層的是最小的兩個節點資料,向頂端求和,然後再依次增加一個次小的
節點資料再求和,知道資料被全部插入Huffman樹中,這樣一來這個樹中的節點就有了一個特點,離根節點越近的節點資料越大,
離根節點越遠的節點資料越小。

用Huffman樹實現檔案壓縮流程:

1.構造一個存放該檔案中字元出現的資訊的結構體,該結構體的成員分別有字元、字元出現的次數,對該字元的編碼。
2.建立一個字元資訊的結構體陣列,讀要壓縮的檔案,統計給檔案的字元資訊寫入該陣列中。
3.根據該字元陣列,按照字元出現的次數構造一個Huffman,由於Huffman樹的構建每次取最小資料的特點,所以我們藉助構建一個小頂堆來實現。
4.根據該Huffman對每個字元進行編碼,將每個字元的編碼寫入字元結構體陣列中。(這裡我們用圖示具體說明一下)
這裡寫圖片描述


5.對該檔案進行壓縮,讀取檔案資訊,根據讀取到的字元壓縮為該字元對應的編碼,編碼夠8位寫入壓縮檔案,讀完檔案中的所有內容,壓縮完成。
需要注意的是什麼時候就完成了對整個檔案的壓縮呢?這個問題其實我們可以有多種方法,我們在第一次讀取原始檔的時候就可以增加一個
計數器來記錄當前檔案的字元總數,另外其實如果我們足夠熟悉Huffman樹,很容易就可以看出來,Huffman數的根節點有什麼意義?
沒有錯,根節點的值是從底端到頂端一層層累加上去的,因此根節點的資料就儲存著檔案的總字元數。

壓縮原始碼:

    string Compress(const char* filename)
    {

        // 1.統計字元出現的次數
assert(filename); FILE* fout = fopen(filename, "rb"); //以只讀方式開啟檔案 assert(fout); //讀取檔案中的字元 int ch = fgetc(fout); while (ch != EOF) { _Infos[ch]._count++; ch = fgetc(fout); } //fclose(fout); // 2.生成huffman tree CharInfo invaild; HuffmanTree<CharInfo> Tree(_Infos, 256,invaild); // 3.生成huffmn code string code; _GenerateHuffmanCode(Tree.ReturnRoot(), code); Node* Root = Tree.ReturnRoot(); cout <<"壓縮"<< Root->_Weight._count <<"字元"<<endl; //4.壓縮 string CompressFilename = filename; CompressFilename.append(".Compress"); FILE* fin = fopen(CompressFilename.c_str(), "wb"); fseek(fout, 0, SEEK_SET); //將檔案偏移量設定為0 ch = fgetc(fout); unsigned char value = 0; int pos = 0; while (ch != EOF) { code = _Infos[ch]._code; for (size_t i = 0; i < code.size(); i++) { //value &= (code[i]&1); //value <<= 1; if (code[i] == '1') { value <<= 1; value |= 1; } else { value <<= 1; value |= 0; } pos++; if (pos == 8) { pos = 0; fputc(value, fin); value = 0; } } ch = fgetc(fout); } if (pos != 0) { value <<= (8 - pos); fputc(value, fin); } cout << "檔案壓縮:" << filename<<endl; /*cout << "一共壓縮:" <<Tree.ReturnRoot()._Weight._count << "字元" << endl;*/ fclose(fin); fclose(fout); //建立配置檔案,配置檔案儲存原始檔中字元和字元所出現的次數 string ConfigFilename = filename; ConfigFilename += ".config"; FILE* finConfig = fopen(ConfigFilename.c_str(), "wb"); string Line; char Buff[128]; for (int i = 0; i < 256; i++) { if (_Infos[i]._count != 0) { /*Line += _Infos[i]._ch;*/ fputc(_Infos[i]._ch, finConfig); /*Line += ',';*/ Line = Line + ','; //Line += itoa(_Infos[i]._count, Buff, 10); _itoa((int)_Infos[i]._count, Buff, 10); Line += Buff; Line += '\n'; fputs(Line.c_str(), finConfig); Line.clear(); } } fclose(finConfig); return CompressFilename; }

這個時候我們就可以來實現解壓縮部分了,那麼如何對壓縮檔案進行解壓縮呢?

6.首先解壓縮時需要一個配置檔案,該檔案是原始檔的字元資訊,需要造次構建一個Huffman,來對應完成檔案解壓,為什麼需要這個配置檔案?
原因很簡單,我們壓縮的時候構建了一個Huffman樹並對每個字元進行了編碼,我們是按照這種規則去壓縮的,因此在解壓縮的時候我們同樣
需要這樣Huffman樹來幫助我們完成解壓縮,正因為這樣的壓縮與解壓縮的對應關係,我們實現了對原始檔準確無誤的還原。

7.讀取壓縮檔案,當我們讀取到碼’1’時指向Huffman樹的指標向右移動,當我們讀取到碼’0’時指標向左移動,由於每個字元對應的都是葉子結點,因此
我們只需要判斷當前指標的Left和Right是否為空,為空則將該段碼字譯為對應的字元即可。迴圈讀取解壓,直到檔案結尾即實現了對整個檔案的解壓縮。

解壓縮原始碼:

string UnCompress(const char* filename)                 //解壓縮
    {
        assert(filename);
        //開啟配置檔案
        string name= filename;
        size_t Config_index = name.rfind('.');
        string ConfigFileName = name.substr(0, Config_index);
        ConfigFileName += ".config";
        FILE* foConfigFilename = fopen(ConfigFileName.c_str(),"rb");


        cout << "解壓縮檔案:" << name << endl;
        //重新構建_Infos 雜湊表
        string Line;
        while (ReadLine(foConfigFilename, Line))
        {
            unsigned char ch = Line[0];
            _Infos[ch]._ch = Line[0];
            LongType count = atoi(Line.substr(2).c_str());
            _Infos[ch]._count = count;
            Line.clear();
        }


        //消除壓縮檔案的字尾(.Compress)
        string CompressFilename = filename;
        size_t index=CompressFilename.rfind('.');
        string UnCompressFilename = CompressFilename.substr(0, index);
        UnCompressFilename.append(".UnCompress");

        // 生成huffman tree
        CharInfo invaild;
        HuffmanTree<CharInfo> Tree(_Infos, 256, invaild);

        //開啟要被解壓縮的檔案
        FILE* fout = fopen(filename, "rb");

        //建立解壓縮檔案,並寫入
        FILE* fin = fopen(UnCompressFilename.c_str(), "wb");

        Node* root = Tree.ReturnRoot();
        Node* cur = root;
        LongType count = root->_Weight._count;

        cout << "解壓縮" << count << "字元" << endl;
        unsigned char ReadCh = fgetc(fout);
        int pos = 7;
        while (ReadCh != EOF)
        {
            if (pos >= 0)
            {
                if (((ReadCh >> pos) & 1) == 1)
                {
                    cur = cur->_Right;
                }
                else
                {
                    cur = cur->_Left;
                }
                --pos;
                if (cur->_Weight == NULL && cur->_Right == NULL&& count > 0)
                {
                    fputc(cur->_Weight._ch, fin);
                    cur = root;
                    --count;
                }
                if (count <= 0)
                {
                    break;
                }
            }
            else
            {
                ReadCh = fgetc(fout);
                pos = 7;
            }
        }
        return UnCompressFilename;
    }

下面我們來看一下壓縮和解壓縮的結果如何?
這裡寫圖片描述
上圖中名為src_file的是我們解壓的原始檔。
字尾為.Compress的是生成的壓縮檔案,我們對比一下原始檔可以看到檔案大小明顯從4KB縮小到了3KB因為我的測試原始檔裡面寫了一小部分內容,檔案大小不是足夠的大,所以解壓效果不是很明顯,大家可以嘗試解壓一個相對較大的檔案,對比一下結果。
第三個字尾為.txt的為為解壓專門生成的配置檔案。
最後一個字尾為.UnCompress的檔案是完成的解壓縮檔案。開啟該檔案內容和原始檔完全相同。

到這我們就順利地完成了基於Huffman樹的檔案壓縮與解壓縮,在實現該功能過程中我遇到了一些問題和大家分享一下:
(1)、讀寫檔案的開啟方式,我們開啟檔案時一定要使用二進位制形式”wb”,”rb”開啟需要讀寫的檔案。文字形式開啟會對檔案中的一些字元做特殊處理,這樣會導致不能正確無誤地解壓檔案。
(2)、本次Huffman樹實現檔案的壓縮與解壓程式用C++模板程式設計,由於模板不能分離編譯,因此跨檔案應用程式時需要將標頭檔案寫為.hpp字尾方可通過編譯。