Huffman樹壓縮程式(c實現)
一、問題描述
信源編解碼是通訊系統的重要組成部分。本實驗旨在通過程式設計實現基於哈夫曼編碼的信源編解碼演算法。程式具備以下功能:
對於給定的源文件SourceDoc.txt,
1) 統計其中所有字元的頻度(某字元的頻度等於其出現的總次數除以總字元數),包括字母(區分大小寫)、標點符號及格式控制符(空格、回車等)。
2) 按頻度統計結果生成哈夫曼編碼碼錶。
3) 基於哈夫曼碼錶進行編碼,生成對應的二進位制碼流,並輸出到檔案Encode.dat。
4) 對二進位制碼流進行哈夫曼解碼,把結果輸出到檔案DecodeDoc.txt。
5) 判斷DecodeDoc.txt與SourceDoc.txt內容是否一致,以驗證編解碼系統的正確性。
這是一個綜合應用c語言各項簡單特性並且跟資訊工程比較貼合的一個程式設計訓練題目,在c語言入門提高時還是可以作為一個比較好的訓練題目。
二、實驗主體的演算法描述
2.1資料流及位運算部分主要演算法
2.1.1中英文歸一化處理的演算法描述
中文實際上是使用GB2313格式儲存(也可以通過UTF-8儲存,但此時為3位元組),是由16位2進位制數構成,並且將其拆解成為兩個8位二進位制數(char型)後,首個char一定為負值。基於以上認識,對於中英文的處理演算法如下:
S1:從檔案中讀入一個char,判斷是否>0
S2:如果是,將其通過位運算方式(與一個a(short型)進行或(|)操作,再左移(<<)8位)暫存起來,從檔案中讀入第二個char,再將其存入暫存中(操作與之前類似,只是不進行左移),輸出暫存即可得到中文字元。
S3:如果否,直接輸出該char。
2.1.2基於位運算的輸出壓縮演算法
在得到了Huffman編碼的基礎上,要輸出到檔案Encode.dat時,考慮到Huffman時壓縮演算法,為了最大限度的保證壓縮效果,將字元一次轉化為0-1二進位制編碼,直接拼接,每8位一輸出,確保在最後一個
演算法如下:
S1:得到該字元的Huffman編碼,將每一位0-1編碼通過或運算併入暫存器H中,每並1位,計數器y++
S2:判斷y是否等於7,如果是,將H輸出至Encode.dat中,H=0,y=0;如果否,回到S1.
S3:持續執行S1及S2,直到讀完整篇文件為止。
2.2連結串列及排序部分主要演算法
連結串列排序選用了快速排序。
單鏈表的快排序和陣列的快速排序基本思想相同,同樣是基於劃分,但是又有很大的不同:
單鏈表不支援基於下標的訪問,故把待排序的連結串列拆分為2個子連結串列。為了簡單起見,選擇連結串列的第一個節點作為基準,然後進行比較,比基準小得節點放入左面的子連結串列,比基準大的放入右邊的子連結串列。在對待排序連結串列掃描一遍之後,左邊子連結串列的節點值都小於基準的值,右邊子連結串列的值都大於基準的值,然後把基準插入到連結串列中,並作為連線兩個子連結串列的橋樑。然後分別對左、右兩個子連結串列進行遞迴快速排序,以提高效能。
但是,由於單鏈表不能像陣列那樣隨機儲存,和陣列的快排序相比較,還是有一些需要注意的細節:
1、支點的選取,由於不能隨機訪問第K個元素,因此每次選擇支點時可以取待排序那部分連結串列的頭指標。
2、遍歷連結串列方式,由於不能從單鏈表的末尾向前遍歷,因此使用兩個指標分別向前向後遍歷的策略實效,
事實上,可以採用一趟遍歷的方式將較小的元素放到單鏈表的左邊。具體方法為:
1)定義兩個指標pslow,pfast,其中pslow指向單鏈表的頭結點,pfast指向單鏈表頭結點的下一個結點;
2)使用pfast遍歷單鏈表,每遇到一個比支點小的元素,就令pslow=pslow->next,然後和pslow進行資料交換。
3)交換資料方式,直接交換連結串列資料指標指向的部分,不必交換連結串列節點本身。根據快速排序的基本思想,連結串列快速排序的時間複雜度為O(nlog(n))
2.3Huffman樹及壓縮解碼部分主要演算法
2.3.1樹結構
主樹根左子樹為哈夫曼樹,右子樹為二叉排序樹。二叉排序樹除根節點外都由哈夫曼樹的葉子節點重排生成。(詳見 效率分析與加速策略,排序樹生成)
2.3.2樹葉節點結構
資料域:
包含整形的字元,頻數,以及陣列形式的密碼。-1標示字元為空,密碼陣列中以2作為結尾標示
指標域:
包括3個指標,父指標指向節點在哈夫曼樹中的父節點(哈夫曼樹與二叉排序樹的父指標指向主根),兩個葉指標指向各自的子樹。
2.3.3樹與壓縮碼 生成
連結串列與樹的同步策略:
連結串列節點資料域中包含一個指向樹葉的指標(初值為空)。初始化樹時首先在所有連結串列節點下接入樹葉並將樹葉連線成雙向鏈,而後在尾端接入一個空樹葉並記錄為二叉排序樹根,再進行後續操作。
哈夫曼樹生成演算法(基於初始連結串列增序):
S1. 建立,初始化並連線新連結串列節點N與樹葉節點L。取有頭連結串列頭後兩位資料N1、N2,對應樹葉節點為L1、L2。
S2. 將L1、L2父節點置為L,L右子樹置為L1,左子樹置為L2。
S3. 將頭節點的下一位置為N2的下一位,回收N1、N2,將N插入連結串列
S4. 當連結串列長度(不含頭)大於等於1時,反覆呼叫S1~S3
S5. 記錄連結串列節點對應的葉子節點,即為哈夫曼樹根
S6. 生成空葉子節點,右子樹與二叉排序樹根連線,左子樹與哈夫曼樹根連線。即為主根root。
壓縮碼生成:
S1.取root的右子樹,向左遍歷各節點並執行S2~S4
S2.記錄父節點,若該節點為右子樹,記錄1;否則,記錄0
S3.迴圈S2. 直到父節點為哈夫曼樹樹根(root的左子樹)
S4.將記錄的反碼反向錄入葉子資料域的密碼陣列中
排序樹生成:
S1.取排序樹樹根(root的右子樹),向左遍歷各節點並執行S2~S3
S2.記錄下一節點,並將此節點的左右子樹置空。
S3.按二叉排序樹插入規則,以字元對應的整形資料位鍵值,將此節點插入排序樹
2.3.4壓縮解碼演算法
壓縮:
搜尋排序樹,並輸出對應壓縮碼即可
解碼:
迴圈讀取壓縮檔案,在哈夫曼樹中,0向左、1向右,遇到樹葉即輸出字元返回哈夫曼樹根
2.3.5效率分析與加速策略
生成效率分析:
對有效長度為n的連結串列,分析如下
同步資料,時間複雜度為n
生成樹,時間複雜度為n-1
生成壓縮碼,生成單個密碼時間複雜度為O(log(n)),故,總密碼生成效率為O(n*log(n))
生成排序樹,由於字元按頻數排序,故字元碼可視為基本隨機。因此,易知平均時間複雜度為O(n*log(n))
總效率為 n+n-1+2*O(n*log(n))=O(n*log(n))
壓縮解碼效率分析:
對總長為m,生成頻數連結串列有效長度為n的文字
壓縮效率:
對單字的平均效率為二叉排序樹效率,平均為O(log(n)),故總效率為O(m*log(n))。
解碼效率:
對單字的解密效率同樣為O(log(n)),故總效率為O(m*log(n))
總效率分析:
由於m>>n本演算法總效率為O(m*log(n)),遠高於無二叉排序樹加速時的期望效率O(m*n)。當m、n有至少一個較大時有較大提升,m、n都較大時有機可觀的效率提升,而僅多佔用2個額外空間,優勢明顯。
加速策略:
本演算法通過對哈夫曼樹底層重排,以2個額外空間為代價,藉助二叉排序樹極大的提升了壓縮效率,同時維持解碼效率不變。實現了加速以及效能提升。
三、實現程式碼(c語言)
3.1 link.h
#include<stdio.h> #include<stdlib.h> #include<malloc.h> #ifndef LINK_H #define LINK_H #endif typedef struct Leaf { int word,p; //word 字 p 頻數 char code[50];//左 0 右 1 struct Leaf *par,*l,*r; }Leaf; typedef struct Node { int data,code; //data 頻數 code 編碼 int letter; // 字 struct Node *next; Leaf *leaf; //指向樹 }Node; struct Node *head; void create() { head=malloc(sizeof(*head)); head->next=NULL; head->leaf=NULL; } void print() { struct Node *p=head->next; while(p!=NULL) { printf("%d ",p->data); printf("%d ",p->letter); printf("%d ",p->code); printf("%d \n",p->next); p=p->next; } } void in(int n,int value,int codes,int letters) { struct Node *q=head,*a; int i; a=malloc(sizeof(*a)); a->next=NULL; a->data=value; a->letter=letters; a->code=codes; a->leaf=NULL; for(i=0;i<n;i++) { q=q->next; } a->next=q->next; q->next=a; } void del(int n) { struct Node *q=head; int i; struct Node *a; for(i=0;i<n;i++) { q=q->next; } a=q->next; q->next=a->next; free(a); a=NULL; } void destroy() { struct Node *q=head->next,*r=q->next; while(q!=NULL) { free(q); q=r; if(r!=NULL) r=r->next; } free(q); q=NULL; r=NULL; free(head); head=NULL; } void my_swap(int *a,int *b) { int temp; temp=*a; *a=*b; *b=temp; } Node *findlength(){ Node *q=head->next; while(q!=NULL){ q=q->next; } return q; } void sort_slist(Node *x, Node *y) { if(x==NULL) { return; } if(y==x){ return; } Node *pslow = x; Node *pfast = x->next; Node *ptemp = x; while(pfast!= y) { if(pfast->data < x->data) { ptemp = pslow; pslow = pslow->next; my_swap(&pslow->data , &pfast->data); my_swap(&pslow->letter , &pfast->letter); } pfast = pfast->next; } my_swap(&pslow->data , &x->data); my_swap(&pslow->letter , &x->letter); sort_slist(x , pslow); sort_slist(pslow->next , y); }
3.2 leaf1.2.h
#include <stdio.h> #include <stdlib.h> #ifndef LEAF_H #define LEAF_H #ifndef STDIO_H #define STDIO_H #endif #ifndef STDLIB_H #define STDLIB_H #endif #ifndef LINK_H #define LINK_H #endif void fpf(FILE *fout,int a) { int b=0; int c=0,d=0; if(a>255) { d=(a|d)&0x000000ff; c=(a|c); c=c>>8; c=c&0x000000ff; fprintf(fout,"%c%c",c,d); return ; } fprintf(fout,"%c",a); return ; } FILE *pf(int a,FILE *f) { int b=0; int c=0,d=0; if(a>255) { d=(a|d)&0x000000ff; c=(a|c); c=c>>8; c=c&0x000000ff; fprintf(f,"%c%c\t",c,d); return f; } fprintf(f,"%c\t",a); return f; } Node *Node_Cre(); int ins (Node *h,Node *node); Leaf *Leaf_Cre(void); int Leaf_Copy(Node *node); int Leaf_Link(Node *node); Leaf *Leaf_LinkAll(Node *h); int Leaf_Fus(Node *h); Leaf *Leaf_Xyz(Node *h); int Leaf_Pen(Leaf *root,Leaf *leaf); int Leaf_Syn(Leaf *root); // Tree,coder,decoder是對外介面 Leaf *Tree(Node *h); //建樹 char *Coder(int word,Leaf *root); //加密 int decoder(char *d,Leaf *root); // 解密 Node *Node_Cre() { Node *node; node=(Node *)calloc(1,sizeof(Node)); node->data=0; node->letter=-1; node->leaf=NULL; node->next=NULL; return node; } int ins (Node *h,Node *node) { Node *a,*b=node; for (a=h;a->next!=NULL;a=a->next) { if(b->data<a->next->data) { b->next=a->next; a->next=b; return 1; } } if (a->next==NULL) { a->next=b; } return 1; } Leaf *Leaf_Cre(void) { Leaf *leaf=(Leaf *)calloc(1,sizeof(Leaf)); int i; leaf->word=-1; leaf->p=0; leaf->l=NULL; leaf->par=NULL; leaf->r=NULL; for(i=0;i<50;i++) { leaf->code[i]=2; } return leaf; } int Leaf_Copy(Node *node) { Leaf *leaf; if (node==NULL) { return 0; } else if(node->leaf==NULL) { return 0; } else { leaf=node->leaf; leaf->word=node->letter; leaf->p=node->data; return 1; } } int Leaf_Link(Node *node) { Leaf *leaf; if(node==NULL) { return 0; } else if(node->leaf==NULL) { leaf=Leaf_Cre(); node->leaf=leaf; } Leaf_Copy(node); return 1; } Leaf *Leaf_LinkAll(Node *h)// 右邊是上一個 ,即從右向左 { Node *a,*b; Leaf *root,*leaf,*t; int k; a=h; for (a=h;a->next!=NULL;a=a->next) { b=a->next; k=Leaf_Link(a); k=Leaf_Link(b); t=a->leaf; t->r=b->leaf; b->leaf->l=a->leaf; } root=Leaf_Cre(); a->leaf->r=root; root->l=a->leaf; a=h; leaf=a->leaf; free(leaf); a=h->next; a->leaf->l=NULL; return root; } int Leaf_Fus(Node *h) { Node *a=h->next,*b,*c; int k; if(a==NULL) { return 0; } else if(a->next==NULL) { return 0; } else { b=a->next; c=Node_Cre(); k=Leaf_Link(c); c->leaf->r=a->leaf; c->leaf->l=b->leaf; c->data=a->data+b->data; c->leaf->p=c->data; a->leaf->par=c->leaf; b->leaf->par=c->leaf; h->next=b->next; k=ins(h,c); free(a); free(b); return 1; } } Leaf *Leaf_Xyz(Node *h)// 左子樹解密 右子樹加密 { Leaf *root,*t; Node *a=h,*b; root=Leaf_Cre(); t=Leaf_LinkAll(h); root->r=t; for (b=a->next;b->next!=NULL;b=a->next) { Leaf_Fus(a); } root->l=b->leaf; b->leaf->par=root; free(a); free(b); return root; } int Leaf_Pen(Leaf *root,Leaf *leaf) // 左小右大 { Leaf *roo=root,*lea=leaf; int a=1; lea->r=NULL; lea->l=NULL; while(lea->word!=roo->word) { if(lea->word>roo->word) { if(roo->r==NULL) { roo->r=lea; return 1; } roo=roo->r; } else { if(roo->l==NULL) { roo->l=lea; return 1; } roo=roo->l; } } return 0; } int Leaf_Syn(Leaf *root) { Leaf *a=root->r,*b1,*b2,*r=root->r; char c[50]; FILE *f=fopen("Statistic.txt","w"); int i,j,n,k; for (i=0;i<50;i++) { c[i]=2; } a=a->l; r->l=NULL; r->word=0; while(a!=NULL) { b1=a; b2=b1->par; i=0; k=0; while (b2!=root) { if(b1!=b2->r) { c[i]=0; } else { c[i]=1; } i++; b1=b2; b2=b2->par; } n=i; i--; if(a->word==10) { fprintf(f,"Enter\t"); } else if(a->word==32) { fprintf(f,"Space\t"); } else if(a->word==9) { fprintf(f,"Tab\t"); } else { f=pf(a->word,f); } fprintf(f,"p=%d\tcode:",a->p); for(j=0;j<n;j++) { a->code[j]=c[i]; fprintf(f,"%d",c[i]); i--; } fprintf(f,"\n"); b1=a; a=a->l; Leaf_Pen(r,b1); } fclose(f); return 1; } Leaf *Tree(Node *h) { Leaf *root; root=Leaf_Xyz(h); Leaf_Syn(root); return root; } char *Coder(int word,Leaf *root) { int w=word; char *c; Leaf *leaf=root->r; if(w==0) { return NULL; } while(leaf!=NULL) { if(w==leaf->word) { c=leaf->code; return c; } else if(w>leaf->word) { leaf=leaf->r; } else { leaf=leaf->l; } } if(leaf==NULL) { return NULL; } } int decoder(char *d,Leaf *root) { unsigned char c=1,t,j; FILE *fou,*fi; Leaf *leaf=root->l; int ii = 0; //fin=fopen(d,"r"); fi=fopen("Encode.dat","rb"); fou=fopen("DecodeDoc.txt","w"); while (1) { ii++; c=0; fscanf(fi,"%c",&c); if(feof(fi)) { break; } t=1<<7; while (t!=0) { j=((t&c)!=0); if (j) { leaf=leaf->r; } else { leaf=leaf->l; } if(leaf->word!=-1) { fpf(fou,leaf->word); leaf=root->l; } t=t>>1; } } printf("文件壓縮完成後佔總位元組數為%d\n",ii); fclose(fi); fclose(fou); return 1; } #endif
3.3 codeout.c
#include<stdio.h> #include<stdlib.h> #include<string.h> #include"link.h" #include"leaf1.2.h" unsigned char GOU=0;//編碼區 int j=0; //有用位數 FILE *fin,*fout; Leaf *treehead; void code(char codein[]) { if(codein==NULL) { return; } //程式健壯性補充 int i=0; //記位器 i=0; while(codein[i] != 2) // { if(j!=8) { if(codein[i] == 0) { j++; } if(codein[i]==1) { switch(j) { case 0: GOU=GOU|0x80;break; case 1: GOU=GOU|0x40;break; case 2: GOU=GOU|0x20;break; case 3: GOU=GOU|0x10;break; case 4: GOU=GOU|0x08;break; case 5: GOU=GOU|0x04;break; case 6: GOU=GOU|0x02;break; default: GOU=GOU|0x01;break; } j++; } } if(j == 8) { fputc(GOU,fout); GOU=0; j=0; } i++; } } void hafuman() { int a0=1; char *a1; fin=fopen("SourceDoc.txt","r"); while(a0!=0) { a0=read(); a1=Coder(a0,treehead); code(a1); } if(GOU!=0) { fputc(GOU,fout); GOU=0; j=0; } } int judgechs(char c) { if(c<0) { return 1; } else { return 0; } } int read() //c為檔名 { int i=0,j=0; int *ii=&i; char c=0; fscanf(fin,"%c",&c); if(judgechs(c)) { j=(int)(c); j=j&0x000000ff; j=j<<8; i=i|j; j=0; fscanf(fin,"%c",&c); j=(int)c; j=j&0x000000ff; i=i|j; } else { i=(int)c; } return i; } void preread(){ create(); int t=0; int i=0,n=0,flag=0; t=read(); struct Node *q=head->next; in(n,1,-1,t); n++; while(t!=0) { t=read(); struct Node *q=head->next; for(i=1;i<=n;i++){ if(q->letter==t){ q->data=q->data+1; flag=1; break; } q=q->next; } if(flag==1){ flag=0; continue; } in(n,1,-1,t); n++; } del(n-1); Node*z=findlength(); sort_slist(head->next,z); } int main(){ system("color 3b"); fin=fopen("SourceDoc.txt","r"); if(fin==NULL) { printf("請正確匯入檔案\n"); system("pause"); return 0; } fout=fopen("Encode.dat","w"); preread(); treehead=Tree(head); hafuman(); printf("編碼成功,加密檔案輸出於Encode.dat,歡迎檢視\n"); fclose(fout); fclose(fin); decoder(fout,treehead); printf("解碼成功,結果輸出於DecodeDoc.txt,歡迎檢視\n"); system("pause"); return 0; }
四、實驗後續
若要真正實現一個可用的壓縮軟體,需要一個互動式的介面,由於c語言本身開發圖形介面並不算太好用,加上在設計初期並沒有考慮這個問題,因此在後續想進一步加入圖形介面是遇到了一些困難,我們嘗試通過把程式變成.h標頭檔案形式嵌入QT工程來實現圖形介面,在QT自帶開發工具下實現了這一目標,但是無法生成可用的.exe檔案。若後續閒暇之時實現了這一功能,將會更新。本文基於當時實驗報告簡易修改而成,感謝當時的兩位一起完成本工程的同學yy及yjc。限於水平有限,文中必然有所疏忽和可以改進之處,如有問題或者想交流歡迎電子郵件或者站內信聯絡。
作者資訊:xbf 西安交通大學資訊通訊系本科
聯絡方式:[email protected]