1. 程式人生 > >基於Huffman編碼的C語言解壓縮檔案程式

基於Huffman編碼的C語言解壓縮檔案程式

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<math.h>
            //極大值用於生成Huffman樹
#define MAXSIZE 100000000
            //用於生成相應葉子節點Huffman編碼的二維字元陣列
typedef char* HCode;
            //Huffman樹節點
typedef struct node
{
    int weight;
    int data;
    int parent,lchild,rchild;
}Node;
            //count 葉子節點數的計算  sum_bit 記錄被壓縮檔案編碼後的編碼總長度
int sum_bit,count;
            //Huffman葉子節點,最多不過256個(以位元組為單位)
Node huffmanNode[260];
            //解壓縮的時候記錄每次讀取到編碼(0....1..)
int num[8];
            //對應詞頻資訊表的資訊,用ASCII值表示
int code[260];
           //二維字元陣列
HCode *HC;
            //統計詞頻時用於查詢是否已經記錄過,記錄過的話返回下標,沒有則返回0
int isInNode(int value)
{
    int i = 1;
    for(;i<=count;i++)
    {
        if(huffmanNode[i].data == value)
        {
            return i;
        }
    }
    return 0;
}
            //獲取檔案詞頻,記錄在Node huffmanNode[260]的節點陣列當中
void  calWeight(char *file)
{
    count = 0;
    FILE *f;
    int a;
            //以二進位制方式開啟檔案,為了讀取到換行符
    f = fopen(file,"rb");
    if(f == NULL)
    {
        printf("檔案不存在!開啟失敗!");
        return ;
    }
    while(!feof(f))
    {
        a = fgetc(f);
        if(a==EOF) break;
        if(!isInNode(a))   //count從1開始計數
        {
            count++;
            huffmanNode[count].weight = 1;
            huffmanNode[count].data   = a;
        }
        else
        {
            int i = isInNode(a);
            huffmanNode[i].weight++;
        }
    }
    fclose(f);
}


/*得到待壓縮檔案的總位元組數,權值為幾就代表著有多少個位元組*/
int getSumBytes()
{
    int i=1;
    int result = 0;
    for(;i<=count;i++)
    {
        result +=huffmanNode[i].weight;
    }
    return result;
}

//獲取壓縮後文件的總bit數
int getSumBits()
{
    int i = 1;
    int result = 0;
    for(;i<=count;i++)
    {
        result+=huffmanNode[i].weight * strlen(HC[i]);
    }
    return result;
}

            //建立huffman樹 根據huffman樹的特性,具有n個節點的huffman樹的具有2n-1個節點
            //n值由全域性變數count值來確定,該函式主要用來初始化Huffman樹的所有節點資訊
void  createHufmanTree(Node * huffmanTree)
{
    int m = 2*count - 1;
    int m1,m2,x1,x2,i,j;
            //初始化結點資訊,從1--count這count個節點資訊為葉子節點的資訊
    for(i=1;i<=count;i++)
    {

        huffmanTree[i].data = huffmanNode[i].data;
        huffmanTree[i].lchild = -1;
        huffmanTree[i].rchild = -1;
        huffmanTree[i].parent = -1;
        huffmanTree[i].weight = huffmanNode[i].weight;
    }
            //從count---2*count-1這些節點首先初始化為空
    for(;i<=m;i++)
    {
        huffmanTree[i].data = 0;    huffmanTree[i].weight = 0;
        huffmanTree[i].lchild = -1; huffmanTree[i].rchild = -1;
        huffmanTree[i].parent = -1;
    }
            //構造huffman樹,按照huffman樹構建原理
    for(i=count+1;i<=m;i++)
    {
        /*m2,m1分別儲存倒數第二小的權值和倒數第一小的權值
          x2,x1分別儲存倒數第二小的下標和倒數第一小的下標*/
        m1 = m2 = MAXSIZE;
        x1 = x2 = 0;
        for(j=1;j<i;j++)
        {
            if(huffmanTree[j].parent == -1&&huffmanTree[j].weight <m1)
            {
                m2 = m1;                    x2 = x1;
                m1 = huffmanTree[j].weight; x1 = j;
            }
            else if(huffmanTree[j].parent == -1&&huffmanTree[j].weight<m2)
            {
                m2 = huffmanTree[j].weight;
                x2 = j;
            }

        }
         /*合併成一顆新的樹*/
            huffmanTree[x1].parent = i; huffmanTree[x2].parent = i;
            huffmanTree[i].lchild = x1; huffmanTree[i].rchild = x2;
            huffmanTree[i].weight = m1+m2;
    }
}

