用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字尾方可通過編譯。