1. 程式人生 > >基於哈夫曼演算法的檔案壓縮軟體

基於哈夫曼演算法的檔案壓縮軟體

資料結構課設(一)

作業要求

1、設計並實現一個使用哈夫曼演算法對檔案進行壓縮的工具軟體。
2、通過命令列引數指定操作模式(壓縮/解壓)、原始檔名、目標檔名。
3、壓縮操作將原始檔按位元組讀入並統計位元組頻率,生成位元組的哈夫曼編碼,將編碼樹和用哈夫曼編碼對位元組重新編碼後的結果儲存到目標檔案中。
4、解壓操作將原始檔中的編碼樹讀出,用編碼樹對後續編碼部分進行解碼操作,將解碼結果儲存到目標檔案中。

主要問題

基於哈夫曼演算法的檔案壓縮要考慮的幾個主要問題:
1、命令列引數的設計與識別。帶引數的main函式應該怎麼用;設計合適的命令列引數來表示要執行的操作、原始檔名和目標檔名。
2、檔案操作。C/C++的檔案流庫是不一樣的,選擇你喜歡的檔案流庫,學習怎麼以正確的方式開啟檔案、逐位元組讀取檔案、寫入檔案。
3、設計壓縮檔案的格式。壓縮檔案頭部應該是儲存生成的哈夫曼樹,後續才是原始檔的壓縮(編碼)結果。這裡的關鍵是檔案頭的設計。
4、統計位元組頻率,建立哈夫曼樹,構造位元組哈夫曼編碼。
5、對原始檔逐位元組進行編碼,並重新組合成位元組。
6、解析壓縮檔案的檔案頭,還原哈夫曼樹。
7、對壓縮檔案的編碼部分進行解碼。

決定按問題順序來一個個解決。

1.命令列引數的設計與識別

由於老師給出了demo程式碼,我就直接拿過來用了

int main(int argc, char *argv[])
{
    cout << "命令列引數:" << endl;
    for (int i = 0; i < argc; i++)
    {
        cout << "arg[" << i << "]: " << argv[i] << endl;
    }

    // 檢查命令列引數,如果沒有讀到所需要的命令列引數,提示使用說明
    if (argc != 4)
    {
        ShowHelp();
    }
    else if (stricmp(argv[1], "z") == 0)
    {
        cout << "Zip " << argv[2] << " to " << argv[3] << " ..." << endl;
        Zip();
    }
    else if (stricmp(argv[1], "e") == 0)
    {
        cout << "Unzip " << argv[2] << " to " << argv[3] << " ..." << endl;
        Unzip();
    }
    else
    {
        ShowHelp();
    }
    return 0;
}

演示
在這裡插入圖片描述

2.檔案操作

這裡用的是C++的iostream和fstream檔案流庫。用二進位制的方式逐位元組讀取檔案,可以讀入文字圖片或者音訊等各種格式。

ifstream instr("in.txt", ios::in | ios::binary);//二進位制方式
if (!instr)
    {
        cerr << "Open in.txt failure." << endl;
        return 0;
    }
ofstream outstr("out.txt", ios::out | ios::binary);
if (!outstr)
    {
        cerr << "Create out.txt failure." << endl;
        instr.close();
        return 0;
    }
    char ch;
    while (instr.get(ch))
    {
        outstr.put(ch);
    }
    instr.close();
    outstr.close();
    return 0;

3.設計壓縮檔案的格式(重點)