/*字元編碼,從構建好的Huffman樹當中讀取每個葉子節點的huffman編碼,並將葉子節點的資訊放入到code[]中*/
HCode * getHuffmanCode(Node * huffmanTree,HCode *HC,int code[])
{
    int i = 1,c,start,f;
    //構造了字元編碼的字元陣列共有count+1個 通過讀取一個複製一個的方式完成編碼獲取

    char * cd = (char *)malloc((count+1)*sizeof(char));
    //還是這個問題的
    cd[count] = '\0';
    for(;i<=count;i++)
    {
        start = count;
        for(c=i,f=huffmanTree[i].parent;f!=-1;c=f,f=huffmanTree[f].parent)
        {
            if(huffmanTree[f].lchild == c)  cd[--start] = '0';
            else cd[--start] = '1';
        }
        //為每個字元陣列分配相應的數量 由於範圍的問題要仔細考慮的
        HC[i] = (char *)malloc((count+1-start)*sizeof(char));
        //引數均為char *
        strcpy(HC[i],&cd[start]);
        code[i] = huffmanTree[i].data;
    }
    return HC;
}
  /*
  將編碼表寫入預設檔案當中,並在結尾存入葉子節點數(count)與壓縮後文件的總bit數
   1111000  27
   ...........
   ...........
   #sum_bit##count#
  */
void freToFile(int code[],HCode *HC)
{
    int i;
    //開啟預設檔案
    FILE *fe = fopen("C:\\dic.txt","wb");
    //將編碼資訊和葉子節點資訊寫入詞典
    for(i=1;i<=count;i++)
    {
      fprintf(fe,"%s %d\n",HC[i],code[i]);
    }
    char c = '#';
    //寫入sum_bit
    fprintf(fe,"%c",c);
    fprintf(fe,"%d",getSumBits());
    fprintf(fe,"%c",c);
    //寫入count
    fprintf(fe,"%c",c);
    fprintf(fe,"%d",count);
    fprintf(fe,"%c",c);

    fclose(fe);
}
//由於詞頻表是按照字串方式儲存的葉子節點資訊,讀取出來的字串需要轉換成int值再使用
//其中需要使用pow函式,由於pow函式有精度損失,自己寫了一個使用
int powmy(int a,int b)
{
    if(b==0) return 1;
    int i = 0;
    int result = 1;
    for(;i<b;i++)
    {
        result *=a;
    }
    return result;
}

/*從編碼表檔案讀取相應資訊以用來解壓檔案,讀取資訊包括編碼和葉子資訊*/
HCode* freFromFile(int code[],HCode *HC)
{
    int i;
    FILE *fe = fopen("C:\\dic.txt","rb");
    if(fe==NULL)
    {
        printf("詞典檔案不存在!");
        return NULL;
    }
        int k;
        int num[10];
        int m;
        int flag = 0;
        char * cd = (char *)malloc((256+1)*sizeof(char));
        //讀取一個位元組
        char c = fgetc(fe);
        for(i=1;flag!=1;i++)
        {
            //如果讀取到#號鍵,就跳出迴圈,繼續讀取sum_bit和count值
            if(c=='#') break;
            //每一行的讀取直到讀到空格,然後就完成了一條huffman編碼的讀取
            int j = 0;
            while(c!=' ')
            {
                cd[j++] = c;
                c = fgetc(fe);
            }
            cd[j] = '\0';

            //將讀取到的huffman編碼存入相應的二維字元陣列當中去
            HC[i] = (char *)malloc((j+1)*sizeof(char));
            strcpy(HC[i],&cd[0]);
            //下面直到讀取到空格鍵為止,讀取huffman葉子節點資訊,讀取到的是字元,需要轉換成int值
            c = fgetc(fe);

            k = 0;
            while(c!='\n')
            {
                num[k++] = c-'0';
                c = fgetc(fe);
            }
            code[i] = 0;
            m = 0;
            //轉換成int值,存入code[]陣列當中
            for(k=k-1;k>=0;k--)
            {
                code[i]+=num[k]*powmy(10,m);
                m++;
            }
            //繼續向下讀取
            c = fgetc(fe);
        }
        //獲取壓縮檔案的總bit數,以用來判斷最後一次讀取的是不是足夠8位
        c = fgetc(fe);
        k = 0;
        while(c!='#')
        {
            num[k++] = c-'0';
            c = fgetc(fe);
        }
        //同樣將讀取到的char轉換成int
        m = 0;
        sum_bit = 0;
        for(k=k-1;k>=0;k--)
        {
            sum_bit+=(num[k]*powmy(10,m));
            m = m + 1;
        }

        c = fgetc(fe);  c = fgetc(fe);//頭一個讀取#,後一個才開始讀取資料
        k = 0;
        while(c!='#')
        {
            num[k++] = c-'0';
            c = fgetc(fe);
        }
        //將讀取到的char轉換成int
        m = 0;  count = 0;
        for(k=k-1;k>=0;k--)
        {
            count+=num[k]*pow(10,m);
            m++;
        }
        fclose(fe);
        return HC;
}


