最優二叉樹(赫夫曼樹、赫夫曼樹和赫夫曼編碼的存儲結構)

最優二叉樹是帶權路徑長度最短的二叉樹。根據結點的個數、權值的不同,最優二叉
樹的形狀也各不相同。圖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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章