一、哈夫曼树的定义
在许多应用中,树中结点常常被赋予一个表示某种意义的数值,称为该结点的权。从树的根到任意结点的路径长度(经过的边数)与该结点上权值的乘积,称为该结点的带权路径长度。树中所有叶子结点的带权路径长度之和称为树的带权路径长度,记作:
W
P
L
=
∑
i
=
1
n
w
i
l
i
WPL=\sum\limits_{i=1}^n{w_i}{l_i}
WPL=i=1∑nwili
在含有n个叶子结点的二叉树中,其中带权路径长度(WPL)最小的二叉树为哈夫曼树。
二、哈夫曼树的构造
构造哈夫曼树的算法描述如下:
1)将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。
2)构造一个新的结点,从F中选取两棵根结点权值最小的树作为新结点的左右子树,并且将新结点的权值置为左右子树上根结点的权值之和。
3)从F中删除刚才选出的两棵树,同时将新得到的树加入F中。
4)重复2)和3),直到F中只剩下一棵树为止。
哈夫曼树的特点
:
- 每个初始结点最终都成为叶子结点,且权值越小的结点到根结点的路径长度越大
- 构造过程中新建了n-1个结点,因此哈夫曼树的结点总数为2n-1
- 每次构造都选择2棵树作为新结点的孩子,因此哈夫曼树中不存在度为1的结点。
三、哈夫曼树的C语言实现
代码如下:
#include<stdio.h>
#include<stdlib.h>
/*哈夫曼树结点*/
typedef struct HTNode
{
int weight;//结点权值
int lchild, rchild, parent;//左右孩子,双亲
}HTNode,*HuffmanTree;
/*选取两个结点权值最小的结点*/
void Select(HuffmanTree HT, int n, int& post1, int& post2)
{
int minum = 0;//保存最小值
/*寻找第一个最小值*/
for (int i = 1; i <= n; i++)
{
if (HT[i].parent == 0)//该结点没有双亲
{
minum = i;
break;
}
}
for (int i = 1; i <= n; i++)
{
if (HT[i].parent == 0)
{
if (HT[i].weight < HT[minum].weight)//比较
{
minum = i;
}
}
}
post1 = minum;
/*寻找第二个最小值*/
for (int i = 1; i <= n; i++)
{
if (HT[i].parent == 0 && i != post1)//该结点没有双亲,且不与第一个最小值结点相同
{
minum = i;
break;
}
}
for (int i = 1; i <= n; i++)
{
if (HT[i].parent == 0 && i != post1)
{
if (HT[i].weight < HT[minum].weight)//比较
{
minum = i;
}
}
}
post2 = minum;
}
/*创建哈夫曼树*/
void CreatHuffTree(HuffmanTree& HT, int n, int* weight)
{
int m, s1, s2;
m = 2 * n - 1;//m表示哈夫曼树的总结点个数
HT = (HuffmanTree)malloc(sizeof(HTNode) * (m + 1));//申请m+1个结点空间,因为第一个不使用
/*1~n表示叶子结点,即存放字符的结点,初始化*/
for (int i = 1; i <= n; i++)
{
HT[i].parent = 0;
HT[i].weight = weight[i];
HT[i].lchild = 0;
HT[i].rchild = 0;
}
/*n+1~m为分支结点,初始化*/
for (int i = n + 1; i <= m; i++)
{
HT[i].parent = 0;
HT[i].weight = 0;
HT[i].lchild = 0;
HT[i].rchild = 0;
}
printf("哈夫曼树如下所示:\n");
/*创建分支结点,构建哈夫曼树*/
for (int i = n+1; i <= m; i++)
{
Select(HT, i - 1, s1, s2);
/*注意,以下两行代码使得s1,s2两个结点的双亲为结点i,并且不会再被Select函数选择,相当于删除了两结点*/
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;//构造出来的结点的权值等于其左右子树的根结点权值的和
printf("%d\t%d\t%d\n", HT[i].weight, HT[s1].weight, HT[s2].weight);
}
}
int main()
{
HuffmanTree HT;
int n;//n表示初始结点个数
int* weight;
printf("请输入初始结点个数:");
scanf("%d", &n);
weight = (int*)malloc(sizeof(int) * (n + 1));
for (int i = 1; i <= n; i++)
{
printf("请输入第%d个结点的权值:\n", i);
scanf("%d", &weight[i]);
}
CreatHuffTree(HT, n, weight);
return 0;
}
![](https://i-blog.csdnimg.cn/blog_migrate/576243fc1c5278586f9b99fa539ef0d7.png)
四、哈夫曼编码
采用前缀编码。
前缀编码:没有一个编码是另一个编码的前缀。
从哈夫曼树得到哈夫曼编码是一件很自然的事情。首先,将每个出现的字符当做一个独立的结点,其权值为它出现的频度(次数),构造出相应的哈夫曼树。这样,所有的字符结点都出现在叶子结点中。我们可以将字符的编码解释为从根结点到该字符的路径上边标记的序列,其中边标记为0表示“转向左孩子”,标记为1表示“转向右孩子”。