哈夫曼树
1、一些基本概念
路径:
指从一个结点到另一个结点之间的分支序列。
路径长度:
指从一个结点到另一个结点所经过的分支数目。
结点的权:
给树的每个结点赋予一个具有某种实际意义的实数。
带权路径长度:
从树根到某一结点的路径长度与该结点的权的乘积。
树的带权路径长度:
树中所有叶子结点的带权路径长度之和。
研究路径长度PL和带权路径长度WPL目的在于寻找最优。
思考:什么样的二叉树的路径长度PL最小?
路径长度为K结点至多只有
2
k
2^k
2k个(满二叉树),所以n个结点二叉树其路径长度至少等于如下序列的前n项之和。
结点n对应的路径长度至少为[
log
2
n
\log_2n
log2n],所以前n项之和最小为
∑
k
=
1
n
[
log
2
k
]
\displaystyle\sum_{k=1}^{n}[\log_2k]
k=1∑n[log2k].
完全二叉树的路径长度等于
∑
k
=
1
h
[
log
2
k
]
\displaystyle\sum_{k=1}^{h}[\log_2k]
k=1∑h[log2k](h为树的深度),具有最小路径长度的性质,但不具有唯一性。
思考:什么样的树带权路径长度最小?
给定权值序列,带权路径长度最小的二叉树,即为哈夫曼树。
2、构造哈夫曼树
哈夫曼树:又叫最优二叉树,由n个带权叶子结点构成的带权路径长度WPL最短的二叉树。
构造哈夫曼树的算法步骤:
1)初始化:用给定的n个权值{w1,w2, … ,wn}对应的n个结点构成n棵二叉树的森林F={T1,T2, …,Tn},其中每一棵二叉树Ti (1≤i≤n)都只有一个权值为wi的根结点,其左、右子树为空。
2)找最小树:在森林F中选择两棵根结点权值最小的二叉树,作为一棵新二叉树的左、右子树,标记新二叉树的根结点权值为其左右子树的根结点权值之和。
3)删除与加入:从F中删除被选中的那两棵二叉树,同时把新构成的二叉树加入到森林F中。
4)判断:重复2)、3)操作,直到森林中只含有一棵二叉树为止,此时得到的这棵二叉树就是哈夫曼树。
直观地看,在哈夫曼树中权越大的叶子离根越近,则其具有最小带权路径长度,是一种典型的贪心法。
哈夫曼树的手工构造的方法也非常简单:
给定数列{W1…Wn},以n个权值构成n棵树的森林F;将F={T1…Tn}按权从小到大排列;取T1和T2合并组成一棵树,使其根结点的权值T=T1+T2,再按大小插入F,反复此过程直到只有一棵树为止。
3、哈夫曼树的类型定义
(1)存储结构
n个叶结点,n-1个非叶结点,共有2n-1个结点。除最后一非叶(分支)结点,每个非叶有且仅有一个叶孩子;
存为2n-1个元素的一维数组:静态三叉链表。
类型定义:
#define N 20 //叶结点数最大值
#define M 2*N-1 //叶结点数最大值
typedef struct
{
int weight;
int parent;
int LChild;
int Rchild;
}HTNode, HuffmanTree[M + 1]; //0号单元不用
4. 哈夫曼树创建算法实现
void CrtHuffmanTree(HuffmanTree ht , int w[ ], int n)
/*w存放n个权值,构造哈夫曼树ht */
{
int m, i, s1, s2;
m = 2 * n - 1;
for(i = 1; i <= n; i++)
ht[i] = {w[i], 0, 0, 0};/*叶结点初始化*/
for(i = n + 1; i <= m; i++)
ht[i] = {0, 0, 0, 0};/*非叶结点初始化*/
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;
}
}
(左边是初态,右边是终态)。
哈夫曼编码
1、哈夫曼编码的概念
哈夫曼树最典型的应用:哈夫曼编码。
编码:使用二进制来表达信息。
定长编码:
判断题答案:1位;
选择题答案:2位;
问答题:编码文字,如ASCII码。
不定长编码:
为缩短信息的长度,节约存储和传输开销,可采用不定长编码:
使用频度高的字符编为较短的编码;
是数据压缩技术的基本思想。
如何不定长编码,最优化问题:
(1)前缀编码:任一编码都不是其他任何编码的前缀(最左子串);
(2)哈夫曼编码:
对一颗具有n个叶子的哈夫曼树,每个左分支赋予0,右分支赋予1(或反之);
从根到每个叶子都得到一个二进制串,该二进制串就是叶子的哈夫曼编码。
可以得到两个结论:
① 哈夫曼编码是前缀编码;
② 哈夫曼编码是最优前缀编码。
2、哈夫曼编码的作用
哈夫曼编码中虽然大部分编码长度大于定长编码的长度3,但程序总位数变小了,可以算出平均码长是2.2.
构造满足哈夫曼编码的最短最优性质:
(1)若di≠dj(字母不同),则对应的树叶不同。因此前缀码不同,一个路径不可能是其他路径的一部分,所以字母之间可以完全区别;
(2)将所有字符变成二进制的哈夫曼编码,使带权路径长度最短,相当总的通路长度最短。
3、哈夫曼编码算法的实现
静态三叉链表的描述如下:
typedef struct
{
unsigned int weight ; /* 用来存放各个结点的权值*/
unsigned int parent, LChild, RChild ; /*父、孩子结点指针*/
}HTNode, *HuffmanTree; /*动态分配数组,存储哈夫曼树*/
typedef char * HuffmanCode[N+1] ; /*哈夫曼编码串头指针数组*/
数组ht的前n个分量表示叶子结点,最后一个分量表示根结点。每个叶子结点对应的编码长度不等,但最长不超过n。
哈夫曼编码的算法:
void CrtHuffmanCode(HuffmanTree ht, HuffmanCode hc, int n)/*从叶到根,逆向求每个叶结点的哈夫曼编码*/
{ char *cd; int i, start, p; unsigned int c;
cd = (char * )malloc(n * sizeof(char )); /*分配求当前编码的工作空间*/
cd[n - 1] = '\0'; /*从右向左逐位存放编码,首先存放编码结束符*/
for(i = 1; i <= n; i++) /*求n个叶结点哈夫曼编码*/
{
start = n-1; /*初始化编码起始指针*/
c = i; p = ht[i].parent; /*从叶子到根结点求编码*/
while(p!=0)
{ --start;
if( ht[p].LChild == c)
cd[start]='0'; /*左分支标0*/
else cd[start]='1'; /*右分支标1*/
c = p, p = ht[p].parent;//向上倒推}
hc[i]=(char *)malloc((n-start)*sizeof(char)); /*为第i个编码分配空间*/
strcpy(hc[i],&cd[start]);
}
free(cd);
}