實驗——樹(根據後序和中序遍歷輸出先序遍歷、哈夫曼編碼)
實驗——樹(根據後序和中序遍歷輸出先序遍歷、哈夫曼編碼)
一、實驗目的
-
熟練掌握二叉樹、完全二叉樹的儲存方式,二叉樹的前序、中序、後序和層次遍歷方法,樹的性質。
-
練習建立二叉樹的演算法,通過前中、後中順序確定二叉樹的演算法。
-
通過二叉樹的演算法,解決哈夫曼編碼等應用問題。
二、 根據後序和中序遍歷輸出先序遍歷
2.1 實驗內容和要求
問題描述
輸入格式
第一行給出正整數N(≤30),是樹中結點的個數。隨後兩行,每行給出N個整數,分別對應後序遍歷和中序遍歷結果,數字間以空格分隔。題目保證輸入正確對應一棵二叉樹。
輸出格式
在一行中輸出Preorder:
以及該樹的先序遍歷結果。數字間有1個空格,行末不得有多餘空格。
輸入樣例
7 2 3 1 5 7 6 4 1 2 3 4 5 6 7
輸出樣例
Preorder: 4 1 3 2 6 5 7
2.2 演算法設計
-
主流程設計
int main(){ 輸入樹中結點個數; 輸入該樹的後序遍歷結果; 輸入該樹的中序遍歷結果; 通過兩種遍歷結果建立該樹; 用先序遍歷方法輸出該樹; return 0; }
2. 構建二叉樹流程分析
*設計思路:通過後序遍歷找到該樹的根結點(即後序遍歷最後一個結點),根結點將中序遍歷序列分為兩個子序列,就可以確定根結點下的左右子樹的結點個數,且後序遍歷序列可以看作根結點左子樹序列+根結點右子樹序列+根結點組成。由樹的遞迴性可以對根結點左子樹序列、根結點右子樹序列進行相同操作。
*具體實現:設定兩序列長度均為Len,後序遍歷序列為Post,中序遍歷序列為In,在遞迴過程中後序遍歷序列區間為[k,Len-1],中序遍歷序列區間為[k,Len-1],由上述分析可以知道,在這一遞迴階段中,根結點為Post[Len-1], 接著在中序遍歷序列中尋找位置index,使In[index] = Post[Len-1],這樣這一遞迴階段中左子樹結點數量為i, 進入下一遞迴階段時,左子樹後序遍歷序列和中序遍歷序列變為[k, i-1],右子樹後序遍歷序列變為[k+i, Len-i-1],中序遍歷序列變為[k+i+1, Len-i-1](‘+1’是該樹的根結點的位置,需要跳過去)。
BinTree CreateBinTree(int* Post, int* In, int Len) { //輸入後序、中序和結點的個數 判斷是否有子樹; 建立樹; 將此時後序遍歷的最後一個數作為一個根結點; 遍歷中序結果; 若找到與根結點相同的數,將該數下標做標記,並且跳出迴圈; 找根結點的左子樹; 找根結點右子樹的值; }
-
ADT定義
typedef struct TNode* PreToTNode; struct TNode { int Data; PreToTNode Left; PreToTNode Right; }; typedef PreToTNode BinTree; //根據前序和中序遍歷,構建二叉樹 BinTree CreateBinTree(int* Post, int* In, int Len); //先序遍歷輸出 void PreorderTraversal(BinTree BT);
-
演算法示例
2.3 演算法分析
通過演算法過程示例發現,時間複雜度為 O(n) = O(NlogN[構建二叉樹] + NlogN[先序遍歷輸出]) = O(2*NlogN) ;
空間複雜度為二叉樹 的構造空間使用 O(n) = O(N)。
三、哈夫曼編碼
3.1 實驗內容和要求
問題描述
給定一段文字,如果我們統計出字母出現的頻率,是可以根據哈夫曼演算法給出一套編碼,使得用此編碼壓縮原文可以得到最短的編碼總長。然而哈夫曼編碼並不是唯一的。例如對字串"aaaxuaxz",容易得到字母 'a'、'x'、'u'、'z' 的出現頻率對應為 4、2、1、1。我們可以設計編碼 {'a'=0, 'x'=10, 'u'=110, 'z'=111},也可以用另一套 {'a'=1, 'x'=01, 'u'=001, 'z'=000},還可以用 {'a'=0, 'x'=11, 'u'=100, 'z'=101},三套編碼都可以把原文壓縮到 14 個位元組。但是 {'a'=0, 'x'=01, 'u'=011, 'z'=001} 就不是哈夫曼編碼,因為用這套編碼壓縮得到 00001011001001 後,解碼的結果不唯一,"aaaxuaxz" 和 "aazuaxax" 都可以對應解碼的結果。本題就請你判斷任一套編碼是否哈夫曼編碼。
輸入格式
首先第一行給出一個正整數 N(2≤N≤63),隨後第二行給出 N 個不重複的字元及其出現頻率,格式如下:
c[1] f[1] c[2] f[2] ... c[N] f[N]
其中c[i]
是集合{'0' - '9', 'a' - 'z', 'A' - 'Z', '_'}中的字元;f[i]
是c[i]
的出現頻率,為不超過 1000 的整數。再下一行給出一個正整數 M(≤1000),隨後是 M 套待檢的編碼。每套編碼佔 N 行,格式為:
c[i] code[i]
其中c[i]
是第i個字元;code[i]
是不超過63個'0'和'1'的非空字串。
輸出格式
對每套待檢編碼,如果是正確的哈夫曼編碼,就在一行中輸出"Yes",否則輸出"No"。 注意:最優編碼並不一定通過哈夫曼演算法得到。任何能壓縮到最優長度的字首編碼都應被判為正確。
輸入樣例
7 A 1 B 1 C 1 D 3 E 3 F 6 G 6 4 A 00000 B 00001 C 0001 D 001 E 01 F 10 G 11 A 01010 B 01011 C 0100 D 011 E 10 F 11 G 00 A 000 B 001 C 010 D 011 E 100 F 101 G 110 A 00000 B 00001 C 0001 D 001 E 00 F 10 G 11
輸出樣例
Yes
Yes
No
No
3.2 演算法設計
-
主流程設計
int main(){ 建一個哈夫曼樹; 將字元存進去; 計算最優WPL; 讀入待檢測的編碼; 比較編碼的wpl是否與最優WPL一致; 檢測是否是字首編碼; }
2. 哈夫曼樹的構造
*設計思路:由哈夫曼樹和帶權路徑長度的定義可知,一棵二叉樹要使其WPL最小,必須使權值越大的葉結點越靠近根結點,而權值越小的葉結點越遠離根結點。可在初始狀態下將每一個字元看成一棵獨立的樹,每一步選擇權值最小的兩顆樹進行合併。
初始化哈夫曼樹; for(遍歷前n個){ 存葉子節點,賦給權值,其他項(parent、lchild、rchild)賦零; } for(遍歷後面的){ 存非葉子節點,權值賦零,其他項也賦零; } 建立哈夫曼樹; for(遍歷每套編碼方案){ 在HT[1,i-1]中找到沒有parent且權值最小的兩個元素(需要一個函式); 將其parent、lchild、rchlid賦值,並算該樹的權值,一步一步建立哈夫曼樹; }
注意根結點的位置變化;
}
3. 最優帶權路徑長度(WPL)的求值
*設計思路:計算該樹的最優帶權路徑長度,用來判斷所給編碼方案是否為最優編碼。 具體實現:運用遞迴函式,由根結點依次向下找出葉節點。
void GetWPL(HuffmanTree HT, int Deep, HTNode* p){ //對葉子節點進行計算 if ((p->lchild == 0) && (p->rchild == 0)){ WPL += (p->weight) * Deep; } if (p->lchild != 0) //注意細節,不能用 else if GetWPL(HT, Deep + 1, HT + (p->lchild)); if (p->rchild != 0) GetWPL(HT, Deep + 1, HT + (p->rchild)); }
-
字首編碼的判斷 *設計思路:字首編碼是指任一字元的編碼都不是另一個字元編碼的字首(等長編碼一定是字首編碼!)。由於需要編碼及其地址,所以運用二維陣列的傳遞更為方便。 *具體實現:將這些編碼逐個的新增到二維陣列中,對於每一個編碼字串,字串中的每一個字元也逐個掃描(需要注意迴圈開始的條件),先假定不是字首編碼,用flag記錄,兩兩相比較,如果在迴圈中有不一樣的編碼位,說明是字首編碼;如果迴圈結束但已掃描到某節點為葉子節點但字串還未結束,或者字串已掃描結束但還當前節點非空,那麼就不是字首碼。
int IsPreCoding(char temp[][64]){ int i, j, h, len1, len2; for (i = 1; i <= N - 1; ++i){ for (j = i + 1; j <= N; ++j){ //j和i為需要比較的元素下標 int flag = 0; for (h = 0; (temp[i][h] != '\0') && (temp[j][h] != '\0'); h++){ if (temp[i][h] != temp[j][h]){ flag = 1; } } if (flag == 0){ return 0; } } } return 1; }
-
ADT定義
int m; //哈夫曼樹節點個數 int w[64], N, M; //權值陣列W[],元素數量N,M套編碼 int WPL; //帶權路徑長度 typedef struct HTNode* HuffmanTree; struct HTNode{ int weight; //結點權值 int parent, lchild, rchild; }; HTNode* Root_pos; typedef char** HuffmanCode; //定義元素型別為 char陣列首地址 的陣列 void Select(HuffmanTree& HT, int n, int& s1, int& s2); //比較權值,找出最小、次小權值 void HuffmanCoding(HuffmanTree& HT, HuffmanCode& HC, int* w, int n); //構造哈夫曼樹 int IsPreCoding(char temp[][64]); //判斷是否為字首編碼 void GetWPL(HuffmanTree HT, int Deep, HTNode* p); //對葉子節點進行計算,求WPL
2.3 演算法分析
該Huffman演算法的複雜度主要由以下幾部分組成:
(1)構造哈夫曼樹:O(N^2);
(2)求最優WPL: O(NlogN);
(3)字首編碼的判斷:O(N^2);
故整體複雜度為 O(n) = (NlogN) ;
空間複雜度為結點空間的使用,即O(N)。
四、總結
根據後序和中序遍歷輸出先序遍歷:
主要不清楚的點在構建樹的左右子樹的遞迴函式中後序序列、中序序列和根結點、長度的關係,根據例子畫圖,按照還原後的二叉樹反過來一步一步帶入到函式中,最後再正著分析才明白;
哈夫曼編碼:
雖然知道如何求WPL、哈夫曼編碼怎麼構建,但也只是頭腦中的動畫演示,程式碼寫不出來,於是在網上找到了程式碼做思考分析。這個程式碼思路特別清晰,難理解的是其使用的方法,例如運用二維陣列的傳遞判斷是否為字首編碼,不知道為什麼用二維陣列、為什麼不用一維或者直接掃描或者其他容器,經過反覆研讀程式碼,做了些小試驗,體會到了運用二維陣列傳遞的簡潔,還有其他一些不懂的,又重新多看了幾遍慕課和書本相關內容。在整個分析中發現自己更多的問題,特別是樹、指標、地址、陣列等的靈活運用,還有程式碼的理解能力等,感覺還有一些沒有想到的問題與分析。這是一道經典題,需要強加記憶、反覆思考,需要勤加鍛鍊、多動手打程式碼,需要反覆修改;
通過這兩道題:
書、慕課、CSDN、畫圖或者動畫演示等都是為自己能夠記住、能夠自己打出成功程式碼而服務的,不要認為理解了、能十分詳細的用圖的形式解釋明白就完事了的,還需要再看反覆看,會有新的認識,對自己寫程式碼就會有更多的幫助。
五、原始碼(主要)
5.1 根據後序和中序遍歷輸出先序遍歷
/* 先序遞迴遍歷 *訪問根結點 *先序遍歷其左子樹 *先序遍歷其右子樹 */ void PreorderTraversal(BinTree BT) { if (BT) { printf(" %d", BT->Data); PreorderTraversal(BT->Left); PreorderTraversal(BT->Right); } } /* 構建樹 *輸入後序、中序和結點的個數 */ BinTree CreateBinTree(int* Post, int* In, int Len) { BinTree T; int index = 0; if (Len == 0) { //判斷是否有子樹 return NULL; } T = (BinTree)malloc(sizeof(struct TNode)); //建立樹 T->Data = Post[Len - 1]; //此時後序遍歷的最後一個數作為一個根結點 for (int i = 0; i < Len; i++) { //遍歷中序結果 if (In[i] == Post[Len - 1]) { //若找到與根結點相同的數,將該數下標做標記,跳出迴圈 index = i; break; } } T->Left = CreateBinTree(Post, In, index); //找根結點的左子樹 T->Right = CreateBinTree(Post + index, In + index + 1, Len - index - 1); //找根結點右子樹的值 return T; }
5.2 哈夫曼編碼
/*找最小、次小權值*/ void Select(HuffmanTree& HT, int n, int& s1, int& s2){ int i; s1 = s2 = 0; int min1 = INT_MAX; //最小值,INT_MAX在<limits.h>中定義的 int min2 = INT_MAX; //次小值 for (i = 1; i <= n; ++i){ if (HT[i].parent == 0){ //找沒有parent的最小和次小權值(下標) //只有兩種情況 if (HT[i].weight < min1){ min2 = min1; //舊的最小權值賦給舊的次小權值 s2 = s1; //下標變動 min1 = HT[i].weight; //賦最小權值 s1 = i; } else if ((HT[i].weight >= min1) && (HT[i].weight < min2)){ min2 = HT[i].weight; //賦次小權值 s2 = i; } } } } void HuffmanCoding(HuffmanTree& HT, HuffmanCode& HC, int* w, int n){ if (n <= 1) return; M = 2 * n - 1; HuffmanTree p; int i; HT = (HuffmanTree)malloc((M + 1)*sizeof(HTNode)); //初始化哈夫曼樹 w++; for (p = HT + 1, i = 1; i <= n; ++i, ++p, ++w){ //前n個存葉子節點,賦權值,其他項賦零 p->weight = *w; p->parent = 0; p->lchild = 0; p->rchild = 0; } for (; i <= M; ++i, ++p){ //後面存非葉子節點; p->weight = 0; p->parent = 0; p->lchild = 0; p->rchild = 0; } //建立哈夫曼樹 int s1, s2; for (i = n + 1; i <= M; i++){ Select(HT, i - 1, s1, s2); //此函式在HT[1,i-1]中選擇父為零且w值最小的兩個元素,返回下標 HT[s1].parent = i; HT[s2].parent = i; HT[i].lchild = s1; HT[i].rchild = s2; HT[i].weight = HT[s1].weight + HT[s2].weight; } Root_pos = (HT + i - 1); } /*字首編碼的判斷*/ int IsPreCoding(char temp[][64]){ int i, j, h, len1, len2; for (i = 1; i <= N - 1; ++i){ //迴圈比較N-1次 for (j = i + 1; j <= N; ++j){ //j和i為需要比較的元素下標 int flag = 0; //假定不是字首編碼 for (h = 0; (temp[i][h] != '\0') && (temp[j][h] != '\0'); h++){ if (temp[i][h] != temp[j][h]){ //迴圈中有不一樣的編碼位,說明是字首編碼 flag = 1; } } if (flag == 0){ //如果迴圈結束flag還沒有變,說明當前的兩個不是字首編碼,返回0 return 0; } } } return 1; } //遞迴求WPL void GetWPL(HuffmanTree HT, int Deep, HTNode* p){ //對葉子節點進行計算 if ((p->lchild == 0) && (p->rchild == 0)){ WPL += (p->weight) * Deep; } if (p->lchild != 0) GetWPL(HT, Deep + 1, HT + (p->lchild)); if (p->rchild != 0) GetWPL(HT, Deep + 1, HT + (p->rchild)); } int main(){ /*處理哈夫曼樹*/ cin >> N; char ch; int i; for (i = 1; i <= N; i++){ cin >> ch >> w[i]; } HuffmanTree HT; HuffmanCode HC, HCp; HuffmanCoding(HT, HC, w, N); /*得出權值最優路徑*/ int Deep = 0; HTNode* p = Root_pos; WPL = 0; GetWPL(HT, Deep, p); /*處理編碼方案*/ cin >> M; for (int j = 0; j < M; j++){ //M套編碼方案 int wpl = 0; char temp[N + 2][64]; //注意開二維陣列,下面每個迴圈用一維 for (i = 1; i <= N; i++){ //每套編碼都是N個元素 cin >> ch >> temp[i]; wpl += (strlen(temp[i]) * w[i]); } if (wpl > WPL){ //判斷是否是最優編碼 cout << "No" << endl; }else{ if (IsPreCoding(temp)){ //判斷是否是字首編碼 cout << "Yes" << endl; }else{ cout << "No" << endl; } } } getchar(); return 0; }
演算法分析、遍歷生成樹分析圖等均為原創作品,歡迎指正!