/*壓縮檔案*/
void compress_file(char* file1,char*file2)
{
    int i,sum = 0,flag = 0,j,k = 0;
    //陣列開設的不夠大是最後的一個bug的成因,因為有可能這個Huffman編碼很長很長
    int eight[1000];
    memset(eight,0,1000*sizeof(int));
    FILE *fo = fopen(file1,"rb");
    FILE *fw = fopen(file2,"wb");
    if(fo == NULL||fw == NULL)
    {
        printf("檔案讀取失敗!");
        return;
    }
    //統計已經壓縮的位元組總數,用於計算壓縮百分比
    int aa = 0;
    int sum_bytes = getSumBytes();
    while(!feof(fo))
    {
        sum = 0;
        int a = fgetc(fo);
        //每次讀取一個位元組就+1
        aa++;
        //讀取了一個位元組之後就與編碼表進行比較,查詢對應的編碼
        for(i=1;i<=count;i++)
        {
            if(code[i] == a)
            {
                //flag作為計數器,當湊夠8位之後就作為一個位元組寫入壓縮檔案
                flag+=strlen(HC[i]);
                int len = strlen(HC[i]);
                //flag 小於8的時候繼續累加,直到湊夠8個
                if(flag<8)
                {
                    for(j=0;j<len;j++)
                    eight[k++] = HC[i][j]-'0';/*我們儲存的是字串,是多少就是多少*/
                }
                //當flag>=8的時候,將8位寫進壓縮檔案,同時將剩餘的沒有寫入的huffman編碼重新移到
                //eight【】陣列前面去,同時修改flag
                else if(flag>=8)
                {
                    //將匹配到的huffman編碼寫進8位陣列,直到k值為8,k值始終代表現在eight【】陣列的長度
                    for(j=0;k<8;j++)
                      eight[k++] = HC[i][j]-'0';
                    //將匹配到的huffman編碼的沒有完全寫進去的新增到後面。
                    for(;j<len;j++)
                      eight[k++] = HC[i][j]-'0';
                    //計算8位對應的int值,寫入檔案
                    sum+=eight[0]*128+eight[1]*64+eight[2]*32+eight[3]*16+eight[4]*8
                        +eight[5]*4+eight[6]*2+eight[7]*1;
                    //前8為置0
                    for(j=0;j<8;j++)
                       eight[j] = 0;
                    //將後面的移植到前面去
                    for(j=8;j<k;j++)
                      eight[j-8] = eight[j];
                    //重置flag與k
                    k = flag = j-8;
                    //寫進檔案
                    char c = sum;
                    fputc(c,fw);

                    if(aa%1000==0)
                    {
                        printf("\r正在進行壓縮,請稍等……%6.2f%%",(double)aa/sum_bytes*100.0);
                    }
                    fflush(fw);
                    i = count+1;
                }
            }
        }
    }
    aa = sum_bytes;
    printf("\r正在進行壓縮,請稍等……%6.2f%%",(double)aa/sum_bytes*100.0);
    printf("壓縮成功!");
    /*考慮到最後可能沒有湊夠八位的情況*/
    if(flag)
    {
        sum+=eight[0]*128+eight[1]*64+eight[2]*32+eight[3]*16+eight[4]*8
                        +eight[5]*4+eight[6]*2+eight[7]*1;
        char c = sum;
        fputc(c,fw);
        sum_bit +=flag;
        fflush(fw);
    }
    fclose(fw);
    fclose(fo);
}