這一步就是壓縮檔案的關鍵了,決定你壓縮檔案最後的大小,如果設計不合理的話壓縮率和壓縮速度就會極不理想(比如說我的就是),壓縮完之後的壓縮檔案可能比原檔案還要大。
首先壓縮檔案需要存你的哈夫曼樹,然後就是你處理過的檔案的哈夫曼編碼。
檔案壓縮成編碼這一步其實很簡單,就是把原先的檔案通過你構建的哈夫曼轉成01序列的哈夫曼編碼,然後把這些編碼以二進位制的形式存進壓縮檔案就行了。我是每8個位元組轉成一個unsigned char,然後存入檔案。解壓縮的時候再把這個還原成哈夫曼編碼的形式就行了。
如果只需要存哈夫曼編碼檔案,不需要對應的哈夫曼樹的話,那麼大部分檔案是可以壓縮的更小的。但是,通常我們解壓縮是隻需要對壓縮檔案進行操作的,如果壓縮檔案裡只存有哈夫曼編碼的資訊,那麼我們是無法進行解碼的。因此我們必須存進對應的哈夫曼樹,這樣才能解碼還原成原檔案。
不過直接將整棵樹全部存進去會使你的壓縮檔案變得極其大,對於原本體積小的檔案比如只有幾十位元組的txt文字來說,壓縮完之後一下子就變成幾百位元組的東西了。由於我一開始沒考慮到這令人窒息的情況,等我全部寫完已經提不起興致去改了,最後做出來的程式對比較大的檔案才能起到一定的壓縮效果。

string Str(string s)//將01二進位制數轉換成字元
{
    map<int,int> change;
    change[7]=1;
    change[6]=2;
    change[5]=4;
    change[4]=8;
    change[3]=16;
    change[2]=32;
    change[1]=64;
    change[0]=128;
    unsigned char c;
    int n=0;
    string t;
    for(int i=0;i<s.size();i++)
    {
        if(i%8==0&&i!=0)
        {
            c=n;
            t+=c;
            n=0;
        }
        if(s[i]=='1')
            n+=change[i%8];
    }
    c=n;
    t+=c;
    return t;
}

int Compresses(vector<Hfm> T,string s,char *file)
{
    cout<<"Zipping..."<<endl;
    ofstream out(file,ios::out|ios::binary);
    int num=T.size();
    out.write((char*)&num,sizeof(int));//樹的大小
    out.write((char*)&T[0],T.size()*sizeof(Hfm));//哈夫曼樹
    int num2=s.size();
    out.write((char*)&num2,sizeof(int));//檔案的大小
    out<<Str(s);//哈夫曼編碼
    cout<<"Zipped!"<<endl;
    out.close();
    return 0;
}

在這裡我存了樹的大小,然後存了樹,然後是檔案的大小,然後是哈夫曼編碼。(極不可取,不要借鑑)

4.統計位元組頻率,建立哈夫曼樹,構造位元組哈夫曼編碼。

這一步沒什麼好說的,會哈夫曼樹就行了。比起找了半天都找不到如何二進位制存檔案來說,這個在網上隨便一搜就能找到。

用vector<Hfm> T存的哈夫曼樹,用map<char,int>統計各個二進位制字元出現的次數。
struct Hfm
{
    char name;//位元組名
    int val;//權值,也就是出現的次數
    int parent,lchild,rchild;
     /*Hfm(char n,int v,int p,int l,int r)
    {
        name=n;;
        val=v;
        parent=p;
        lchild=l;
        rchild=r;
    }
    void Show()
    {
        cout<<val<<" "<<parent<<" "<<lchild<<" "<<rchild<<endl;
    }*/
};
int CreatTree(map<char,int> arr,vector<Hfm> &T)
{
    int len=arr.size();
    map<char,int>::iterator iter;
	for (iter = arr.begin(); iter != arr.end(); iter++) {
            Hfm tt;
    tt.name=iter->first;
    tt.val=iter->second;
    tt.parent=-1;
    tt.lchild=-1;
    tt.rchild=-1;
    T.push_back(tt);
	}
    for(int i=0;i<len-1;i++)
    {
        int m,n;
        Select(T,m,n);
        Hfm tt;
    tt.name=-1;
    tt.val=T[m].val+T[n].val;
    tt.parent=-1;
    tt.lchild=m;
    tt.rchild=n;
    T.push_back(tt);
    //T.push_back(Hfm(-1,T[m].val+T[n].val,-1,m,n));
    T[m].parent=T.size()-1;
    T[n].parent=T.size()-1;
    }
    return 0;
}

