哈夫曼樹的基本概念
哈夫曼樹(Huffman Tree),又叫最優二叉樹,指的是對於一組具有確定權值的葉子結點的具有最小帶權路徑長度的二叉樹。
(1)路勁(Path):從樹中的一個結點到另一個結點之間的分支構成兩個結點間的路徑。
(2)路徑長度(Path Length):路徑上的分支樹。
(3)樹的路徑長度(Path Length of Tree):從樹的根結點到每個結點的路徑長度之和。在結點數目相同的二叉樹中,完全二叉樹的路徑長度最短。
(4)結點的權(Weight of Node):在一些應用中,賦予樹中結點的一個有實際意義的樹。
(5)結點的帶權路徑長度(Weight Path Length of Node):從該結點到樹的根結點的路徑長度與該結點的權的乘積。
(6)樹的帶權路徑長度(WPL):樹中所有葉子結點的帶權路徑長度之和,記為
在下圖所示的四棵二叉樹,都有4個葉子結點,其權值分別1、2、3、4,他們的帶權路徑長度分別為:
(a)WPL = 1 x 2 + 2 x 2 + 3 x 2 + 4 X 2 = 20
(b)WPL = 1 x 1 + 2 x 2 + 3 x 3 + 4 x 3 = 28
(c)WPL = 1 x 3 + 2 x 3 + 3 x 2 + 4 x 1 = 19
(d)WPL = 2 x 1 + 1 x 2 + 3 x 3 + 4 x 3 = 29
其中,(c)圖所示的二叉樹的帶權路徑長度最小,這棵樹就是哈夫曼樹。可以驗證,哈夫曼樹的帶權路徑長度最小。
------------------------------------------------------------------------------------------------
哈夫曼樹的構造
//(假設有n個權值,則構造出的哈夫曼樹有n個葉子結點。)
對於已知的一組葉子的權值W 1 ,W 2...... ,W n
①首先把 n 個葉子結點看做 n 棵樹(僅有一個結點的二叉樹),把它們看做一個森林。
②在森林中把權值最小和次小的兩棵樹合並成一棵樹,該樹根結點的權值是兩棵子樹權值之和。這時森林中還有 n-1 棵樹。
③重復第②步直到森林中只有一棵為止。此樹就是哈夫曼樹。
n 個葉子構成的哈夫曼樹其帶權路徑長度唯一嗎?確實唯一。樹形唯一嗎?不唯一。因為將森林中兩棵權值最小和次小的子棵合並時,哪棵做左子樹,哪棵做右子樹並不嚴格限制。選擇不同時畫出的樹形有所不同,但 WPL 值相同。為了便於討論交流在此提倡權值較小的做左子樹, 權值較大的做右子。
需要考慮的問題:
(1) 用什么樣的存儲結構;
(2) 怎么選擇選擇最小和次小權值的樹;
(3) 怎么存放合成的新樹
//存儲結構:
順序存儲(靜態數組/堆分配空間):數組長度為帶權值的元素個數n額的倆倍,其中0號單元空置,1到n號單元存放只有當個節點的樹(沒有父節點,parent為0),
n+1之后的元素存放分別以前n個節點中的最小權重節點及次小權重節點為做做孩子和右孩子的雙親
//節點:
typedef struct HuffmanTree{
char data;
int weight;
int parent,lchild,rchild; parent用來標記該節點是否為沒有父節點的樹(一個節點)
}hafnode,*haftree;
哈夫曼樹的構造:(堆分配存儲)
void select(haftree ht,int n,int &s1,int &s2){
for(i=1;i<=n;i++){ //所有沒有父節點的節點中以第一個節點為最小權重節點
if(ht[i]->parent==0){
s1=i;
break;
}
}
for(j=i+1;j<=n;j++){ //找到最小權值的節點
if(ht[j]->parent==0&&ht[j]->weightweight)
si=j;
}
for(i=1;i<=n;i++){
if(ht[i]->parent==0&&i!=s1){ //所有沒有父節點的節點中以第一個節點為次小權重節點
s2=i;
break;
}
}
for(j=i+1;j<=n;j++){ //找到次小權值的節點
if(ht[j]->parent==0&&ht[j]->weightweight)
s2=j;
}
}
void createhaftree(haftree &ht,int n,int quan[],char value[]){ //quan存放各節點的權重,value存放各節點 的值。0號單元均設空
int s1,s2;
m=2*n-1;
ht=(Haftree)malloc(sizeof(hafnode)*(m+1));//構造2n個節點長度的數組,第一個設空。前n存n個有權值的 節點,后n-1個存放每次最
//小與次小的父節點。n-1次就可以將n個節點全部合並完成,故設n-1個節點長度
for(i=1;i<=n;i++,ht++,quan++,value++){ //前n個節點進行初始化
ht->data=value[i];
ht->weight=quan[i];
ht->parent=ht->lchild=ht->rchild=0;
}
for(i=n+1;i<=m;i++,ht++){ //后n-1個節點初始化
ht->weight=0;
ht->parent=ht->lchild=ht->rchild=0;
}
for(i=n+1;i<=m;i++){ //循環構造哈夫曼樹
select(ht,i-1,s1,s2);
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;
}
}
注:構造得到的哈夫曼樹存在數組中,前n個元素為葉子節點(帶權節點),最后一個元素為根節點
-------------------------------------------------------------------------------------------------
哈夫曼樹的應用:
1.用於通信編碼
在通信及數據傳輸中多采用二進制編碼。為了使電文盡可能的縮短,可以對電文中每個字符
出現的次數進行統計。設法讓出現次數多的字符的二進制碼短些,而讓那些很少出現的字符的二
進制碼長一些。假設有一段電文,其中用到 4 個不同字符A,C,S,T,它們在電文中出現的
次數分別為 7 , 2 , 4 , 5 。把 7 , 2 , 4 , 5 當做 4 個葉子的權值構造哈夫曼樹如圖
6.24(a) 所示。在樹中令所有左分支取編碼為 0 ,令所有右分支取編碼為1。將從根結點起到某
個葉子結點路徑上的各左、右分支的編碼順序排列,就得這個葉子結點所代表的字符的二進制編
碼,如圖 6.24(b) 所示。
這些編碼拼成的電文不會混淆,因為每個字符的編碼均不是其他編碼的前綴,這種編碼稱做前
綴編碼。
關於信息編碼是一個復雜的問題,還應考慮其他一些因素。比如前綴編碼每個編碼的長度不相
等,譯碼時較困難。還有檢測、糾錯問題都應考慮在內。這里僅對哈夫曼樹舉了一個應用實例。
(1)編碼:
算法思想: (將帶權字符構建成哈弗曼樹再進行編碼,編碼需從葉子節點發出一條走向根節點的路徑,
約定左孩子分支為0,右孩子分支為1.)
void hafcoding(haftree ht,char *hc){ //hc指向一個字符指針數組,數組長度為n,n個指針分別指
// 向n個葉子節點的字符進行字符編碼后得到的編碼串
hc=(char *)malloc(n*sizeof(char *));
char *cd; //cd指向一個字符數組,臨時存放字符的編碼串,因為是從葉子節點
cd=(char *)malloc(n*sizeof(char)); //出發,則編碼串是倒序逐個插入字符數組的,結尾有'\0',讀取時順序。
cd[n-1]='\0';
for(i=1;i<=n;i++){ //對n個葉子節點的字符進行編碼
start=n-1;
for(child=i,parent=ht[i]->parent;parent!=0;child=parent,parent=ht[parent]->parent){ //對葉子節點的字符進行編碼
if(child==ht[parent]->lchild) //從葉子節點出發到根節點
cd[--start]='0'; //child是為了判定該節點是其父節點的左孩子還是右孩子
else
cd[--start]='1';
}
hc[i-1]=(char *)malloc((n-start)*sizeof(char)); //字符指針數組的每個元素指向一個剛好存放其對應葉子節點編碼串長度的字符數組
strcpy(hc[i-1],&cd[start]);
}
free(cd);
}
(2)譯碼
算法:輸入一串以哈夫曼編碼方式編碼的字符串,從根節點出發走向葉子節點,找到葉子節點
后輸出該節點的字符然后繼續讀取編碼串,直至讀完
void decode(haftree ht){
char s[100];
gets(s);
i=m;
j=0;
while(s[j]!='\0'){ //遍歷字符數組/編碼串
if(s[j]=='0')
i=ht[i]->lchild; //走向左孩子
else
i=ht[i]->rchild //走向右孩子
if(ht[i]->lchild==NULL){ //看是否該節點為葉子節點
printf("%c",ht[i]->data); //是的話輸出,並返回根節點
i=m;
}
j++; //無論是否找到葉子節點都讀取下一個編碼串字符
}
}

