Huffman的應用之檔案壓縮與解壓縮
檔案壓縮與解壓縮>
最近這段時間一直在學習樹的這種資料結構,也接觸到了Huffman樹以及瞭解了什仫是Huffman編碼,而我們常用的zip壓縮也是利用的Huffman編碼的特性,那仫是不是可以自己實現一個檔案壓縮呢?當然可以了.在檔案壓縮中我實現了Huffman樹和建堆Heap的程式碼,zip壓縮的介紹>
下面開始介紹自己實現的檔案壓縮的思路和問題...
1).統計>讀取一個檔案統計這個檔案中字元出現的次數.
2).建樹>以字元出現的次數作為權值使用貪心演算法構建Huffman樹(根據Huffman樹的特性>字元出現次數多的一定靠近根結點,出現次數少的一定遠離根結點).
3).生成Huffman編碼>規則左0右1.
4).壓縮>再次讀取檔案,根據生成的Huffman編碼壓縮檔案.
5).生成配置檔案>將字元以及字元出現的次數寫進配置檔案中.
6).解壓縮>利用配置檔案還原出Huffman樹,根據壓縮檔案還原出原檔案.
7).測試>判斷解壓是否正確需要判斷原檔案和解壓縮之後的檔案是否相同,利用Beyond Compare軟體進行對比.
下面是我舉的一個簡單的範例,模擬壓縮和解壓縮的過程,希望有讀者有幫助
利用Beyond Compare軟體進行對比>
在實現中出現了很多的問題,下面我提出幾個容易犯的問題,僅供參考
1).在使用貪心演算法構建Huffman樹的時候,如果我們以unsigned char一個位元組來儲存它總共有2^8=256個字元,如果將所有的字元都構建Huffman樹,這不僅降低了效率還將分配極大的記憶體.所以我設立了非法值這個概念,只有當字元出現的次數不為0的時候才將該字元構建到Huffman樹中.
2).在寫壓縮檔案的時候我們是將字元對應的Huffman編碼轉化為對應的位,每當填滿一個位元組(8位)後再寫入壓縮檔案中.如果最後一個位元組沒有填滿我們就將已經填的位移位空出後幾個位置,將未滿的位置補0補滿一個位元組再寫入壓縮檔案中.
3).如果我們將原始檔壓縮之後再去解壓,因為你的Huffman樹和Huffman編碼都是在壓縮函式中得到的,此時再去解壓那仫你的Huffman編碼以及不存在了該如何去還原檔案呢?這就是為什仫要生成配置檔案的原因了,在配置檔案中寫入了字元以及字元出現的次數.在解壓縮中根據配置檔案構建新的Huffman樹.
4).由壓縮檔案還原檔案的時候如何知道壓了多少個字元呢?也就是說因為我們在壓縮的時候最後一個位元組是補了0的在解壓縮的時候可能會把這個補的位當成字元的編碼來處理.一種想法是在統計字元出現的次數的時候設定一個變數,每讀取一個字元該變數就加1,最後將該變數寫進配置檔案中.另外一種想法就是根據根結點的權值,通過上述簡單的例項觀察可知根結點權值中的_count就是字元出現的次數.
解決了以上幾個問題,我的程式已經可以壓縮那256個字元並正確的還原了,那仫如果是大檔案或者是漢字,圖片以及音訊視訊呢?
1).因為有些特殊的字元編碼,所以我們統計字元出現的次數的時候應該用的是unsigned char,剛開始我用的檔案結束標誌是EOF在ASII中它的編碼是-1此時已經不可以用EOF來判斷是否檔案結束了,所以我用了feof這個函式來判斷檔案是否結束.
2).統計字元出現次數應該用的型別是long long,這就解決了大檔案的壓縮和解壓縮的問題了.
3).因為漢字,圖片,視訊這些在記憶體中是以二進位制的形式存在的,所以我們將以文字形式開啟讀或者寫的修改為以二進位制的形式讀或者寫.
為了驗證大檔案的壓縮我找了一個8.09M的檔案壓縮之後是6.50M,並且可以正確還原.
1).測試效率>
2).利用Beyond Compare軟體進行對比,如果一樣說明壓縮成功>
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include "Heap.h"
template<class T>
struct HuffmanTreeNode
{
T _weight;
HuffmanTreeNode<T> *_left;
HuffmanTreeNode<T> *_right;
HuffmanTreeNode<T> *_parent;
HuffmanTreeNode(const T& w=T())
:_weight(w)
,_left(NULL)
,_right(NULL)
,_parent(NULL)
{}
};
template<class T>
class HuffmanTree
{
typedef HuffmanTreeNode<T> Node;
public:
HuffmanTree()
:_root(NULL)
{}
HuffmanTree(const T* a,size_t size)
:_root(NULL)
{
_root=_CreatHuffmanTree(a,size);
}
//將未出現的字元過濾出來,不構造堆
HuffmanTree(const T* a,size_t size,const T& invalid)
{
_root=_CreatHuffmanTree(a,size,invalid);
}
Node* GetRoot()
{
return _root;
}
~HuffmanTree()
{
_Destroy(_root);
}
protected:
Node *_CreatHuffmanTree(const T* a,size_t size)
{
struct NodeLess
{
bool operator()(Node *l,Node *r)const
{
return l->_weight < r->_weight;
}
};
Heap<Node *,NodeLess> minHeap;
//建立結點並放入vector中
for (size_t i=0;i<size;++i)
{
Node *tmp=new Node(a[i]);
minHeap.Push(tmp);
}
//取出較小的兩個結點作為左右孩子並構建父結點
while (minHeap.Size() > 1)
{
Node *left=minHeap.Top();
minHeap.Pop();
Node *right=minHeap.Top();
minHeap.Pop();
Node *parent=new Node(left->_weight + right->_weight);
parent->_left=left;
parent->_right=right;
left->_parent=parent;
right->_parent=parent;
minHeap.Push(parent);
}
return minHeap.Top();
}
//思路類似不帶過濾功能的
Node *_CreatHuffmanTree(const T* a,size_t size,const T& invalid)
{
struct NodeLess
{
bool operator()(Node *l,Node *r)const
{
return l->_weight < r->_weight;
}
};
Heap<Node *,NodeLess> minHeap;
//建立結點並放入vector中
for (size_t i=0;i<size;++i)
{
if(a[i] != invalid)
{
Node *tmp=new Node(a[i]);
minHeap.Push(tmp);
}
}
//取出較小的兩個結點作為左右孩子並構建父結點
while (minHeap.Size() > 1)
{
Node *left=minHeap.Top();
minHeap.Pop();
Node *right=minHeap.Top();
minHeap.Pop();
Node *parent=new Node(left->_weight + right->_weight);
parent->_left=left;
parent->_right=right;
left->_parent=parent;
right->_parent=parent;
minHeap.Push(parent);
}
return minHeap.Top();
}
void _Destroy(Node *&root)
{
if(root == NULL)
return ;
Node *cur=root;
if(cur)
{
_Destroy(cur->_left);
_Destroy(cur->_right);
delete cur;
cur=NULL;
return ;
}
}
protected:
Node *_root;
};
void testHuffmanTree()
{
int a[]={0,1,2,3,4,5,6,7,8,9};
int size=sizeof(a)/sizeof(a[0]);
HuffmanTree<int> ht(a,size);
}
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
//利用仿函式的特性實現程式碼的複用性
template<class T>
struct Small
{
bool operator()(const T& l,const T& r)
{
return l < r;
}
};
template<class T>
struct Large
{
bool operator()(const T& l,const T& r)
{
return l > r;
}
};
template<class T,class Compare=Large<T>> //預設是建小堆
class Heap
{
public:
Heap()
{}
Heap(const T *a,int size)
{
assert(a);
_a.reserve(size);
for (int i=0;i<size;++i)
{
_a.push_back(a[i]);
}
//建堆的時候從倒數第一個非葉子結點開始.
for (int j=(size-2)/2;j>=0;--j)
{
_AdjustDown(j);
}
}
void Push(const T& x)
{
_a.push_back(x);
_AdjustUp(_a.size()-1);
}
void Pop()
{
assert(!_a.empty());
swap(_a[0],_a[_a.size()-1]);
_a.pop_back();
_AdjustDown(0);
}
size_t Size()
{
return _a.size();
}
bool Empty()
{
return _a.empty();
}
const T& Top()const
{
assert(!_a.empty());
return _a[0];
}
void Display()
{
for (size_t i=0;i<_a.size();++i)
{
cout<<_a[i]<<" ";
}
cout<<endl;
}
protected:
void _AdjustDown(int root)
{
int parent=root;
size_t child=2*root+1;
while (child < _a.size())
{
Compare com;
//child指向左右孩子中較大的那個數
//if (child+1 < _a.size()
// && _a[child+1] > _a[child])
if(child+1 < _a.size()
&& com(_a[child+1],_a[child]))
{
child++;
}
//if (_a[child] > _a[parent])
if(com(_a[child],_a[parent]))
{
swap(_a[child],_a[parent]);
parent=child;
//初始的child預設指向左孩子
child=2*parent+1;
}
else
break;
}
}
void _AdjustUp(int child)
{
while (child > 0)
{
int parent=(child-1)/2;
Compare com;
//if (_a[child] > _a[parent])
if(com(_a[child],_a[parent]))
{
swap(_a[child],_a[parent]);
child=parent;
}
else
//插入的資料比父節點的資料域小
break;
}
}
protected:
vector<T> _a;
};
//利用堆解決優先順序佇列的問題
template<class T,class Compare=Large<T>>
class PriorityQueue
{
public:
PriorityQueue(int *a,int size)
:_pq(a,size)
{}
void Push(const T& x)
{
_pq.Push(x);
}
void Pop()
{
_pq.Pop();
}
const T& Top()const
{
return _pq.Top();
}
void Display()
{
_pq.Display();
}
protected:
Heap<T,Compare> _pq;
};
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include "HuffmanTree.h"
typedef long long Type;
struct CharInfo
{
unsigned char _ch; //出現的字元
Type _count; //統計次數
string _code; //Huffman編碼
CharInfo(Type count=0)
:_ch(0)
,_count(count)
,_code("")
{}
//過載對應的操作符
CharInfo operator + (const CharInfo& fc)const
{
return CharInfo(_count + fc._count);
}
bool operator != (const CharInfo fc)const
{
return _count != fc._count;
}
bool operator < (const CharInfo& fc)const
{
return _count < fc._count;
}
};
class FileCompress
{
public:
//預設的建構函式
FileCompress()
{
for(size_t i=0;i<256;++i)
{
_infos[i]._ch=i;
}
}
string Compress(const char *filename)
{
assert(filename);
FILE *pf=fopen(filename,"rb");
assert(pf);
unsigned char ch=fgetc(pf);
//統計字元出現的次數
while (!feof(pf))
{
_infos[ch]._count++;
ch=fgetc(pf);
}
//以該字元出現的次數構建一顆HuffmanTree.
CharInfo invalid; //非法值
HuffmanTree<CharInfo> ht(_infos,256,invalid);
//生成Huffman編碼
string code;
_CreatHuffmanCode(ht.GetRoot(),code);
//_CreatHuffmanCode(ht.GetRoot());
//壓縮檔案
fseek(pf,0,SEEK_SET); //回到檔案頭
string compressfile=filename;
compressfile += ".compress"; //壓縮後的檔名
FILE *fin=fopen(compressfile.c_str(),"wb");
assert(fin);
size_t pos=0; //記錄位數
unsigned char value=0;
ch=fgetc(pf);
while (!feof(pf))
{
string &code=_infos[ch]._code;
for (size_t i=0;i<code.size();++i)
{
value <<= 1;
if(code[i] == '1')
value |= 1;
else
value |= 0; //do-nothing
++pos;
if(pos == 8) //滿一個位元組
{
fputc(value,fin);
value=0;
pos=0;
}
}
ch=fgetc(pf);
}
if(pos) //解決不足8位的情況.
{
value <<= (8-pos);
fputc(value,fin);
}
//配置檔案--便於重建Huffman樹
string configfilename=filename;
configfilename += ".config";
FILE *finconfig=fopen(configfilename.c_str(),"wb");
assert(finconfig);
string line;
char buff[128];
for (size_t i=0;i<256;++i)
{
//一行一行的讀
if (_infos[i]._count)
{
line += _infos[i]._ch;
line += ",";
line += _itoa(_infos[i]._count,buff,10);
line += "\n";
//fputs(line.c_str(),finconfig);
fwrite(line.c_str(),1,line.size(),finconfig);
line.clear();
}
}
fclose(pf);
fclose(fin);
fclose(finconfig);
return compressfile;
}
string UnCompress(const char *filename)
{
assert(filename);
string configfilename=filename;
size_t index=configfilename.rfind(".");
configfilename=configfilename.substr(0,index);
configfilename += ".config";
FILE *foutconfig=fopen(configfilename.c_str(),"rb");
assert(foutconfig);
string line;
//讀取配置檔案--獲取字元出現的次數
unsigned char ch=0;
while (ReadLine(foutconfig,line))
{
if(line.empty())
{
line += '\n';
continue;
}
//讀到空行
ch=line[0];
_infos[ch]._count = atoi(line.substr(2).c_str());
line.clear();
}
//構建Huffman樹
CharInfo invalid;
HuffmanTree<CharInfo> hft(_infos,256,invalid);
//根結點的權值也就是字元出現的次數總和
HuffmanTreeNode<CharInfo> *root=hft.GetRoot();
Type charcount=root->_weight._count;
//解壓縮
string uncompressfilename=filename;
index=uncompressfilename.rfind(".");
uncompressfilename=uncompressfilename.substr(0,index);
uncompressfilename += ".uncompress";
FILE *fin=fopen(uncompressfilename.c_str(),"wb");
assert(fin);
//由壓縮檔案還原檔案
string compressfilename=filename;
FILE *fout=fopen(compressfilename.c_str(),"rb");
assert(fout);
HuffmanTreeNode<CharInfo> *cur=root;
int pos=7;
ch=fgetc(fout);
while (charcount > 0)
{
while (cur)
{
if(cur->_left == NULL && cur->_right == NULL)
{
//葉子結點
fputc(cur->_weight._ch,fin);
cur=root;
--charcount;
if (charcount == 0) //所有的字元都處理完成
break;
}
if (ch & (1 << pos)) //檢查字元的每個位
cur=cur->_right; //1向右走
else
cur=cur->_left; //0向左走
--pos;
if(pos < 0) //一個位元組解壓完成
{
ch=fgetc(fout);
pos=7;
}
}
}
fclose(foutconfig);
fclose(fin);
fclose(fout);
return uncompressfilename;
}
//讀取一行字元並放在line中
bool ReadLine(FILE *fout,string& line)
{
int ch=fgetc(fout);
if(ch == EOF)
return false;
while (ch != EOF && ch != '\n')
{
line += ch;
ch=fgetc(fout);
}
return true;
}
protected:
//遞迴的方法求HuffmanTreeCode
void _CreatHuffmanCode(HuffmanTreeNode<CharInfo> *root,string &code)
{
if(root == NULL)
return ;
_CreatHuffmanCode(root->_left,code+'0');
_CreatHuffmanCode(root->_right,code+'1');
if(root->_left == NULL && root->_right == NULL) //葉子結點
{
_infos[root->_weight._ch]._code=code;
}
}
//非遞迴求HuffmanTreeCode
void _CreatHuffmanCode(HuffmanTreeNode<CharInfo> *root)
{
if(root == NULL)
return ;
_CreatHuffmanCode(root->_left);
_CreatHuffmanCode(root->_right);
if(root->_left == NULL && root->_right == NULL) //葉子結點
{
string& code=_infos[root->_weight._ch]._code;
HuffmanTreeNode<CharInfo> *cur=root;
HuffmanTreeNode<CharInfo> *parent=root->_parent;
while (parent)
{
if(parent->_left == cur)
code.push_back('0'); //左0
else
code.push_back('1'); //右1
cur=parent;
parent=cur->_parent;
}
//編碼是從根到葉子結點的,需要逆置
reverse(code.begin(),code.end());
}
}
protected:
CharInfo _infos[256];
};
void testFileCompress()
{
FileCompress fc;
cout<<"開始壓縮"<<endl;
cout<<"壓縮用時:";
int start=GetTickCount();
fc.Compress("2.png"); //input input.BIG 3.mp3
int end=GetTickCount();
cout<<end-start<<endl;
cout<<"開始解壓"<<endl;
cout<<"解縮用時:";
start=GetTickCount();
fc.UnCompress("2.png.compress"); //input.compress input.BIG.compress 3.mp3
end=GetTickCount();
cout<<end-start<<endl;
}
void testFileUncompress()
{
FileCompress fc;
cout<<"開始解壓"<<endl;
cout<<"解縮用時:";
int start=GetTickCount();
fc.UnCompress("2.png");
int end=GetTickCount();
cout<<end-start<<endl;
}
經過測試這個小專案已經可以壓縮並還原一些檔案了,目前還沒有發現什仫大的Bug,如果有童鞋發現請第一時間告訴我哦...