5.對原始檔逐位元組進行編碼,並重新組合成位元組。

int Hfmcode(vector<Hfm> T,map<char,string> &Code)
{
    for(int i=0;i<T.size();i++)
    {
        string s;
        stack<char> c;
        if(T[i].lchild==-1&&T[i].rchild==-1)
        {
            int j=i;
            while(T[j].parent>=0){
                    int temp=j;
            j=T[j].parent;
            if(T[j].lchild==temp)
            {
                c.push('0');
            }
            else c.push('1');
            }
        }
        else break;
        while(!c.empty())
        {
            s=s+c.top();
            c.pop();
        }
        Code[T[i].name]=s;
    }
    return 0;
}
int main()
{
	vector<Hfm> T;
    map<char,string> Code;
    CreatTree(arr,T);
    Hfmcode(T,Code);
    string Zip;//哈夫曼編碼
    for(int i=0;i<s.size();i++)
    {
        Zip+=Code[s[i]];
    }
}

6.解壓縮

老師給的6、7步我就一塊說了,我的壓縮效果太差,不好意思寫了。

string Bin(string t,int n)//將0-256的數轉換成8位二進位制
{
    map<int,int> change;
    change[7]=1;
    change[6]=2;
    change[5]=4;
    change[4]=8;
    change[3]=16;
    change[2]=32;
    change[1]=64;
    change[0]=128;
    string s;
    for(int j=0;j<t.size();j++)
    {
        unsigned char c=t[j];
        int a=c;
        for(int i=0;i<8;i++)
        {
            if(a>=change[i])
            {
                a-=change[i];
                s+='1';
            }
            else s+='0';
        }
    }
    int sur=8-n%8;
    while(sur--)
    {
        s.erase(s.end()-1);
    }
    return s;
}

int Uncode(vector<Hfm> T,string t,int sur,char *file)
{
    ofstream Un(file,ios::out|ios::binary);
    int r=T.size()-1;
    int temp=r;
    string s=Bin(t,sur);
    for(int i=0;i<s.size();i++)
    {
        if(s[i]=='0')
        {
            temp=T[r].lchild;
            if(T[temp].lchild==-1&&T[temp].rchild==-1)
            {
                Un.put(T[temp].name);
                r=T.size()-1;
            }
            else r=temp;
        }
        else
        {
            temp=T[r].rchild;
            if(T[temp].lchild==-1&&T[temp].rchild==-1)
            {
                Un.put(T[temp].name);
                r=T.size()-1;
            }
            else r=temp;
        }
    }
    Un.close();

    return 0;
}

int Uncode2(char *file1,char *file2)
{
    ifstream ss(file1,ios::in|ios::binary);
    int f;
    ss.read((char*)&f,sizeof(int));
    vector<Hfm> T0(f);
    ss.read((char*)&T0[0],T0.size()*sizeof(Hfm));
    int f2;
    ss.read((char*)&f2,sizeof(int));
    char x;
    string t;
    while(ss.get(x))
    {
        t+=x;
    }
    ss.close();
    Uncode(T0,t,f2,file2);
    cout<<"Unzipped!"<<endl;
    return 0;
}

結束

這個演算法寫的不太好,不過也不想回頭去改了,還有兩個課設沒寫,而且期末考試還沒複習。
大概總結一下,其實這個演算法算是比較簡單的,難點主要在如何進行二進位制檔案的操作,這些格式轉換的亂七八糟的,網上找又不好找,沒有統一的方法,只有自己慢慢摸索。經過這幾天的學習,好歹熟悉了fstream/ifstream/ofstream這幾個檔案流的使用。

原始碼

以後原始碼都上傳到我的GitHub上。(假裝自己寫了很多東西的樣子)