目录
一、相关概念
(1) 路径、路径长度和树的路径长度
路径:从一个结点到另一个结点的分支序列。
路径长度: 从一个结点到另一个结点所经过的分支数目。
树的路径长度:从根结点到每个结点的路径长度之和。
(2)结点的权和带权路径长度
结点的权:给树的每个结点赋予的一个具有实际意义的实数。
带权路径长度:从根结点到某一结点的路径长度与该结点的权的乘积。
(3)树的带权路径长度
树的带权路径长度:从根结点到每个叶结点的带权路径长度之和。
Q:给定n个权值,求一棵具有n个终端结点的二叉树,使其带权路径长度最短?
A:哈夫曼给出了构造这种树的规律,将给定结点构成一棵带权路径长度最短的二叉树,即哈夫曼树。
二、构建哈夫曼树
哈夫曼树是由n个带权叶结点构成的所有二叉树中带权路径长度最短的二叉树。因为这种树最早由哈夫曼研究,所以称为哈夫曼树,又称最优二叉树。
构建哈夫曼树的步骤:
示例:
三、哈夫曼树的存储结构和类型定义
3.1 存储结构
由离散数学知识可知,一棵有n个叶结点的哈夫曼树共有2n-1个结点。因此,可以用一个大小为2n-1的一维数组来存放哈夫曼树的各个结点。
对于有n个叶结点的哈夫曼树,其结点总数为2n-1个。为实现方便,将叶结点集中存储在前面的1-n个位置,而后面的n-1个位置中存储其余的非叶结点。
3.2 类型定义
#define MAXN 20
#define MAXM 2 * MAXN - 1
typedef struct {
int weight;
int parent;
int LChild;
int RChild;
} HTNode, HuffmanTree[MAXM+1]; /* 弃用0号单元 */
四、哈夫曼树的算法实现
4.1 初始化哈夫曼树
w[ ]:存储结点权重的数组。
n:叶结点的数量。
for (i = 1; i <= m; ++i) {
/* 对叶结点填入已知权重,对非叶结点暂时全部填入0 */
ht[i].weight = i <= n ? w[i] : 0;
ht[i].parent = ht[i].LChild = ht[i].RChild = 0;
}
4.2 选出权重最小的两个结点
结点未被处理过的标志:双亲结点为0。
/* 选出权重最小的两个结点 */
void Select(HuffmanTree ht, int n, int *s1, int *s2) {
*s1 = 1;
for (int i = 1; i <= n; ++i)
if (ht[i].parent == 0 && ht[i].weight < ht[*s1].weight)
*s1 = i;
*s2 = *s1 == 1 ? 2 : 1; /* 避免结点重复 */
for (int i = 1; i <= n; ++i)
if (ht[i].parent == 0 && ht[i].weight < ht[*s2].weight && i != *s1)
*s2 = i;
}
4.3 构建哈夫曼树
void CreateHuffmanTree(HuffmanTree ht, int *w, int n) {
int i, s1, s2;
int m = 2 * n - 1;
/* 初始化哈夫曼树 */
for (i = 1; i <= m; ++i) {
ht[i].weight = i <= n ? w[i] : 0;
ht[i].parent = ht[i].LChild = ht[i].RChild = 0;
}
/* 处理所有非叶结点 */
for (i = n + 1; i <= m; ++i) {
/* 查找范围扩大:纳入已处理非叶结点 */
Select(ht, i - 1, &s1, &s2);
/* 选出的两个结点的双亲结点为i */
ht[s1].parent = ht[s2].parent = i;
ht[i].LChild = s1;
ht[i].RChild = s2;
ht[i].weight = ht[s1].weight + ht[s2].weight;
}
}