哈夫曼(huffman)樹和哈夫曼編碼
目錄
正文
哈夫曼樹
哈夫曼樹也叫最優二叉樹(哈夫曼樹)
問題:什麼是哈夫曼樹?
例:將學生的百分制成績轉換為五分製成績:≥90 分: A,80~89分: B,70~79分: C,60~69分: D,<60分: E。
if (a < 60){ b = 'E'; } else if (a < 70) { b = ‘D’; } else if (a<80) { b = ‘C’; } else if (a<90){ b = ‘B’; } else { b = ‘A’; }
判別樹:用於描述分類過程的二叉樹。
如果每次輸入量都很大,那麼應該考慮程式執行的時間
如果學生的總成績資料有10000條,則5%的資料需 1 次比較,15%的資料需 2 次比較,40%的資料需 3 次比較,40%的資料需 4 次比較,因此 10000 個數據比較的
次數為: 10000 (5%+2×15%+3×40%+4×40%)=31500次
此種形狀的二叉樹,需要的比較次數是:10000 (3×20%+2×80%)=22000次,顯然:兩種判別樹的效率是不一樣的。
問題:能不能找到一種效率最高的判別樹呢?
那就是哈夫曼樹
回憶樹的基本概念和術語
路徑:若樹中存在一個結點序列k1,k2,…,kj,使得ki是ki+1的雙親,則稱該結點序列是從k1到kj的一條路徑。 路徑長度:等於路徑上的結點數減1。 結點的權:在許多應用中,常常將樹中的結點賦予一個有意義的數,稱為該結點的權。 結點的帶權路徑長度:是指該結點到樹根之間的路徑長度與該結點上權的乘積。 樹的帶權路徑長度:樹中所有葉子結點的帶權路徑長度之和,通常記作: 其中,n表示葉子結點的數目,wi和li分別表示葉子結點ki的權值和樹根結點到葉子結點ki之間的路徑長度。 赫夫曼樹(哈夫曼樹,huffman樹)定義: 在權為w1,w2,…,wn的n個葉子結點的所有二叉樹中,帶權路徑長度WPL最小的二叉樹稱為赫夫曼樹或最優二叉樹。例:有4 個結點 a, b, c, d,權值分別為 7, 5, 2, 4,試構造以此 4 個結點為葉子結點的二叉樹。
WPL=7´2+5´2+2´2+4´2= 36
WPL=7´3+5´3+2´1+4´2= 46
WPL=7´1+5´2+2´3+4´3= 35
WPL=7´1+5´2+2´3+4´3= 35
後兩者其實就是最有二叉樹(也就是哈夫曼樹)
哈夫曼樹的構造(哈夫曼演算法) 1.根據給定的n個權值{w1,w2,…,wn}構成二叉樹集合F={T1,T2,…,Tn},其中每棵二叉樹Ti中只有一個帶權為wi的根結點,其左右子樹為空. 2.在F中選取兩棵根結點權值最小的樹作為左右子樹構造一棵新的二叉樹,且置新的二叉樹的根結點的權值為左右子樹根結點的權值之和. 3.在F中刪除這兩棵樹,同時將新的二叉樹加入F中. 4.重複2、3,直到F只含有一棵樹為止.(得到哈夫曼樹)例:有4 個結點 a, b, c, d,權值分別為 7, 5, 2, 4,構造哈夫曼樹。
根據給定的n個權值{w1,w2,…,wn}構成二叉樹集合F={T1,T2,…,Tn},其中每棵二叉樹Ti中只有一個帶權為wi的根結點,其左右子樹為空.
在F中選取兩棵根結點權值最小的樹作為左右子樹構造一棵新的二叉樹,且置新的二叉樹的根結點的權值為左右子樹根結點的權值之和.
在F中刪除這兩棵樹,同時將新的二叉樹加入F中.
重複,直到F只含有一棵樹為止.(得到哈夫曼樹)
關於哈夫曼樹的注意點:
1、滿二叉樹不一定是哈夫曼樹
2、哈夫曼樹中權越大的葉子離根越近 (很好理解,WPL最小的二叉樹)
3、具有相同帶權結點的哈夫曼樹不惟一
4、哈夫曼樹的結點的度數為 0 或 2, 沒有度為 1 的結點。
5、包含 n 個葉子結點的哈夫曼樹中共有2n – 1 個結點。
6、包含 n 棵樹的森林要經過 n–1 次合併才能形成哈夫曼樹,共產生 n–1 個新結點
再看一個例子:如權值集合W={7,19,2,6,32,3,21,10 }構造赫夫曼樹的過程。根據給定的n個權值{w1,w2,…,wn}構成二叉樹集合F={T1,T2,…,Tn},其中每棵二叉樹Ti中只有一個帶權為wi的根結點,其左右子樹為空.
在F中選取兩棵根結點權值最小的樹
作為左右子樹構造一棵新的二叉樹,置新的二叉樹的根結點的權值為左右子樹根結點的權值之和
在F中刪除這兩棵樹,同時將新的二叉樹加入F中.
重複,直到F只含有一棵樹為止.(得到哈夫曼樹)
在F中刪除這兩棵樹,同時將新的二叉樹加入F中.
構造完畢(哈夫曼樹,最有二叉樹),也就是最佳判定樹
哈夫曼編碼
哈夫曼樹的應用很廣,哈夫曼編碼就是其在電訊通訊中的應用之一。廣泛地用於資料檔案壓縮的十分有效的編碼方法。其壓縮率通常在20%~90%之間。在電訊通訊業務中,通常用二進位制編碼來表示字母或其他字元,並用這樣的編碼來表示字元序列。
例:如果需傳送的電文為 ‘ABACCDA’,它只用到四種字元,用兩位二進位制編碼便可分辨。假設 A, B, C, D 的編碼分別為 00, 01,10, 11,則上述電文便為 ‘00010010101100’(共 14 位),譯碼員按兩位進行分組譯碼,便可恢復原來的電文。
能否使編碼總長度更短呢?
實際應用中各字元的出現頻度不相同,用短(長)編碼表示頻率大(小)的字元,使得編碼序列的總長度最小,使所需總空間量最少
資料的最小冗餘編碼問題
在上例中,若假設 A, B, C, D 的編碼分別為 0,00,1,01,則電文 ‘ABACCDA’ 便為 ‘000011010’(共 9 位),但此編碼存在多義性:可譯為: ‘BBCCDA’、‘ABACCDA’、‘AAAACCACA’ 等。
譯碼的惟一性問題
要求任一字元的編碼都不能是另一字元編碼的字首,這種編碼稱為字首編碼(其實是非字首碼)。在編碼過程要考慮兩個問題,資料的最小冗餘編碼問題,譯碼的惟一性問題,利用最優二叉樹可以很好地解決上述兩個問題
用二叉樹設計二進位制字首編碼
以電文中的字元作為葉子結點構造二叉樹。然後將二叉樹中結點引向其左孩子的分支標 ‘0’,引向其右孩子的分支標 ‘1’; 每個字元的編碼即為從根到每個葉子的路徑上得到的 0, 1 序列。如此得到的即為二進位制字首編碼。
編碼: A:0,C:10,B:110,D:111
任意一個葉子結點都不可能在其它葉子結點的路徑中。
用哈夫曼樹設計總長最短的二進位制字首編碼
假設各個字元在電文中出現的次數(或頻率)為 wi ,其編碼長度為 li,電文中只有 n 種字元,則電文編碼總長為:
設計電文總長最短的編碼,設計哈夫曼樹(以 n 種字元出現的頻率作權),
由哈夫曼樹得到的二進位制字首編碼稱為哈夫曼編碼
例:如果需傳送的電文為 ‘ABACCDA’,即:A, B, C, D
的頻率(即權值)分別為 0.43, 0.14, 0.29, 0.14,試構造哈夫曼編碼。
編碼: A:0,C:10, B:110,D:111 。電文 ‘ABACCDA’ 便為 ‘0110010101110’(共 13 位)。
例:如果需傳送的電文為 ‘ABCACCDAEAE’,即:A, B, C, D, E 的頻率(即權值)分別為0.36, 0.1, 0.27, 0.1, 0.18,試構造哈夫曼編碼。
編碼: A:11,C:10,E:00,B:010,D:011 ,則電文 ‘ABCACCDAEAE’ 便為 ‘110101011101001111001100’(共 24 位,比 33 位短)。
譯碼 從哈夫曼樹根開始,對待譯碼電文逐位取碼。若編碼是“0”,則向左走;若編碼是“1”,則向右走,一旦到達葉子結點,則譯出一個字元;再重新從根出發,直到電文結束。電文為 “1101000” ,譯文只能是“CAT”
哈夫曼編碼演算法的實現
由於哈夫曼樹中沒有度為1的結點,則一棵有n個葉子的哈夫曼樹共有2×n-1個結點,可以用一個大小為2×n-1 的一維陣列存放哈夫曼樹的各個結點。 由於每個結點同時還包含其雙親資訊和孩子結點的資訊,所以構成一個靜態三叉連結串列。
1 //haffman 樹的結構 2 typedef struct 3 { 4 //葉子結點權值 5 unsigned int weight; 6 //指向雙親,和孩子結點的指標 7 unsigned int parent; 8 unsigned int lChild; 9 unsigned int rChild; 10 } Node, *HuffmanTree; 11 12 //動態分配陣列,儲存哈夫曼編碼 13 typedef char *HuffmanCode; 14 15 //選擇兩個parent為0,且weight最小的結點s1和s2的方法實現 16 //n 為葉子結點的總數,s1和 s2兩個指標引數指向要選取出來的兩個權值最小的結點 17 void select(HuffmanTree *huffmanTree, int n, int *s1, int *s2) 18 { 19 //標記 i 20 int i = 0; 21 //記錄最小權值 22 int min; 23 //遍歷全部結點,找出單節點 24 for(i = 1; i <= n; i++) 25 { 26 //如果此結點的父親沒有,那麼把結點號賦值給 min,跳出迴圈 27 if((*huffmanTree)[i].parent == 0) 28 { 29 min = i; 30 break; 31 } 32 } 33 //繼續遍歷全部結點,找出權值最小的單節點 34 for(i = 1; i <= n; i++) 35 { 36 //如果此結點的父親為空,則進入 if 37 if((*huffmanTree)[i].parent == 0) 38 { 39 //如果此結點的權值比 min 結點的權值小,那麼更新 min 結點,否則就是最開始的 min 40 if((*huffmanTree)[i].weight < (*huffmanTree)[min].weight) 41 { 42 min = i; 43 } 44 } 45 } 46 //找到了最小權值的結點,s1指向 47 *s1 = min; 48 //遍歷全部結點 49 for(i = 1; i <= n; i++) 50 { 51 //找出下一個單節點,且沒有被 s1指向,那麼i 賦值給 min,跳出迴圈 52 if((*huffmanTree)[i].parent == 0 && i != (*s1)) 53 { 54 min = i; 55 break; 56 } 57 } 58 //繼續遍歷全部結點,找到權值最小的那一個 59 for(i = 1; i <= n; i++) 60 { 61 if((*huffmanTree)[i].parent == 0 && i != (*s1)) 62 { 63 //如果此結點的權值比 min 結點的權值小,那麼更新 min 結點,否則就是最開始的 min 64 if((*huffmanTree)[i].weight < (*huffmanTree)[min].weight) 65 { 66 min = i; 67 } 68 } 69 } 70 //s2指標指向第二個權值最小的葉子結點 71 *s2 = min; 72 } 73 74 //建立哈夫曼樹並求哈夫曼編碼的演算法如下,w陣列存放已知的n個權值 75 void createHuffmanTree(HuffmanTree *huffmanTree, int w[], int n) 76 { 77 //m 為哈夫曼樹總共的結點數,n 為葉子結點數 78 int m = 2 * n - 1; 79 //s1 和 s2 為兩個當前結點裡,要選取的最小權值的結點 80 int s1; 81 int s2; 82 //標記 83 int i; 84 // 建立哈夫曼樹的結點所需的空間,m+1,代表其中包含一個頭結點 85 *huffmanTree = (HuffmanTree)malloc((m + 1) * sizeof(Node)); 86 //1--n號存放葉子結點,初始化葉子結點,結構陣列來初始化每個葉子結點,初始的時候看做一個個單個結點的二叉樹 87 for(i = 1; i <= n; i++) 88 { 89 90 //其中葉子結點的權值是 w【n】陣列來儲存 91 (*huffmanTree)[i].weight = w[i]; 92 //初始化葉子結點(單個結點二叉樹)的孩子和雙親,單個結點,也就是沒有孩子和雙親,==0 93 (*huffmanTree)[i].lChild = 0; 94 (*huffmanTree)[i].parent = 0; 95 (*huffmanTree)[i].rChild = 0; 96 }// end of for 97 //非葉子結點的初始化 98 for(i = n + 1; i <= m; i++) 99 { 100 (*huffmanTree)[i].weight = 0; 101 (*huffmanTree)[i].lChild = 0; 102 (*huffmanTree)[i].parent = 0; 103 (*huffmanTree)[i].rChild = 0; 104 } 105 106 printf("\n HuffmanTree: \n"); 107 //建立非葉子結點,建哈夫曼樹 108 for(i = n + 1; i <= m; i++) 109 { 110 //在(*huffmanTree)[1]~(*huffmanTree)[i-1]的範圍內選擇兩個parent為0 111 //且weight最小的結點,其序號分別賦值給s1、s2 112 select(huffmanTree, i-1, &s1, &s2); 113 //選出的兩個權值最小的葉子結點,組成一個新的二叉樹,根為 i 結點 114 (*huffmanTree)[s1].parent = i; 115 (*huffmanTree)[s2].parent = i; 116 (*huffmanTree)[i].lChild = s1; 117 (*huffmanTree)[i].rChild = s2; 118 //新的結點 i 的權值 119 (*huffmanTree)[i].weight = (*huffmanTree)[s1].weight + (*huffmanTree)[s2].weight; 120 121 printf("%d (%d, %d)\n", (*huffmanTree)[i].weight, (*huffmanTree)[s1].weight, (*huffmanTree)[s2].weight); 122 } 123 124 printf("\n"); 125 } 126 127 //哈夫曼樹建立完畢,從 n 個葉子結點到根,逆向求每個葉子結點對應的哈夫曼編碼 128 void creatHuffmanCode(HuffmanTree *huffmanTree, HuffmanCode *huffmanCode, int n) 129 { 130 //指示biaoji 131 int i; 132 //編碼的起始指標 133 int start; 134 //指向當前結點的父節點 135 int p; 136 //遍歷 n 個葉子結點的指示標記 c 137 unsigned int c; 138 //分配n個編碼的頭指標 139 huffmanCode=(HuffmanCode *)malloc((n+1) * sizeof(char *)); 140 //分配求當前編碼的工作空間 141 char *cd = (char *)malloc(n * sizeof(char)); 142 //從右向左逐位存放編碼,首先存放編碼結束符 143 cd[n-1] = '\0'; 144 //求n個葉子結點對應的哈夫曼編碼 145 for(i = 1; i <= n; i++) 146 { 147 //初始化編碼起始指標 148 start = n - 1; 149 //從葉子到根結點求編碼 150 for(c = i, p = (*huffmanTree)[i].parent; p != 0; c = p, p = (*huffmanTree)[p].parent) 151 { 152 if( (*huffmanTree)[p].lChild == c) 153 { 154 //從右到左的順序編碼入陣列內 155 cd[--start] = '0'; //左分支標0 156 } 157 else 158 { 159 cd[--start] = '1'; //右分支標1 160 } 161 }// end of for 162 //為第i個編碼分配空間 163 huffmanCode[i] = (char *)malloc((n - start) * sizeof(char)); 164 165 strcpy(huffmanCode[i], &cd[start]); 166 } 167 168 free(cd); 169 //列印編碼序列 170 for(i = 1; i <= n; i++) 171 { 172 printf("HuffmanCode of %3d is %s\n", (*huffmanTree)[i].weight, huffmanCode[i]); 173 } 174 175 printf("\n"); 176 } 177 178 int main(void) 179 { 180 HuffmanTree HT; 181 HuffmanCode HC; 182 int *w,i,n,wei,m; 183 184 printf("\nn = " ); 185 186 scanf("%d",&n); 187 188 w=(int *)malloc((n+1)*sizeof(int)); 189 190 printf("\ninput the %d element's weight:\n",n); 191 192 for(i=1; i<=n; i++) 193 { 194 printf("%d: ",i); 195 fflush(stdin); 196 scanf("%d",&wei); 197 w[i]=wei; 198 } 199 200 createHuffmanTree(&HT, w, n); 201 creatHuffmanCode(&HT,&HC,n); 202 203 return 0; 204 }
補充:樹的計數
已知先序序列和中序序列可確定一棵唯一的二叉樹;
已知後序序列和中序序列可確定一棵唯一的二叉樹;
已知先序序列和後序序列不能確定一棵唯一的二叉樹。