樹的形狀也各不相同。圖634 是3 棵最優二叉樹的例子。它們的共同特點是:帶權值的
結點都是葉子結點。權值越小的結點,其到根結點的路徑越長。構造最優二叉樹的方法
如下:
(1) 將每個帶有權值的結點作爲一棵僅有根結點的二叉樹,樹的權值爲結點的權值;
(2) 將其中兩棵權值最小的樹組成一棵新二叉樹,新樹的權值爲兩棵樹的權值之和;
(3) 重複(2),直到所有結點都在一棵二叉樹上。這棵二叉樹就是最優二叉樹。
最優二叉樹的左右子樹是可以互換的,因爲這不影響樹的帶權路徑長度。當結點的權
值差別大到一定程度,最優二叉樹就形成了如圖634(b)所示的“一邊倒”的形狀。有些
書稱最優二叉樹都是這種“一邊倒”的形狀是不對的。這通過計算二叉樹的帶權路徑長度
是否最短就可看出。當所有結點的權值一樣,或其權值差別很小,最優二叉樹就形成了如
圖634(c)所示的完全二叉樹的形狀。葉子結點的路徑長度近似相等。
最優二叉樹除了葉子結點就是度爲2 的結點,沒有度爲1 的結點。這樣才使得樹的帶
權路徑長度最短。根據二叉樹的性質3,最優二叉樹的結點數爲葉子數的2 倍減1。
// c6-7.h 赫夫曼樹和赫夫曼編碼的存儲結構(見圖6.35)
typedef struct
{
unsigned int weight;
unsigned int parent,lchild,rchild;
}HTNode,*HuffmanTree; // 動態分配數組存儲赫夫曼樹
typedef char **HuffmanCode; // 動態分配數組存儲赫夫曼編碼表
c6-7.h 定義的二叉樹結構是我們在前邊沒有討論過的,但它特別適合建立赫夫曼樹。
赫夫曼樹是由多棵二叉樹(森林)組合成而的一棵樹。這種二叉樹結構既適合表示樹,也適
合表示森林。赫夫曼樹結點的結構包括權值、雙親及左右孩子,雙親值爲0 的是根結點,
左右孩子值均爲0 的是葉子結點。這種二叉樹結構是動態生成的順序結構。當葉子結點數
確定,赫夫曼樹的結點數也確定。由圖636(d)可見,建成的赫夫曼樹除0 號結點空間不
用外,每個結點空間都沒空置。
// func6-1.cpp 程序 algo6-1.cpp和algo6-2.cpp要調用
int min(HuffmanTree t,int i)
{ // 返回i個結點中權值最小的樹的根結點序號,函數select()調用
int j,flag;
unsigned int k=UINT_MAX; // 取k爲不小於可能的值(無符號整型最大值)
for(j=1;j<=i;j++)
if(t[j].weight<k&&t[j].parent==0) // t[j]是樹的根結點
k=t[j].weight,flag=j;
t[flag].parent=1; // 給選中的根結點的雙親賦1,避免第2次查找該結點
return flag;
}
void select(HuffmanTree t,int i,int &s1,int &s2)
{ // 在i個結點中選擇2個權值最小的樹的根結點序號,s1爲其中序號小的那個
int j;
s1=min(t,i);
s2=min(t,i);
if(s1>s2)
{
j=s1;
s1=s2;
s2=j;
}
}
// algo6-1.cpp 求赫夫曼編碼。實現算法6.12的程序
#include"c1.h"
#include"c6-7.h"
#include"func6-1.cpp"
void HuffmanCoding(HuffmanTree &HT,HuffmanCode &HC,int *w,int n) // 算法6.12
{ // w存放n個字符的權值(均>0),構造赫夫曼樹HT,並求出n個字符的赫夫曼編碼HC
int m,i,s1,s2,start;
unsigned c,f;
HuffmanTree p;
char *cd;
if(n<=1)
return;
m=2*n-1;
HT=(HuffmanTree)malloc((m+1)*sizeof(HTNode)); // 0號單元未用
for(p=HT+1,i=1;i<=n;++i,++p,++w)
{
(*p).weight=*w;
(*p).parent=0;
(*p).lchild=0;
(*p).rchild=0;
}
for(;i<=m;++i,++p)
(*p).parent=0;
for(i=n+1;i<=m;++i) // 建赫夫曼樹
{ // 在HT[1~i-1]中選擇parent爲0且weight最小的兩個結點,其序號分別爲s1和s2
select(HT,i-1,s1,s2);
HT[s1].parent=HT[s2].parent=i;
HT[i].lchild=s1;
HT[i].rchild=s2;
HT[i].weight=HT[s1].weight+HT[s2].weight;
}
// 從葉子到根逆向求每個字符的赫夫曼編碼
HC=(HuffmanCode)malloc((n+1)*sizeof(char*));
// 分配n個字符編碼的頭指針向量([0]不用)
cd=(char*)malloc(n*sizeof(char)); // 分配求編碼的工作空間
cd[n-1]='\0'; // 編碼結束符
for(i=1;i<=n;i++)
{ // 逐個字符求赫夫曼編碼
start=n-1; // 編碼結束符位置
for(c=i,f=HT[i].parent;f!=0;c=f,f=HT[f].parent)
// 從葉子到根逆向求編碼
if(HT[f].lchild==c)
cd[--start]='0';
else
cd[--start]='1';
HC[i]=(char*)malloc((n-start)*sizeof(char));
// 爲第i個字符編碼分配空間
strcpy(HC[i],&cd[start]); // 從cd複製編碼(串)到HC
}
free(cd); // 釋放工作空間
}
void main()
{
HuffmanTree HT;
HuffmanCode HC;
int *w,n,i;
printf("請輸入權值的個數(>1): ");
scanf("%d",&n);
w=(int*)malloc(n*sizeof(int));
printf("請依次輸入%d個權值(整型):\n",n);
for(i=0;i<=n-1;i++)
scanf("%d",w+i);
HuffmanCoding(HT,HC,w,n);
for(i=1;i<=n;i++)
puts(HC[i]);
}
代碼的運行結果(以教科書圖6.24 爲例,如圖636 所示):
請輸入權值的個數(>1): 4
請依次輸入4個權值(整型):
7 5 2 4
0
10
110
111
圖636 是運行過程的圖解。初始狀態下(見圖636(b)),權值分別爲7、5、2、4
的4 個結點是4 棵獨立的樹(根結點)。它們沒有雙親,也沒有左右孩子。反覆查找權值最
小的兩棵樹,並把它們合併成一棵樹,其權值爲兩樹的權值之和。最後,所有結點合併成
一棵赫夫曼樹(見圖636(d))。
算法6.12(在algo6-1.cpp 中)在找到兩個無雙親且權值最小的結點後,將序號小的結
點作爲左子樹,序號大的結點作爲右子樹,如果不按這個規則,赫夫曼編碼的形式會改
變。但碼長不會改變,仍然是赫夫曼編碼。
// algo6-2.cpp 實現算法6.13的程序
#include"c1.h"
#include"c6-7.h"
#include"func6-1.cpp"
void HuffmanCoding(HuffmanTree &HT,HuffmanCode &HC,int *w,int n) // 前半部分爲算法6.12
{ // w存放n個字符的權值(均>0),構造赫夫曼樹HT,並求出n個字符的赫夫曼編碼HC
int m,i,s1,s2; // 此句與algo6-1.cpp不同
unsigned c,cdlen; // 此句與algo6-1.cpp不同
HuffmanTree p;
char *cd;
if(n<=1)
return;
m=2*n-1;
HT=(HuffmanTree)malloc((m+1)*sizeof(HTNode)); // 0號單元未用
for(p=HT+1,i=1;i<=n;++i,++p,++w)
{
(*p).weight=*w;
(*p).parent=0;
(*p).lchild=0;
(*p).rchild=0;
}
for(;i<=m;++i,++p)
(*p).parent=0;
for(i=n+1;i<=m;++i) // 建赫夫曼樹
{ // 在HT[1~i-1]中選擇parent爲0且weight最小的兩個結點,其序號分別爲s1和s2
select(HT,i-1,s1,s2);
HT[s1].parent=HT[s2].parent=i;
HT[i].lchild=s1;
HT[i].rchild=s2;
HT[i].weight=HT[s1].weight+HT[s2].weight;
}
// 以下爲算法6.13,無棧非遞歸遍歷赫夫曼樹,求赫夫曼編碼,以上同算法6.12
HC=(HuffmanCode)malloc((n+1)*sizeof(char*));
// 分配n個字符編碼的頭指針向量([0]不用)
cd=(char*)malloc(n*sizeof(char)); // 分配求編碼的工作空間
c=m;
cdlen=0;
for(i=1;i<=m;++i)
HT[i].weight=0; // 遍歷赫夫曼樹時用作結點狀態標誌
while(c)
{
if(HT[c].weight==0)
{ // 向左
HT[c].weight=1;
if(HT[c].lchild!=0)
{
c=HT[c].lchild;
cd[cdlen++]='0';
}
else if(HT[c].rchild==0)
{ // 登記葉子結點的字符的編碼
HC[c]=(char *)malloc((cdlen+1)*sizeof(char));
cd[cdlen]='\0';
strcpy(HC[c],cd); // 複製編碼(串)
}
}
else if(HT[c].weight==1)
{ // 向右
HT[c].weight=2;
if(HT[c].rchild!=0)
{
c=HT[c].rchild;
cd[cdlen++]='1';
}
}
else
{ // HT[c].weight==2,退回
HT[c].weight=0;
c=HT[c].parent;
--cdlen; // 退到父結點,編碼長度減1
}
}
free(cd);
}
void main()
{ // 主程序同algo6-1.cpp
HuffmanTree HT;
HuffmanCode HC;
int *w,n,i;
printf("請輸入權值的個數(>1): ");
scanf("%d",&n);
w=(int *)malloc(n*sizeof(int));
printf("請依次輸入%d個權值(整型):\n",n);
for(i=0;i<=n-1;i++)
scanf("%d",w+i);
HuffmanCoding(HT,HC,w,n);
for(i=1;i<=n;i++)
puts(HC[i]);
}
代碼的運行結果(以教科書例62 爲例):
請輸入權值的個數(>1): 8
請依次輸入8個權值(整型):
5 29 7 8 14 23 3 11
0110
10
1110
1111
110
00
0111
010