哈夫曼树可用来构造最优编码,用于信息传输、数据压缩等方面,哈夫曼树是一种应用广泛的二叉树
在介绍哈夫曼树之前,先给出几个基本概念。
问题 1: 什么样的二叉树的路径长度 PL 最小?
一棵树的路径长度为 0 结点至多只有 1 个 (根)
路径长度为 1 结点至多只有 2 个 (两个孩子)
路径长度为 2 结点至多只有 4 个
依此类推: 路径长度为 K 结点至多只有 2k个
所以 n 个结点二叉树其路径长度至少等于如下图所示序列的前 n 项之和。
所以完全二叉树具有最小路径长度的性质,但不具有惟一性。有些树并不是完全二叉树,但也可以具有最小路径长度。如下图所示。
问题 2:什么样的带权的树路径长度最小?
例如:给定一个权值序列{2,3,4,7},可构造如下图所示的多种二叉树的形态。
哈夫曼树:它是由 n 个带权叶子结点构成的所有二叉树中带权路径长度最短的二叉树。 因为这种树最早由哈夫曼(Huffman)研究,所以称为哈夫曼树,又叫最优二叉树,图 6.38 (c)所示的二叉树就是一棵哈夫曼树。
构造哈夫曼树的算法步骤如下:
(1) 初始化:用给定的 n 个权值 {w1, w2, … , wn} 对应的由 n 棵二叉树构成的森林 F={T1,T2, …,Tn},其中每一棵二叉树 Ti (1≤i≤n)都只有一个权值为 wi的根结点,其 左、右子树为空。
(2) 找最小树:在森林 F 中选择两棵根结点权值最小的二叉树,作为一棵新二叉树的左、 右子树,标记新二叉树的根结点权值为其左、右子树的根结点权值之和。
(3) 删除与加入:从 F 中删除被选中的那两棵二叉树,同时把新构成的二叉树加入到森林 F 中。
(4) 判断:重复(2)、(3)操作,直到森林中只含有一棵二叉树为止,此时得到的这棵二 叉树就是哈夫曼树。
直观地看,先选择权小的,所以权小的结点被放置在树的较深层,而权较大的离根较近, 这样自然在哈夫曼树中权越大叶子离根越近,这样一来,在计算树的带权路径长度时,自然 会具有最小带权路径长度,这种生成算法就是一种典型的贪心法。
手工构造的方法也非常简单:给定一组权值 { w1, w2, … , wn},用 n 个权值构成 n 棵单 根树的森林 F;将 F={T1,T2,… ,Tn}按权值从小到大排列; 取 T1和 T2合并组成一棵树, 使其根结点的权值 T=T1+T2,再按大小插入 F,重复此过程,直到只有一棵树为止。
给定一组权值 {7,4,3,2 },用上述方法构造哈夫曼树,将得到图 6.38(c)所示的 二叉树。
哈夫曼树是一种二叉树,当然可以采用前面已经介绍过的通用存储方法,而哈夫曼树 是求某种最优方案,由于哈夫曼树中没有度为 1 的结点,因此一棵有 n 个叶子的哈夫曼树 共有 2×n-1 个结点,可以用一个大小为 2×n-1 的一维数组存放哈夫曼树的各个结点。 由于每个结点同时还包含其双亲信息和孩子结点的信息,所以构成一个静态三叉链表。
静态三叉链表中:每个结点的结构如下图所示:
各结点存储在一维数组中,0 号单元不使用,从 1 号位置开始使用。 下图给出了一棵二叉树及其静态三叉链表。
对于有 n 个叶子结点的哈夫曼树,结点总数为 2n-1 个,为实现方便,将叶子结点集 中存储在前面部分 1~n 个位置,而后面的 n-1 个位置中存储其余非叶子结点。
用静态三叉链表实现的哈夫曼树类型定义如下:
#define N 20 /* 叶子结点的最大值。*/
#define M 2*N-1 /* 所有结点的最大值。*/
typedef struct
{
int weight ; /* 结点的权值*/
int parent ; /* 双亲的下标*/
int LChild ; /* 左孩子结点的下标*/
int RChild ; /* 右孩子结点的下标*/
} HTNode, HuffmanTree[M+1]; /* HuffmanTree 是一个结构数组类型,0 号单元不用。 */
【算法描述】 创建哈夫曼树算法
void CrtHuffmanTree(HuffmanTree ht, int w[ ], int n)
{ /*构造哈夫曼树 ht[M+1], w[ ]存放 n 个权值。*/
for(i=1;i<=n;i++)
ht[i] ={ w[i],0,0,0}; /* 1 ~ n 号单元存放叶子结点,初始化*/
m=2*n-1;
for(i=n+1;i<=m;i++)
ht[i] ={0,0,0,0}; /* n+1 ~ m 号单元存放非叶结点,初始化 */
for(i=n+1; i<=m; i++) /*创建非叶结点,建哈夫曼树*/
{
select(ht, i-1, s1, s2); /* 在 ht[1] ~ ht[i-1] 的范围内选择两个 parent 为 0 且 weight 最小的结点,其序号分别赋值给 s1、s2 返回 */
ht [i].weight= ht
[s1].weight+ ht [s2].weight;
ht [s1].parent=i;
ht [s2].parent=i;
ht [i].LChild=s1;
ht [i].RChild=s2;
} /*哈夫曼树建立完毕*/
}
该算法分成两大部分,其中第一部分是初始化,先初始化 ht 的前 1-n 号元素,存放叶子结 点(相当初始森林),它们都没有双亲与孩子。再初始化 ht 的后 n-1 个(从 n+1~2n-1)非叶 结点元素;第二部分为实施选择、删除合并 n-1 次(相当步骤(2)-(4)):选择是从当前 森林中(在森林中树的根结点的双亲为 0)选择两棵根的权值最小的树;删除合并是将选到 的两棵树的根权和存入 ht 的当前最前面的空闲元素中(相当于合并树中新结点),并置入相应的双亲与孩子的位置指示。
例 数据传送中的二进制编码。要传送数据 state, seat, act, tea, cat, set, a ,eat ,如何使传送的 长度最短?