/*用於在解壓的時候將讀取到的ASCII碼轉換為二進位制數*/
int  swap(int data)
{
    int i = 0;
    while(data)
    {
        num[i++] = data%2;
        data = data/2;
    }
    return i;
}

/*進行檔案的解壓*/
void uncompress_file(char* file1,char* file2)
{

    FILE *fo = fopen(file1,"rb");
    FILE *fw = fopen(file2,"wb");
    if(fo==NULL ||fw == NULL)
    {
        printf("檔案開啟失敗!");
        return;
    }
    char str[1000];
    int i,j,k,temp = 0;
    int index;
    int sum_bit2 = sum_bit;
    //直到讀取到檔案結尾
    while(!feof(fo))
    {
       if(sum_bit2<0) break;
       //讀取一次,減去8位
       sum_bit2 -=8;
       int data = fgetc(fo);
       if(data == -1) break;
       //index用來在sum_bit2小於0的時候設定讀取為位數(也就是說最後不用讀取8位了)
       if(sum_bit2<0)
       {
            index = 0-sum_bit2;
       }
       else
       {
           index = 0;
       }
       if(data == -1) break;
       memset(num,0,sizeof(num));
       //將讀取到的data轉換成二進位制數
       swap(data);
       i = temp;
       //將轉換後的二進位制數變為字串,注意順序
       //是一位一位的往裡面填,填進去一位立即進行比較,當找到相應的資訊就調出來
       for(k=7;k>=index;i++,k--)
       {
           if(num[k])
              str[i] = '1';
            else
              str[i] = '0';

           str[i+1] ='\0';
           //查詢編碼表當中與該字串(編碼)相同的資訊,然後將葉子資訊寫入解壓檔案
           for(j=1;j<=count;j++)
           {
               if(strcmp(str,HC[j])==0)
               {
                    //將葉子資訊寫入到檔案(寫入的是int值,是該int值表示的字元)
                    fputc(code[j],fw);
                    if((sum_bit-sum_bit2)%1500==0)
                    {
                        printf("\r檔案正在解壓中,請耐心等待……%6.2f%%",(double)(sum_bit-sum_bit2)/sum_bit*100.0);
                    }

                    fflush(fw);
                    j = count+1;
                    i = -1;
               }
           }
       }
       if(i)
       {
            temp = i;
       }
       else
       {
            temp = 0;
       }
    }
    sum_bit2 = 0;
    printf("\r檔案正在解壓中,請耐心等待……%6.2f%%",(double)(sum_bit-sum_bit2)/sum_bit*100.0);
    printf("解壓成功!");
    fclose(fw);
    fclose(fo);
}

int main(int argc, char **argv)
{
   if(strcmp(argv[1],"-c")==0)
    {
                //獲取檔案的詞頻
        calWeight(argv[2]);
                //申請Huffman樹的記憶體,已經獲得葉子節點數,根據節點總數與葉子節點數的關係分配記憶體
        Node *huffmanTree = (Node *)malloc((2*count-1+1)*sizeof(Node));
                //建立Huffman樹
        createHufmanTree(huffmanTree);
                //為Huffman編碼表申請一個二維的字元陣列指標
        HC = (HCode *)malloc((count+1)*sizeof(HCode));
                //向指標賦值,getHuffmanCode()函式返回編碼表
        HC = getHuffmanCode(huffmanTree,HC,code);
                //根據編碼表HC和編碼對應的data表code壓縮檔案
        compress_file(argv[2],argv[3]);
                //將編碼存入到預設的編碼表當中(C:\\dic.txt)
        freToFile(code,HC);
    }
    else if(strcmp(argv[1],"-u")==0)
	{
                //為編碼表分配記憶體,由於不知道葉子節點數,分配257
       HC = (HCode *)malloc(257*sizeof(HCode));
                //從詞頻表當中獲取編碼
       HC = freFromFile(code,HC);
                //根據編碼表和data表解壓檔案
       uncompress_file(argv[2],argv[3]);
	}
    return 0;
}