從週五開始學習霍夫曼樹,一直到今天終於完成,期間遇到了各種各樣的棘手的問題,通過一遍遍在紙上分析每一步的具體狀態得以解決。現在對學習霍夫曼樹的過程加以記錄
首先介紹霍夫曼樹
霍夫曼樹(Huffman Tree),又稱最優二叉樹,是一類帶權路徑長度最短的樹。假設有n個權值{w1,w2,…,wn},如果構造一棵有n個葉子節點的二叉樹,而這n個葉子節點的權值是{w1,w2,…,wn},則所構造出的帶權路徑長度最小的二叉樹就被稱爲赫夫曼樹。
這裏補充下樹的帶權路徑長度的概念。樹的帶權路徑長度指樹中所有葉子節點到根節點的路徑長度與該葉子節點權值的乘積之和,如果在一棵二叉樹中共有n個葉子節點,用Wi表示第i個葉子節點的權值,Li表示第i個也葉子節點到根節點的路徑長度,則該二叉樹的帶權路徑長度 WPL=W1*L1 + W2*L2 + … Wn*Ln。
根據節點的個數以及權值的不同,赫夫曼樹的形狀也各不相同,赫夫曼樹具有如下特性:
對於同一組權值,所能得到的霍夫曼樹不一定是唯一的。
赫夫曼樹的左右子樹可以互換,因爲這並不影響樹的帶權路徑長度。
帶權值的節點都是葉子節點,不帶權值的節點都是某棵子二叉樹的根節點。
權值越大的節點越靠近赫夫曼樹的根節點,權值越小的節點越遠離赫夫曼樹的根節點。
赫夫曼樹中只有葉子節點和度爲2的節點,沒有度爲1的節點。
一棵有n個葉子節點的赫夫曼樹共有2n-1個節點。
赫夫曼樹的構建步驟如下:
1、將給定的n個權值看做n棵只有根節點(無左右孩子)的二叉樹,組成一個集合HT,每棵樹的權值爲該節點的權值。
2、從集合HT中選出2棵權值最小的二叉樹,組成一棵新的二叉樹,其權值爲這2棵二叉樹的權值之和。
3、將步驟2中選出的2棵二叉樹從集合HT中刪去,同時將步驟2中新得到的二叉樹加入到集合HT中。
4、重複步驟2和步驟3,直到集合HT中只含一棵樹,這棵樹便是赫夫曼樹。
假如給定如下5個權值:
則按照以上步驟,可以構造出如下面左圖所示的赫夫曼樹,當然也可能構造出如下面右圖所示的赫夫曼樹,這並不是唯一的。
Huffman編碼
赫夫曼樹的應用十分廣泛,比如衆所周知的在通信電文中的應用。在等傳送電文時,我們希望電文的總長儘可能短,因此可以對每個字符設計長度不等的編碼,讓電文中出現較多的字符采用儘可能短的編碼。爲了保證在譯碼時不出現歧義,我們可以採取如下圖所示的編碼方式:
即左分支編碼爲字符0,右分支編碼爲字符1,將從根節點到葉子節點的路徑上分支字符組成的字符串作爲葉子節點字符的編碼,這便是赫夫曼編碼。我們根據上面左圖可以得到各葉子節點的赫夫曼編碼如下:
權值爲5的也自己節點的赫夫曼編碼爲:11
權值爲4的也自己節點的赫夫曼編碼爲:10
權值爲3的也自己節點的赫夫曼編碼爲:00
權值爲2的也自己節點的赫夫曼編碼爲:011
權值爲1的也自己節點的赫夫曼編碼爲:010
而對於上面右圖,則可以得到各葉子節點的赫夫曼編碼如下:
權值爲5的也自己節點的赫夫曼編碼爲:00
權值爲4的也自己節點的赫夫曼編碼爲:01
權值爲3的也自己節點的赫夫曼編碼爲:10
權值爲2的也自己節點的赫夫曼編碼爲:110
權值爲1的也自己節點的赫夫曼編碼爲:111
下面給出C語言實現
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
/*定義霍夫曼樹節點*/
typedef struct HTNode{
int parent;/*記錄雙親*/
int Lchild;/*左右子樹*/
int Rchild;
int Weight;/*記錄權重*/
}HTNode;
typedef struct HTNode * HuffmanTree;
typedef char ** HuffmanCode;
/*在前k棵樹種找到權重最小的樹*/
int Min(HuffmanTree HT,int k){
int i=0,min_weight=0,min=0;
/*找出第一個雙親存在的節點,將其權值賦值給min_weight*/
/*注意此處不能直接將HT[0].weight賦給min_weight,原因是如果HT[0].weight最小,那麼在第一次構造二叉樹時就會被選走,而後續的每一輪選擇最小權值構造二叉樹的比較
還是先用HT[0].weight的值來進行判斷,這樣又會再次將其選走,從而產生邏輯上的錯誤。*/
while(HT[i].parent!=-1)
i++;
min_weight=HT[i].Weight;
min=i;
for(i;i<k;i++){
if(HT[i].Weight<min_weight&&HT[i].parent==-1){
min_weight=HT[i].Weight;
min=i;
}
}
/*找到最小權重的樹,將其雙親置爲1*/
/*!!!!!注意這的HT的下標!!!!!一晚上才找出這個小問題,別寫成HT[i]!!!!!!*/
HT[min].parent=1;
return min;
}
/*從前k棵樹中選出權重最小的兩棵樹,將其序號賦給min1和min2*/
Status SelectMin(HuffmanTree HT,int k,int &min1,int &min2){
min1=Min(HT,k);
min2=Min(HT,k);
return OK;
}
/*創建一課霍夫曼樹,-1表示不存在*/
/*wet爲一個記錄權重的數組,類型爲int*/
HuffmanTree CreateHuffmanTree(HuffmanTree HT,int *wet,int n){
int i=0;
int total=2*n-1;/*有n個數據需要編碼,即有n個葉子節點,也就有n-1個度爲2的節點,總節點數爲n+n-1=2*n-1*/
/*初始狀態下,前n個節點的雙親,左右子樹應該均爲-1,權重爲對應的權重*/
/*用HT的前n個分量存儲n棵樹(由n個待編碼的數據組成)的森林*/
/*申請total個int組成的動態數組*/
HT=(HuffmanTree)malloc(total*sizeof(HTNode));
if(!HT)
return ERROR;
for(i=0;i<n;i++){
HT[i].Lchild=-1;
HT[i].parent=-1;
HT[i].Rchild=-1;
HT[i].Weight=*wet;
wet++;
}
/*對n到total的分量進行初始化*/
for(i;i<total;i++){
HT[i].Lchild=-1;
HT[i].Rchild=-1;
HT[i].parent=-1;
HT[i].Weight=0;
}
/*用HT的後n-1個分量存儲霍夫曼樹*/
/*調用函數SelectMin找出森林中兩棵權重最小的樹*/
int min1=0,min2=0;
for(i=n;i<total;i++){
SelectMin(HT,i,min1,min2);
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;
}
return HT;
}
/*從葉子節點開始逆向進行霍夫曼編碼*/
/*HC用來儲存霍夫曼編碼值*/
Status HuffmanCoding(HuffmanTree HT,HuffmanCode &HC,int n){
/*HC本身是一個char類型數組的指針,其指向n個char類型的地址,所以給HC分配內存應該寫成下面那樣*/
HC=(HuffmanCode)malloc(n*sizeof(char *));
if(!HC)
return ERROR;
/*聲明一個動態數組code,用來臨時存儲霍夫曼編碼,數組含有n-1個霍夫曼碼,加上一個'\0'終止符正好是n個元素,所以分配內存時*/
char *code;
code=(char *)malloc(n*sizeof(char));
if(!code)
return ERROR;
code[n-1]='\0';/*讓最後一個元素爲終止符*/
int i=0;
for(i=0;i<n;i++){
int current=i;
int father=HT[i].parent;
int start=n-1;
while(father!=-1){
if(current==HT[father].Lchild)
code[--start]='0';
else
code[--start]='1';
current=father;
father=HT[father].parent;
}
/*HC[i]用於最終存儲霍夫曼碼,是char類型的數組,有n個char類型的數據*/
HC[i]=(char *)malloc((n-start)*sizeof(char));
if(!HC[i])
return ERROR;
/*從臨時空間中複製到HC[i]中*/
strcpy(HC[i],code+start);
}
/*釋放臨時存儲空間*/
free(code);
code=NULL;
return OK;
}
int main(void){
int amount=0,i=0;
int *wet=(int *)malloc(amount*sizeof(int));
printf("請輸入要編碼的字符個數(個數爲整型且>1)");
scanf("%d",&amount);
while(amount<=1){
printf("字符個數必須大於1\n");
scanf("%d",&amount);
}
printf("請輸入要編碼的字符的權值");
for(i=0;i<amount;i++){
scanf("%d",wet+i);
}
HuffmanTree HT;
HT=CreateHuffmanTree(HT,wet,amount);
HuffmanCode HC;
HuffmanCoding(HT,HC,amount);
for(i=0;i<amount;i++){
puts(HC[i]);
}
free(wet);
return OK;
}
參考了http://blog.csdn.net/ns_code/article/details/19174553這篇博客
2016-05-09更新:
今日在複習霍夫曼樹的過程中,發現自己忽視了一些細節問題,造成了一些錯誤,下面加以記錄並反思
問題一:出現在函數Min中
int Min(HuffmanTree HT,int k){
int min=0,min_weight=0,i=0;
/*注意此處是先尋找雙親不存在的節點再賦值,給min_weight賦值應該在循環以外*/
while(HT[i].parent!=-1)
i++;
min_weight=HT[i].weight;
min=i;
for(i;i<k;i++){
if(HT[i].weight<min_weight&&HT[i].parent==-1){
min_weight=HT[i].weight;
min=i;
}
}
/*賦值之後切記要將其雙親賦值爲1,否則會出現錯誤*/
HT[min].parent=1;
return min;
}
問題二:出現在CreateHuffmanTree函數中
HuffmanTree CreateHuffmanTree(HuffmanTree HT,int * wet,int amount){
int i=0,min_weight=0,min=0;
int total=2*amount-1;
/*注意此處分配內存時,sizeof()中應該是HTNode!!要對HT的本質進行理解,HT是一棵樹!*/
HT=(HuffmanTree)malloc(total*sizeof(HTNode));
if(!HT)
return ERROR;
for(i=0;i<amount;i++){
HT[i].Lchild=-1;
HT[i].parent=-1;
HT[i].Rchild=-1;
HT[i].weight=*wet;
wet++;
}
for(i;i<total;i++){
HT[i].Lchild=-1;
HT[i].Rchild=-1;
HT[i].parent=-1;
HT[i].weight=0;
}
int min1=0,min2=0;
/*注意此處i的初始值,要對這個過程加以理解,霍夫曼樹是用amount之後的分量來存儲的,故不能寫i=0*/
for(i=amount;i<total;i++){
SelectMin(HT,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;/*注意新生成的節點的權值爲選出的兩個最小的節點的權值之和*/
}
return HT;
}
這個問題主要是對建立霍夫曼樹的過程理解不夠透徹,導致在爲HT分配內存時寫錯大小,和循環過程中將i的初始值誤寫爲0,今後應當注意。
問題三:出現在HuffmanCoding函數中
Status HuffmanCoding(HuffmanTree HT,HuffmanCode &HC,int amount){
int i=0;
/*要先給HC分配內存。HC存儲了amount個指向霍夫曼碼的指針*/
HC=(HuffmanCode)malloc(amount*sizeof(char *));
if(!HC)
return ERROR;
char * code;
code=(char *)malloc(amount*sizeof(char));
if(!code)
return ERROR;
code[amount-1]='\0';
for(i=0;i<amount;i++){
int current=i;
int start=amount-1;
int father=HT[i].parent;
while(father!=-1){
if(current==HT[father].Lchild)
code[--start]='0';
else
code[--start]='1';
current=father;
father=HT[father].parent;
}
/*HC[i]是HC中若干個存儲單元之一,存儲着一個具體字符的霍夫曼碼,所以分配內存空間時sizeof()中爲char*/
HC[i]=(char *)malloc((amount-start)*sizeof(char));
if(!HC[i])
return ERROR;
strcpy(HC[i],code+start);
}
free(code);
code=NULL;
}
對HC的用途理解不夠到位,再次強調,HC是一個存儲了若干個指向霍夫曼碼的指針的數組,HC[i]則用於存儲具體字符的霍夫曼碼
問題四:反覆出現在主函數之中,必須加以記錄,認真反省
int main(void){
HuffmanTree HT;
int amount=0;
printf("請輸入需要編碼的字符個數");
scanf("%d",&amount);
int i=0;
int * wet=(int *)malloc(amount*sizeof(int));
printf("請輸入每個字符的權重");
for(i=0;i<amount;i++){
scanf("%d",wet+i);
}
/*此處忘記寫HT=,導致未能成功建立霍夫曼樹,由於這裏要改變函數形參的值,一般情況考慮傳入指針變量,但這個函數如果寫成指針又太複雜,
很容易出錯,故這裏使用return把建立好的霍夫曼樹直接返回,會方便許多,但是切記要把返回值賦給HT!!!*/
HT=CreateHuffmanTree(HT,wet,amount);
HuffmanCode HC;
HuffmanCoding(HT,HC,amount);
for(i=0;i<amount;i++){
puts(HC[i]);
}
free(wet);
}