資料結構知識整理 - 哈夫曼樹與哈夫曼編碼
主要內容
基本概念
1)路徑:由一個結點到另一個結點之間的所有分支共同構成。
2)路徑長度:結點之間的分支數目。
3)樹的路徑長度:從樹的根結點到其他所有結點的路徑長度之和。
4)權:賦予某一實體的值。在資料結構中,實體包括結點和邊,所以對應有結點權和邊權。
5)結點的帶權路徑長度:結點與樹的根結點之間的路徑長度與結點權的乘積。
6)樹的帶權路徑長度:所有葉結點的帶權路徑長度之和。
7)哈夫曼樹(Huffman Tree):
構造思路
假設有8個結點n1~n8。
權值越大的結點應該離樹的根結點越近,因而先找到樹中最小的兩個結點n1和n2,並把它們作為最後一層的葉結點。
最小的兩個結點作為新結點n9的左、右子樹根結點後,又從n3~n9選出最小的兩個結點n4和n6,並把它們作為新結點n10的左、右子樹根結點。
不斷重複上面的操作,直至n1~n8都成為哈夫曼樹的葉結點。
最終得到的哈夫曼樹沒有度為1的結點,因此一棵有n個葉結點的哈夫曼樹,一共有2n-1個結點,可以儲存在一個大小為2n-1的一維陣列中。
為了方便表示,增加一個單元的陣列長度,從1號位置開始使用,所以陣列長度為2n
將葉結點n1~n8儲存在前面的1~8號位置,其餘結點儲存在9~2n-1號位置。
儲存結構
哈夫曼樹是特殊的、帶權路徑長度最小的二叉樹,當然可以使用傳統的二叉連結串列儲存。
但在樹的儲存結構篇裡已經提過,順序儲存結構僅適用於完全二叉樹,而二叉連結串列存在許多空指標域,再加上哈夫曼樹的構造具有規律,而且結點數目確定,所以可以用一個確定的一維陣列儲存。
除了0號位置空出來,其餘每個位置存放一個結點。每個陣列元素包括四個資訊:權值、父結點編號、左子樹根結點編號和右子樹根結點編號。
typedef struct { int weight; int parent; int lchild, rchild; } HFNode, *HFBiTree; /*陣列長度根據給定結點數n來確定*/
構造演算法
void CreatHuffmanTree(HFBiTree &HT, int n)
{
if(n <= 1) return ERROR; /*結點數不足,報錯*/
int m = 2*n-1; /*哈夫曼樹結點數*/
HT = new HFNode[m+1]; /*分配陣列空間*/
/*-------初始化-------*/
for(int i = 1; i <= m; i++)
{
HT[i].parent = HT[i].lchild = HT[i].rchild = 0;
}
for(int i = 1; i <= n; i++)
cin>>HT[i];
/*-----構建哈夫曼樹-----*/
for(int i = n+1; i <= m; i++)
{
Select(HT, i-1, s1, s2);
/*從HT的1~i-1號位置中選出兩個權值最小的結點,並返回它們的編號s1和s2*/
HT[s1].parent = HT[s2].parent = i;
HT[i].lchild = s1;
HT[1].rchild = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight;
}
}
哈夫曼編碼的引入
哈夫曼樹在通訊、編碼和資料壓縮等技術領域有著廣泛的應用,其中哈夫曼編碼是構造通訊碼的一個典型應用。
在資料通訊、資料壓縮時,需要將資料檔案轉換成由二進位制字元0/1組成的二進位制串,這個過程稱為編碼。
編碼的方案一般有三種:
等長編碼方案 | 不等長編碼方案 | 哈夫曼編碼方案 | |||
字元 | 編碼 | 字元 | 編碼 | 字元 | 編碼 |
a | 00 | a | 0 | a | 0 |
b | 01 | b | 01 | b | 10 |
c | 10 | c | 010 | c | 110 |
d | 11 | d | 111 | d | 111 |
為什麼要引入哈夫曼編碼?
1)為了使頻率高的字元儘可能採用更短的編碼,而頻率低的字元可以採用稍長的編碼,構造一種不等長編碼以獲得更好的空間效率。這也是檔案壓縮技術的核心思想。
2)但不等長編碼不能隨意設計,任何一個字元的編碼都不可以是另一個字元的編碼的字首,不合理的編碼將有可能導致在解碼時出現二義性。
為了合理地設計不等長編碼,同時考慮各個字元的使用頻率,哈夫曼編碼出現了。
在哈夫曼樹中,權值越大的結點離根結點越近,路徑長度越短,這與使用頻率越高,編碼越短的思想相同。
在哈夫曼樹中,根結點到任何一個葉結點的路徑都不可能是到另一個葉結點路徑的一部分,這與任何一個字元的編碼都不可以是另一個字元的編碼的字首的條件相同。
因此我們可以根據每個字元出現的概率,構造出一棵哈夫曼樹。
求哈夫曼編碼
設左分支為0,右分支為1。
我們已經知道由根結點到葉結點的路徑可以求出一個字元的哈夫曼編碼。但是考慮到哈夫曼樹結點的儲存結構,每個結點的資訊包括:權值、父結點編號、左子樹根結點編號和右子樹根結點編號。
如果由根結點遍歷到葉結點,訪問下一個結點時總是得考慮左、右兩個方向,顯然,這樣的求解效率很低。
反過來,如果從葉結點遍歷到根結點,訪問一下個結點,即父結點時,只需要考慮一個方向,顯然這樣的求解目的性更強,效率也更高。
typedef char **HFCode; /*動態分配陣列儲存哈夫曼編碼表(指標陣列)*/
void CreatHuffmanCode(HFBiTree &HT, HFCode &HC, int n)
{
/*從葉結點回溯到根結點求每個字元的哈夫曼編碼,儲存在編碼表HC中*/
HC = new char *[n+1]; /*0號單元不用,分配儲存n個字元編碼的編碼表空間*/
char *cd = new char[n]; /*分配臨時存放每個字元編碼的動態陣列空間*/
cd[n-1] = '\0'; /*編碼結束符*/
for(int i = 1; i <= n; i++) /*逐個字元求哈夫曼編碼*/
{
/*哈夫曼編碼是從根結點開始到葉結點,但因為演算法中從葉結點向上回溯,所以編碼也要從後往前寫*/
start = n-1; /*start開始時指向最後,即編碼結束符位置*/
int c = i, f = HT[i].parent; /*f是結點c的父結點編號*/
while(f != 0) /*從葉結點向上回溯,直到根結點(f == 0)*/
{
start--; /*回溯一次,start指向前一個位置*/
if(HT[f].lchild == c) cd[start] = '0'; /*左分支,程式碼0*/
else cd[start] = '1'; /*右分支,程式碼1*/
c = f; f = HT[f].parent; /*繼續往上回溯*/
} /*求出第i個字元的編碼*/
HC[i] = new char[n-start]; /*為第i個字元編碼分配空間*/
strcpy(HC[i], &cd[start]); /*將求得的編碼從臨時空間cd複製到HC的當前行中*/
}
delete cd; /*釋放臨時空間*/
}