最小堆實現哈夫曼樹的構造及哈夫曼編碼、解碼
以下程式的演算法思想主要來自於浙江大學陳越老師主編的資料結構一書。最大堆(最小堆思想差不多)(之後會寫一篇部落格介紹),這裡主要講講哈夫曼樹的定義及實現。
Huffman Tree
相關概念:
結點的路徑長度:從根結點到該結點的路徑上分支的數目。
樹的路徑長度:樹中每個結點的路徑長度之和。(從樹根到其餘各結點的路徑長度之和)
結點的帶權路徑長度(WPL):結點的路徑長度與該結點所帶權值的乘積。
樹的帶權路徑長度:樹中所有葉子結點的帶權路徑長度之和。
設一棵樹有n個葉子結點,每個葉結點帶有權值wk,結點的路徑長度為lk,則樹的帶權路徑長度可以表示為:
哈夫曼樹的定義: 假設有n個權值,構造有n個葉子結點的二叉樹,每個葉子結點的權值是n個權值之一,這樣的二叉樹可以構造很多棵,其中必有一棵是帶權路徑長度最小的,這棵二叉樹就稱為最優二叉樹或哈夫曼樹。
構造Huffman樹的步驟:
(1) 根據給定的n個權值,構造n棵只有一個根結點的二叉樹,n個權值分別是這些二叉樹根結點的權;
(2) 設F是由這n棵二叉樹構成的集合,在F中選取兩棵根結點權值最小的樹作為左、右子樹,構造成一顆新的二叉樹,置新二叉樹根結點的權值等於左、右子樹根結點的權值之和。為了使得到的哈夫曼樹的結構唯一,規定根結點權值最小的作為新二叉樹的左子樹;
(3) 從F中刪除(2)中作為左、右子樹的兩棵二叉樹,並將新構造的二叉樹加入到集合F中;
(4) 重複(2)、(3)步,直到F中只含一棵二叉樹為止,這棵二叉樹便是要建立的哈夫曼樹。
說明:n個結點需要進行n-1次合併,每次合併都產生一個新的結點,最終的Huffman樹共有2n-1個結點。
為了便於抽取最小權值的子樹,在樹的構造過程中使用了最小堆及其插入,刪除等操作。這裡堆中的元素是一個加了權值的樹結點的指標。
說了這麼多,也該實現程式碼了。100好幾十行的程式碼擼下來,感覺自己神清氣爽,等等...,編譯沒毛病,執行。。。呃呃呃,什麼!!!我就輸入了一個整數(表示接下來我要輸入多少個權值),程式就跟我說拜拜了!!!於是我就只能進入搬磚工Debug模式了。。。除錯程式嘛,當然要一步步來,首先當然是除錯主程式裡的第一個函式(CreateMinHeap(2*N)),我就在這個函式前後寫了一句printf("ok\n"),一執行,,,O no,只有一個ok,當然這個函式有毛病,於是我進去這個函式定義裡面一看,這老哥沒毛病啊,左看右看,找不出來。。。想了老久,只好啟動碼農搬救兵(大佬-傑)模式,什麼!!!你也看不出來???蒼天啊,我的內心其實還有點【小竊喜】,救兵也沒看出來,說明我這個問題不是一般的問題,我自己沒看出來也還ok。我的肚子這時候又出來加速我的放棄了,於是我就打算吃飯,打道回府。。。
故事到這裡難道就結束了嗎?no no no。在去食堂的路上,我想著作為一名黨神聖的搬磚工,不能就這麼放棄了啊,不然將來怎麼建設黨的偉大事業。我就在路上想啊想啊,越想越得勁,感覺思路越來越清晰,到了食堂打完飯一坐下,沒吃幾口飯,猛一拍大腿【臥槽】,想出來了。。。當時恨不得當場把電腦掏出來除錯一波【別人會不會把我當傻子。。。】。主要問題是我在動態申請一個指標陣列的時候給這個指標陣列分配的記憶體空間(這時候指標陣列的元素可能是空指標,或者不知道指向一個什麼地方),我卻在後面直接使用了這個指標指向的物件(結構體)裡面的元素,這樣肯定不行啊!!!比如說你指向了一片虛無的空間或者是指向一個不知道是什麼地方,然後對另一個人說,這個地方有好多妹子,快去撩妹吧,撩你個大頭鬼哦,你個糟老頭子壞得很~,沒給我地址怎麼找這個地方!!!你要是指著女生宿舍說這個還差不多(不過咱們也不能真進去女生宿舍啊,進去了也別說是我教唆的【哈哈】)。於是我只好先一個個申請一個地址空間,再把他們賦值給這個指標陣列就行了。(這裡告訴我們一個道理,當你調不出程式碼的時候,出去吃個飯,或者散散步說不定就想出來了,大腦持續高負荷工作也是會累的)
修改好之後執行程式沒毛病,再也不會沒執行完就崩潰了,說明程式大體上沒什麼毛病(諸如溢位、指標指向不明),只不過演算法上的實現是需要程式設計師自己檢查的。
你們以為這就結束了嗎?那你們也太小看程式設計師為什麼高薪了(那是因為他們總寫出滿帶bug的程式,然後他們成天到晚Debug,累得不行【過頭了,過頭了】),然後有人問,那直接寫出沒bug的程式唄,說的輕巧,大可一試,小程式倒還好,那種成百上千行的程式碼難道能完全沒問題嗎?哈哈,偏題了,偏題了。接下來這個問題是我也不曾想過會出現的,當然因為程式執行結果不對,我只好對寫的函式一個個在主函式裡面進行分別實現,當然,你很容易看出沒問題的函式就沒必要了。最後,終於定位在了哈夫曼建構函式這個函式裡面(因為其他函式都沒問題),但我非常不解啊,因為這個函式是教科書上有的,我原封不動的抄下來的,不應該有問題啊(函式演算法我也理解的差不多),我找啊找,各種Debug模式開啟,終於發現for迴圈裡的判決條件有問題!!!(下面程式中也標記了)for循壞裡面的操作會改變判決條件,【臥槽】,這可不行啊,於是做了一個小小修改就完美解決了。(這件事告訴我們一個道理,即使是書本上的知識,也是會有毛病的。。。)修改之後,編譯、執行、程式結果終於出來了。。。
#include<stdio.h>
#include<stdlib.h>
typedef struct TreeNode *HuffmanTree;
typedef struct TreeNode{
int Weight; //權值
HuffmanTree Left;
HuffmanTree Right;
}HuffmanNode;
#define MinData -1 //隨著堆元素的具體值而改變
typedef struct HeapStruct *MinHeap;
struct HeapStruct{
HuffmanTree *data; //儲存堆元素的陣列 儲存時從下標1開始
int Size; //堆的當前元素的個數
int Capacity; //堆的最大容量
};
HuffmanTree NewHuffmanNode();
MinHeap CreateMinHeap(int MaxSize);
bool Insert(MinHeap H,HuffmanTree item);
HuffmanTree DeleteMin(MinHeap H);
MinHeap BuildMinHeap(MinHeap H);
HuffmanTree Huffman(MinHeap H);
void PreOrderTraversal(HuffmanTree BST);
int main()
{
int i,N;
MinHeap h;
HuffmanTree T,BT = NULL;
printf("請輸入葉子結點的個數:\n");
scanf("%d",&N);
h = CreateMinHeap(2*N); //建立最小堆
printf("請輸入%d個葉子結點對應的權值:\n",N);
for(i=1; i<=N; i++){/*最小堆元素賦值*/
T = NewHuffmanNode();
scanf("%d",&(T->Weight));
h->data[++(h->Size)] = T;
}
BT = Huffman(h); //構造哈夫曼樹
printf("先序遍歷此哈夫曼樹的權值:\n");
PreOrderTraversal(BT); //先序遍歷此哈夫曼樹
return 0;
}
/*哈夫曼樹構造演算法*/
HuffmanTree Huffman(MinHeap H)
{/*假設H->Size個權值已經存在H->data[]->Weight裡*/
int i,num;
HuffmanTree T;
BuildMinHeap( H ); //將H->data[]按權值調整為最小堆
/*此處必須將H->Size的值交給num,因為後面做DeleteMin()和 Insert()函式會改變H->Size的值*/
num = H->Size;
for(i=1; i<num; i++){ //做 H->Size-1次合併 //此處教科書有問題!原書直接為H->Size
T = NewHuffmanNode(); //建立一個新的根結點
T->Left = DeleteMin(H); //從最小堆中刪除一個節點,作為新T的左子結點
T->Right = DeleteMin(H); //從最小堆中刪除一個節點,作為新T的右子結點
T->Weight = T->Left->Weight+T->Right->Weight; //計算新權值
//printf("%3d 0x%x 0x%x\n",T->Weight,T->Left,T->Right);
Insert(H,T); //將新T插入到最小堆
}
T = DeleteMin(H);
return T;
}
/*****先序遍歷*****/
void PreOrderTraversal(HuffmanTree BST)
{
if( BST ){
printf("%d ",BST->Weight); //先訪問根節點
PreOrderTraversal(BST->Left); //再訪問左子樹
PreOrderTraversal(BST->Right); //最後訪問右子樹
}
}
HuffmanTree NewHuffmanNode()
{
HuffmanTree BST = (HuffmanTree)malloc(sizeof(HuffmanNode));
BST->Weight = 0;
BST->Left = BST->Right = NULL;
return BST;
}
MinHeap CreateMinHeap(int MaxSize)
{ /*建立容量為MaxSize的最小堆*/
MinHeap H = (MinHeap)malloc(sizeof(struct HeapStruct));
H->data = (HuffmanTree *)malloc((MaxSize+1) * sizeof(HuffmanTree));
H->Size = 0;
H->Capacity = MaxSize;
HuffmanTree T = NewHuffmanNode();
T->Weight = MinData; /*定義哨兵-為小於堆中所有可能元素權值的值,便於以後更快操作*/
H->data[0] = T;
return H;
}
bool IsFull(MinHeap H)
{
return (H->Size == H->Capacity);
}
bool IsEmpty(MinHeap H)
{
return (H->Size == 0);
}
/*插入演算法-將新增結點插入到從其父結點到根結點的有序序列中*/
bool Insert(MinHeap H,HuffmanTree item)
{/*將元素item插入到最小堆H中,其中H->data[0]已被定義為哨兵*/
int i;
if( IsFull(H) ){
printf("最小堆已滿\n");
return false;
}
i = ++H->Size; //i指向插入後堆中的最後一個元素的位置
for(; H->data[i/2]->Weight > item->Weight; i/=2) //無哨兵,則增加判決條件 i>1
H->data[i] = H->data[i/2]; //向下過濾結點
H->data[i] = item; //將item插入
return true;
}
HuffmanTree DeleteMin(MinHeap H)
{/*從最小堆H中取出權值為最小的元素,並刪除一個結點*/
int parent,child;
HuffmanTree MinItem,temp = NULL;
if( IsEmpty(H) ){
printf("最小堆為空\n");
return NULL;
}
MinItem = H->data[1]; //取出根結點-最小的元素-記錄下來
/*用最小堆中的最後一個元素從根結點開始向上過濾下層結點*/
temp = H->data[H->Size--]; //最小堆中最後一個元素,暫時將其視為放在了根結點
for(parent=1; parent*2<=H->Size; parent=child){
child = parent*2;
if((child != H->Size) && (H->data[child]->Weight > H->data[child+1]->Weight)){/*有右兒子,並且左兒子權值大於右兒子*/
child++; //child指向左右兒子中較小者
}
if(temp->Weight > H->data[child]->Weight){
H->data[parent] = H->data[child]; //向上過濾結點-temp存放位置下移到child位置
}else{
break; //找到了合適的位置
}
}
H->data[parent] = temp; //temp存放到此處
return MinItem;
}
MinHeap BuildMinHeap(MinHeap H)
{/*這裡假設所有的H->Size個元素已經存在H->data[]中*/
/*本函式將H->data[]中的元素調整,使其滿足堆的有序性*/
int i,parent,child;
HuffmanTree temp;
for(i=H->Size/2;i>0;i--){ //從最後一個父結點開始,直到根結點
temp = H->data[i];
for(parent=i; parent*2<=H->Size; parent=child){
/*向下過濾*/
child = parent*2;
if((child != H->Size) && (H->data[child]->Weight > H->data[child+1]->Weight)){/*有右兒子,並且左兒子權值大於右兒子*/
child++; //child指向左右兒子中較小者
}
if(temp->Weight > H->data[child]->Weight){
H->data[parent] = H->data[child]; //向上過濾結點-temp存放位置下移到child位置
}else{
break; //找到了合適的位置
}
}/*結束內部for迴圈對以H->data[i]為根的子樹的調整*/
H->data[parent] = temp; //temp(原H->data[i])存放到此處
}
return H;
}
執行結果為:
哈夫曼編碼解碼
哈夫曼樹構造完成以後,哈夫曼編碼就很容易完成了。我們只要把哈夫曼樹每個結點的左分支標記為0,右分支標記為1,某一字元(權值)的編碼可通過組合從根結點到該字元結點(葉結點)的路徑上所標記的0,1得到。解碼的話我們只要根據編碼序列從根結點開始出發,0則想左子樹走,1則向右子樹走,直到走到一個葉子結點出,就可輸出此葉子結點的字元值;每輸出一個葉子結點,就從根結點從新開始走。具體如下圖所示:
因為每個葉子結點的權值可能一樣,為了區分、並且我們接下來要對字元進行 編解碼,所以我們給樹結點結構在加入一個字元元素。如下:
typedef struct TreeNode *HuffmanTree;
typedef struct TreeNode{
char ch; //要編碼的字元
int Weight; //權值
HuffmanTree Left;
HuffmanTree Right;
}HuffmanNode;
具體的編解碼演算法如下:
/************遞迴進行哈夫曼編碼*************/
void HuffmanCode(HuffmanTree BST,int depth) //depth為目前編碼到哈夫曼樹的深度(層次)
{
static int code[10]; //編碼空間
if( BST ){
if( (BST->Left == NULL) && (BST->Right == NULL)){ //找到了葉結點
printf("字元%c對應權值為%d的哈夫曼編碼為:",BST->ch,BST->Weight);
for(int i=0; i<depth; i++){
printf("%d",code[i]);
}
printf("\n");
}else{
code[depth] = 0; //往左子樹方向編碼為0
HuffmanCode(BST->Left,depth+1);
code[depth] = 1; //往右子樹方向編碼為1
HuffmanCode(BST->Right,depth+1);
}
}
}
/*******************哈夫曼解碼*********************/
void HuffmanDecode(char ch[],HuffmanTree BST) //ch[] 要解碼的序列
{
int cnt;
int num[100];
HuffmanTree temp;
for(int i=0; i<strlen(ch); i++){
if(ch[i] == '0'){
num[i] = 0;
}else{
num[i] = 1;
}
}
if( BST ){
cnt = 0; //計數已經解碼0101串的長度
while(cnt < strlen(ch)){
temp = BST;
while((temp->Left != NULL ) && (temp->Right != NULL)){
if(num[cnt] == 0){
temp = temp->Left;
}else{
temp = temp->Right;
}
cnt++;
}
printf("%c",temp->ch); //輸出解碼後對應結點的字元
}
}
}
將這兩個函式放入到主程式裡面,可以得到:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef struct TreeNode *HuffmanTree;
typedef struct TreeNode{
char ch; //要編碼的字元
int Weight; //權值
HuffmanTree Left;
HuffmanTree Right;
}HuffmanNode;
#define MinData -10 //隨著堆元素的具體值而改變
typedef struct HeapStruct *MinHeap;
struct HeapStruct{
HuffmanTree *data; //儲存堆元素的陣列 儲存時從下標1開始
int Size; //堆的當前元素的個數
int Capacity; //堆的最大容量
};
HuffmanTree NewHuffmanNode();
MinHeap CreateMinHeap(int MaxSize);
bool Insert(MinHeap H,HuffmanTree item);
HuffmanTree DeleteMin(MinHeap H);
MinHeap BuildMinHeap(MinHeap H);
HuffmanTree Huffman(MinHeap H);
void PreOrderTraversal(HuffmanTree BST);
void HuffmanCode(HuffmanTree BST,int depth);
void HuffmanDecode(char ch[],HuffmanTree BST);
int main()
{
int i,N;
MinHeap h;
HuffmanTree T,BT = NULL;
printf("請輸入葉子結點的個數:\n");
scanf("%d",&N);
h = CreateMinHeap(2*N); //建立最小堆 //N個葉子節點最終形成的哈夫曼樹最多有2N-1個樹結點
printf("請輸入%d個葉子結點對應的權值:\n",N);
for(i=1; i<=N; i++){/*最小堆元素賦值*/
T = NewHuffmanNode();
scanf("%d",&(T->Weight));
//scanf("%d-%c",&(T->Weight),&(T->ch));
h->data[++(h->Size)] = T;
}
char string[100];
printf("請連續輸入這%d個葉子結點各自代表的字元:\n",N);
getchar(); //吸收上面的換行符
gets(string);
for(i=1; i<=h->Size; i++){/*最小堆元素賦值*/
h->data[i]->ch= string[i-1];
}
BT = Huffman(h); //構造哈夫曼樹
printf("先序遍歷此哈夫曼樹的權值:\n");
PreOrderTraversal(BT); //先序遍歷此哈夫曼樹
printf("\n");
HuffmanCode(BT,0);
printf("要解碼嗎?請輸入二進位制編碼序列:\n");
char ch[100];
gets(ch);
printf("解碼結果為:\n");
HuffmanDecode(ch,BT);
return 0;
}
/*哈夫曼樹構造演算法*/
HuffmanTree Huffman(MinHeap H)
{/*假設H->Size個權值已經存在H->data[]->Weight裡*/
int i,num;
HuffmanTree T;
BuildMinHeap( H ); //將H->data[]按權值調整為最小堆
/*此處必須將H->Size的值交給num,因為後面做DeleteMin()和 Insert()函式會改變H->Size的值*/
num = H->Size;
for(i=1; i<num; i++){ //做 H->Size-1次合併 //此處教科書有問題!
T = NewHuffmanNode(); //建立一個新的根結點
T->Left = DeleteMin(H); //從最小堆中刪除一個節點,作為新T的左子結點
T->Right = DeleteMin(H); //從最小堆中刪除一個節點,作為新T的右子結點
T->Weight = T->Left->Weight+T->Right->Weight; //計算新權值
// printf("%3d 0x%x 0x%x\n",T->Weight,T->Left,T->Right);
Insert(H,T); //將新T插入到最小堆
}
T = DeleteMin(H);
return T;
}
/************遞迴進行哈夫曼編碼*************/
void HuffmanCode(HuffmanTree BST,int depth) //depth為目前編碼到哈夫曼樹的深度(層次)
{
static int code[10]; //編碼空間
if( BST ){
if( (BST->Left == NULL) && (BST->Right == NULL)){ //找到了葉結點
printf("字元%c對應權值為%d的哈夫曼編碼為:",BST->ch,BST->Weight);
for(int i=0; i<depth; i++){
printf("%d",code[i]);
}
printf("\n");
}else{
code[depth] = 0; //往左子樹方向編碼為0
HuffmanCode(BST->Left,depth+1);
code[depth] = 1; //往右子樹方向編碼為1
HuffmanCode(BST->Right,depth+1);
}
}
}
/*******************哈夫曼解碼*********************/
void HuffmanDecode(char ch[],HuffmanTree BST) //ch[] 要解碼的序列
{
int cnt;
int num[100];
HuffmanTree temp;
for(int i=0; i<strlen(ch); i++){
if(ch[i] == '0'){
num[i] = 0;
}else{
num[i] = 1;
}
}
if( BST ){
cnt = 0; //計數已經解碼0101串的長度
while(cnt < strlen(ch)){
temp = BST;
while((temp->Left != NULL ) && (temp->Right != NULL)){
if(num[cnt] == 0){
temp = temp->Left;
}else{
temp = temp->Right;
}
cnt++;
}
printf("%c",temp->ch); //輸出解碼後對應結點的字元
}
}
}
/*****先序遍歷*****/
void PreOrderTraversal(HuffmanTree BST)
{
if( BST ){
printf("%d ",BST->Weight); //先訪問根節點
PreOrderTraversal(BST->Left); //再訪問左子樹
PreOrderTraversal(BST->Right); //最後訪問右子樹
}
}
HuffmanTree NewHuffmanNode()
{
HuffmanTree BST = (HuffmanTree)malloc(sizeof(HuffmanNode));
BST->Weight = 0;
BST->Left = BST->Right = NULL;
return BST;
}
MinHeap CreateMinHeap(int MaxSize)
{ /*建立容量為MaxSize的最小堆*/
MinHeap H = (MinHeap)malloc(sizeof(struct HeapStruct));
H->data = (HuffmanTree *)malloc((MaxSize+1) * sizeof(HuffmanTree));
H->Size = 0;
H->Capacity = MaxSize;
HuffmanTree T = NewHuffmanNode();
T->ch = '\0'; //空字元
T->Weight = MinData; /*定義哨兵-為小於堆中所有可能元素權值的值,便於以後更快操作*/
H->data[0] = T;
return H;
}
bool IsFull(MinHeap H)
{
return (H->Size == H->Capacity);
}
bool IsEmpty(MinHeap H)
{
return (H->Size == 0);
}
/*插入演算法-將新增結點插入到從其父結點到根結點的有序序列中*/
bool Insert(MinHeap H,HuffmanTree item)
{/*將元素item插入到最小堆H中,其中H->data[0]已被定義為哨兵*/
int i;
if( IsFull(H) ){
printf("最小堆已滿\n");
return false;
}
i = ++H->Size; //i指向插入後堆中的最後一個元素的位置
for(; H->data[i/2]->Weight > item->Weight; i/=2) //無哨兵,則增加判決條件 i>1
H->data[i] = H->data[i/2]; //向下過濾結點
H->data[i] = item; //將item插入
return true;
}
HuffmanTree DeleteMin(MinHeap H)
{/*從最小堆H中取出權值為最小的元素,並刪除一個結點*/
int parent,child;
HuffmanTree MinItem,temp = NULL;
if( IsEmpty(H) ){
printf("最小堆為空\n");
return NULL;
}
MinItem = H->data[1]; //取出根結點-最小的元素-記錄下來
/*用最小堆中的最後一個元素從根結點開始向上過濾下層結點*/
temp = H->data[H->Size--]; //最小堆中最後一個元素,暫時將其視為放在了根結點
for(parent=1; parent*2<=H->Size; parent=child){
child = parent*2;
if((child != H->Size) && (H->data[child]->Weight > H->data[child+1]->Weight)){/*有右兒子,並且左兒子權值大於右兒子*/
child++; //child指向左右兒子中較小者
}
if(temp->Weight > H->data[child]->Weight){
H->data[parent] = H->data[child]; //向上過濾結點-temp存放位置下移到child位置
}else{
break; //找到了合適的位置
}
}
H->data[parent] = temp; //temp存放到此處
return MinItem;
}
MinHeap BuildMinHeap(MinHeap H)
{/*這裡假設所有的H->Size個元素已經存在H->data[]中*/
/*本函式將H->data[]中的元素調整,使其滿足堆的有序性*/
int i,parent,child;
HuffmanTree temp;
for(i=H->Size/2;i>0;i--){ //從最後一個父結點開始,直到根結點
temp = H->data[i];
for(parent=i; parent*2<=H->Size; parent=child){
/*向下過濾*/
child = parent*2;
if((child != H->Size) && (H->data[child]->Weight > H->data[child+1]->Weight)){/*有右兒子,並且左兒子權值大於右兒子*/
child++; //child指向左右兒子中較小者
}
if(temp->Weight > H->data[child]->Weight){
H->data[parent] = H->data[child]; //向上過濾結點-temp存放位置下移到child位置
}else{
break; //找到了合適的位置
}
}/*結束內部for迴圈對以H->data[i]為根的子樹的調整*/
H->data[parent] = temp; //temp(原H->data[i])存放到此處
}
return H;
}
執行結果為: