實驗三:哈夫曼編/譯碼器
實驗目的: 掌握哈夫曼樹
實驗內容: 利用哈夫曼編碼進行通信可以大大提高信道利用率,縮短信息傳輸時間,降低傳輸成本。但是,這要求在發送端通過一個編碼系統對待傳數據預先編碼,在接收端將傳來的數據進行譯碼(復原)。對於雙工信道(即可以雙向傳輸信息的信道),每端都需要一個完整的編/譯碼系統。試爲這樣的信息收發站寫一個哈夫曼碼的編/譯碼系統。
實驗要求: 編寫完整的系統,要求具有以下功能:
I:初始化(Initialization)。從終端讀入字符集大小n,以及n個字符和n個權值,建立哈夫曼樹,並將它存入文件hfmTree中。
E:編碼(Encoding)。利用以建好的哈夫曼樹(如不在內存,則從文件hfmTree中讀入),對文件ToBeTran中的正文進行編碼,然後將結果存入文件CodeFile中。
D:譯碼(Decoding)。利用已建好的哈夫曼樹將文件CodeFile中的代碼進行譯碼,結果存入文件TextFile中。
P:印代碼文件(Print)。將文件CodeFile以緊湊格式顯示在終端上,每行50個代碼。同時將此字符形式的編碼文件寫入文件CodePrin中。
T:印哈夫曼樹(Tree printing)。將已在內存中的哈夫曼樹以直觀的方式(樹或凹入表形式)顯示在終端上,同時將此字符形式的哈夫曼樹寫入文件TreePrint中。
實驗過程
-
需求分析
首先要確定n個字符及其權值。一篇英語短文包括字母的大小寫以及基本的符號,從網上查找資料獲得各個字母出現的平均概率,作爲各個符號的權值。由於符號衆多,我們選擇將其保存到文件裏面進行讀取。
其次,關於文件的寫入操作。我們直接將屏幕上打印出來的內容保存到相應的txt文件中去。
最後樹的打印,我們採取中序遍歷樹的方式,將一棵樹橫向的打印到文件中去。 -
概要設計
相關的數據結構設計:
哈夫曼樹的節點typedef struct Node { int weight;//權值 int parent;//父節點的序號,爲0表示根節點 int lchild, rchild;//左右孩子節點的序號,爲0的是葉子節點 }HTNode, *HuffmanTree;
編譯結果的保存
struct Ans { char ch; // 保存字符 int wet; // 保存權值 }ans[maxn];
主程序的流程:
- 初始化:從文件中讀取權值和字符,建立一棵哈夫曼樹。
- 編碼:編寫函數HuffmanCoding確定每個字符的哈夫曼編碼。
- 譯碼:利用事先準備好的一篇文章,打印出文章結果。
- 打印部分:打印包括代碼和哈夫曼樹。
-
詳細設計
- 建立哈夫曼樹的過程
HuffmanTree create_HuffmanTree(struct Ans *p, int n) { int t = 2 * n - 1; HuffmanTree HT = (HuffmanTree)malloc(total * sizeof(HTNode)); int i; for (i = 0; i < n; ++i) {//先構造n個葉節點 HT[i].parent = 0;//parent後面會改變 HT[i].lchild = 0; HT[i].rchild = 0; HT[i].weight = p->wet; p++; } for (; i < total; ++i) { HT[i].parent = 0; HT[i].lchild = 0; HT[i].rchild = 0; HT[i].weight = 0; } int min1, min2;//用來保存每一輪選出的兩個weight最小且parent爲0的節點下標 //每一輪比較後選擇出min1和min2構成一課二叉樹,最後構成一棵哈夫曼樹 for (i = n; i < t; ++i) { select_min(HT, i, min1, min2); //另外編寫的一個函數,用來尋找i之前的結點 HT[min1].parent = i; HT[min2].parent = i; //兩個最小權值的結點構成子樹 HT[i].lchild = min1; HT[i].rchild = min2; HT[i].weight = HT[min1].weight + HT[min2].weight; } /* for(int i=0;i<total;i++) { printf("%d-%d-%d\n",HT[i].lchild,HT[i].rchild,HT[i].weight); printf("%c\n",ans[i].ch); }*/ return HT; }
- 譯碼過程
void HuffmanCoding(HuffmanTree HT, int n) { char temp[maxn + 5];//臨時用來保存每次求得的一個哈夫曼編碼串 temp[maxn] = '\0';//結束符 for (int i = 0; i < n; ++i) { int current = i; int fa = HT[i].parent; int start = maxn; //初始爲編碼結束符的位置 while (fa != 0) { if (HT[fa].lchild == current) temp[--start] = '0'; else temp[--start] = '1'; current = fa; fa = HT[current].parent; } strcpy(HC[i], temp + start);//將編碼串從最後的start編碼~\0複製到HC上 } }
- 初始化(從文件中讀入權值)
int n;//需要編碼的字符數 void Initialzation() { FILE *fp1,*fpx; fp1 = fopen("./DataFile.data", "r"); printf("從文件DataFile.txt中讀取編碼的字符種類個數\n"); fscanf(fp1, "%d", &n); printf("以及這%d個字符及其權值(整型)\n", n); for (int i = 0; i < n; ++i) { fscanf(fp1, " %c %d", &ans[i].ch, &ans[i].wet); } HuffmanTree HT = create_HuffmanTree(ans, n); HuffmanCoding(HT, n);//求解每個字符的編碼 printf("各字符對應權值的哈夫曼編碼爲(按照輸入順序):\n"); for (int i = 0; i < n; ++i) { printf("%c :", ans[i].ch); puts(HC[i]); } printf("將它們存入htmtree.txt中去:\n"); fpx=fopen("./htmtree.txt","w+"); for(int i=0;i<n;i++) { fprintf(fpx,"%c %s\n",ans[i].ch,HC[i]); } fclose(fp1); fclose(fpx); printf("\n******************************************************\n\n"); }
- 編碼
void EnCoding() { //char ch[maxn*maxn],temp; printf("下面對文件ToBeTran.data中的文本進行編碼形成報文\n"); printf("文章寫在文件CodeFile.txt中\n"); FILE *fp2, *fp3; fp2 = fopen("ToBeTran.txt", "rb"); fp3 = fopen("CodeFile.txt", "w"); //fscanf(fp2,"%s",temp); char ch; do { ch = fgetc(fp2); /* read a char from the file */ if (ch == ' ') fprintf(fp3, " "); else if (ch == '\n') fprintf(fp3, "\n"); else if(ch-'a'>=0) fprintf(fp3, "%s", HC[ch - 'a']); else if(ch-'A'>=0) fprintf(fp3, "%s", HC[ch - 'A'+26]); else if(ch-'0'>=0&&'9'-ch>=0) fprintf(fp3, "%s", HC[ch - '0'+52]); else if(ch==',') fprintf(fp3, "%s", HC[62]); else if(ch=='.') fprintf(fp3, "%s", HC[63]); } while (ch != EOF); fclose(fp2); fclose(fp3); for(int i=0;i<n;i++) printf("%s\n",HC[i]); }
- 譯碼
void Decoding() { printf("下面對文件CodeFile.data中的代碼進行解碼形成原文\n結果存入文件Textfile.txt中\n"); FILE *fp4, *fp5; fp4 = fopen("CodeFile.txt", "rb"); fp5 = fopen("Textfile.txt", "w"); char ch; char temp[maxn]; char *s = temp; memset(temp, 0, sizeof(temp)); //while(fgets(temp,maxn*maxn,fp4))//讀到換行符,並加上\0,存入temp do { ch = fgetc(fp4); if (ch == '\n') memset(temp, 0, sizeof(temp)), s = temp, fprintf(fp5, "\n"); else if (ch == ' ') memset(temp, 0, sizeof(temp)), s = temp, fprintf(fp5, " "); else { *s++ = ch; if (fun(temp) >= 26&&fun(temp)<52) fprintf(fp5, "%c", 'A' + fun(temp)%26), memset(temp, 0, sizeof(temp)), s = temp; else if(fun(temp)>=0&&fun(temp)<26) fprintf(fp5, "%c", 'a' + fun(temp)), memset(temp, 0, sizeof(temp)), s = temp; else if(fun(temp)>=52&&fun(temp)<62) fprintf(fp5, "%c", '0' + fun(temp)%52), memset(temp, 0, sizeof(temp)), s = temp; else if(fun(temp)==62) fprintf(fp5, "%c", ','), memset(temp, 0, sizeof(temp)), s = temp; else if(fun(temp)==63) fprintf(fp5, "%c", '.'), memset(temp, 0, sizeof(temp)), s = temp; else continue; } } while (ch != EOF); fclose(fp4); fclose(fp5); }
- 打印
void print_tree(HuffmanTree HT,int t,int step)//橫向打印哈夫曼樹 { int i; if(HT[t].rchild) print_tree(HT, HT[t].rchild, step + 1); for (i = 0; i < step; i++) { printf(" "); fprintf(pt," "); } printf(" -%d-▏\n", HT[t].weight); fprintf(pt," -%d-▏\n",HT[t].weight); if(HT[t].lchild) print_tree(HT, HT[t].lchild, step + 1); } void Output() { FILE *fp6; fp6=fopen("CodePrin.txt","w"); for (int i = 0; i < n; ++i) { printf("%c :%lf\n", ans[i].ch, ans[i].wet*1.0 / sum); } printf("\n******************************************************\n\n"); printf("下面輸出輸出ToBeTran.data及其報文CodeFile.txt\n\n"); FILE *fp2, *fp3; fp2 = fopen("ToBeTran.txt", "r"); fp3 = fopen("CodeFile.txt", "r"); char ch; printf("ToBeTran.data :\n"); do { ch = fgetc(fp2); printf("%c", ch); } while (ch != EOF); printf("\n\n\n"); printf("CodeFile.txt :\n"); int cnt=0; do { ch = fgetc(fp3); printf("%c", ch); fprintf(fp6,"%c",ch); cnt+=1; if(cnt%50==0) { printf("\n"); fprintf(fp6,"\n"); } } while (ch != EOF); printf("\n\n\n"); printf("\n******************************************************\n\n"); printf("下面輸出CodeFile.data及其原文Textfile.txt\n\n"); FILE *fp4, *fp5; fp4 = fopen("CodeFile.txt", "r"); fp5 = fopen("Textfile.txt", "r"); printf("CodeFile.data :\n"); do { ch = fgetc(fp4); printf("%c", ch); } while (ch != EOF); printf("\n\n\n"); printf("Textfile.txt :\n"); do { ch = fgetc(fp5); printf("%c", ch); } while (ch != EOF); printf("\n\n\n"); fclose(fp6); }
-
調試分析
(1)調試過程中遇到的問題- 大小寫以及符號的問題。如果只有小寫字母,那我們可以根據Ascii碼
-'a'
來確定每個字符的相對大小,但是引入了大寫和符號,所以我們就需要多增加幾個條件分支來保存他們的權值。 - 如何打印樹。如果直接選擇常規的方法去遍歷樹就會像得十分的麻煩,所以我選擇了橫向打印樹的方式,類似下圖。
(2) 算法的時空複雜度。
假設編碼的文本字符串長度爲n,字母爲k個符號。
由於我們的權值讀入是無序的,因此需要先對權值進行排序,時間複雜度O(k*log(k))對於每個編碼符號,必須遍歷樹以解碼該符號。樹包含k個節點,並且平均需要O(log k)個節點訪問來解碼符號。所以時間複雜度爲O(n log k)。
空間複雜度爲樹的O(k)和解碼文本的O(n)。
- 大小寫以及符號的問題。如果只有小寫字母,那我們可以根據Ascii碼
實驗結果:
首先準備好n個權值和一篇英文文章。
I:初始化
67個權值
一篇英語文章
實驗終端打印
文件中打印
E:編碼
將編碼結果打印出來並且保存到CodeFile文件裏
文章原文
CodeFile文件
D:譯碼
將譯碼之後的結果保存到TextFile文件裏
P:打印
將文件CodeFile以緊湊格式顯示在終端上,每行50個代碼。同時將此字符形式的編碼文件寫入文件CodePrin中。
CodePrin文件
T:印哈夫曼樹
將哈夫曼樹打印出來
實驗總結
本次實驗花費時間比較長,有以下幾點感悟:
- 注意內存空間的管理,很有可能就出現內存泄漏的情況。
- 對於文件的打印,最後一定要記得把文件關掉。
- 關於遞歸問題的文件打印,文件指針最好是要設置成全局變量,不然文件很有可能在中途就關閉。
- 哈夫曼樹是一種效率很高的數據結構,如果輸入權值前就進行好排序,可能時間複雜度會更好。
- 橫向打印樹可以是一種輸出樹結構的好方式。