哈夫曼树相关的几个名词
图1 哈夫曼树
路径:在一棵树中,一个结点到另一个结点之间的通路,称为路径
图 1 中,从根结点到结点 a 之间的通路就是一条路径
路径长度:在一条路径中,每经过一个结点,路径长度都要加 1
图 1 中,从根结点到结点 c 的路径长度为
结点的权:给每一个结点赋予一个新的数值,被称为这个结点的权
图 1 中,结点 a 的权为 ,结点 b 的权为
结点的带权路径长度:指的是从根结点到该结点之间的路径长度与该结点的权的乘积
图 1 中,结点 b 的带权路径长度为
WPL:树的带权路径长度为树中所有叶子结点的带权路径长度之和
图 1 中,所示的这颗树的带权路径长度为:
什么是哈夫曼树
当用 n 个叶子结点构建一棵树时,如果构建的这棵树的带权路径长度(WPL)最小,称这棵树为最优二叉树,有时也叫赫夫曼树或者哈夫曼树
在构建哈弗曼树时,要使树的带权路径长度最小,只需要遵循一个原则:权重越大的结点离树根越近
构建哈夫曼树的过程
对于给定的有各自权值的 n 个叶子结点,构建哈夫曼树有一个行之有效的办法:
- 在 n 个权值中选出两个最小的权值,对应的两个结点组成一个新的二叉树,且新二叉树的根结点的权值为左右孩子权值的和
- 在原有的 n 个权值中删除那两个最小的权值,同时将新的权值加入到 n–2 个权值的行列中,以此类推
- 重复 1 和 2 ,直到所有的结点构建成了一棵二叉树为止,这棵树就是哈夫曼树
图 2 哈夫曼树的构建过程
上图 2中,给定了4个叶子结点,如果我们想构建哈夫曼树,构建过程是怎么样的呢!
第一步:在4个权值中选出二个最小的权值(2 和 4),将这二个权值对应的结点组成一个新的二叉树,且新二叉树的根结点(第五个结点)的权值为结点 c 和 d 的权值和 ,在原有4个权值中删除权值为 2 和 4后得到 7 和 5 二个权值,将新的权值 6 加入其中
第二步:在3个权值中选出二个最小的权值(5 和 6),将这二个权值对应的结点组成一个新的二叉树,且新二叉树的根结点(第六个结点)的权值为结点 b 和第五个结点的权值和 ,在原有3个权值中删除权值为 5 和 6后得到 7 一个权值,将新的权值 11 加入其中
第三步:在2个权值中选出二个最小的权值(7 和 11),将这二个权值对应的结点组成一个新的二叉树,且新二叉树的根结点(第七个结点)的权值为结点 a 和第六个结点的权值和 ,在原有2个权值中删除权值为 7 和 11,将新的权值 18 加入其中
第四步:此时已经构成了二叉树!这棵树称为哈夫曼树
哈弗曼树中结点结构
构建哈夫曼树时,首先需要确定树中结点的构成(叶子结点和构建结点)
由于哈夫曼树的构建是从叶子结点开始,不断地构建新的父结点,直至树根,所以结点中应包含指向父结点的指针,但是在使用哈夫曼树时是从树根开始,根据需求遍历树中的结点,因此每个结点需要有指向其左孩子和右孩子的指针
// 哈夫曼树结点结构
typedef struct{
int weight; // 结点权重
int parent, left, right;
// 父结点、左孩子、右孩子在数组中的位置下标
} HTNode;
对实现哈夫曼树的代码实现
完全代码!!!
#include <stdio.h>
#include <stdlib.h>
// 哈夫曼树结点结构
typedef struct{
int weight; // 结点权重
int parent, left, right;
// 父结点、左孩子、右孩子在数组中的位置下标
} HTNode;
void Select(HTNode *HT, int end, int *s1, int *s2){
int min1, min2;
// 遍历数组初始下标为 1
int i = 1;
// 找到还没构建树的结点
while(HT[i].parent != 0 && i <= end){
i++;
} // i=1 ——第一个树叶结点
min1 = HT[i].weight; // 7 5 2 4
*s1 = i; // 初始化权重最小位置为 1
i++;
while(HT[i].parent != 0 && i <= end){
i++;
}
// 对找到的两个结点比较大小(min2为大的,min1为小的)
if(HT[i].weight < min1){
min2 = min1;
*s2 = *s1;
min1 = HT[i].weight;
*s1 = i;
}else{
min2 = HT[i].weight;
*s2 = i;
}
// 两个结点和后续的所有未构建成树的叶子结点做比较
for(int j = i+1; j <= end; j++){ // j=3(直到 j = end)
// 如果有父结点,直接跳过,进行下一个
if(HT[j].parent != 0){
continue;
}
// 如果比最小的还小,将 min2=min1,min1赋值新的结点的下标
if(HT[j].weight < min1){
min2 = min1;
min1 = HT[j].weight;
*s2 = *s1;
*s1 = j;
}else if(HT[j].weight >= min1 && HT[j].weight < min2){
// 如果介于两者之间,min2赋值为新的结点的位置下标
min2 = HT[j].weight;
*s2 = j;
}
}
}
HTNode *CreateHuffmanTree(HTNode *HT, int *w, int n){
if(n <= 1) return HT;
int m = 2*n-1; // 哈夫曼树总节点数,n是叶子结点(m=7)
HT = (HTNode*) malloc((m+1)*sizeof(HTNode)); // HT[7]
HTNode *p = HT;
// 初始化哈夫曼树中的所有叶子结点
for(int i = 1; i <= n; i++){ // n=4
p[i].weight = w[i-1]; // p[1].weight = w[0] ... p[4].weight = w[3]
p[i].parent = 0;
p[i].left = 0;
p[i].right = 0;
}
// 初始化除叶子结点外的其它结点
for(int i = n+1; i <= m; i++){
p[i].weight = 0;
p[i].parent = 0;
p[i].left = 0;
p[i].right = 0;
}
// 构建哈夫曼树
for(int i = n+1; i <= m; i++){
int s1, s2; // 保存了 HT数组中 weight较小值的二个下标
Select(HT, i-1, &s1, &s2);
// s1 = 3,s2 = 4
// s1 = 1,s2 = 5
// s1 = 1,s2 = 6
HT[s1].parent = HT[s2].parent = i;
// 权重为 2和 4的二个结点的双亲为第五个结点
// 权重为 5和 6的二个结点的双亲为第六个结点
// 权重为 7和 11的二个结点的双亲为第七个结点
HT[i].left = s1;
// 初始化第五个结点的左孩子为权重为 2 的结点
// 初始化第六个结点的左孩子为权重为 5 的结点
// 初始化第七个结点的左孩子为权重为 7 的结点
HT[i].right = s2;
// 初始化第五个结点的右孩子为权重为 4 的结点
// 初始化第六个结点的右孩子为权重为 6 的结点
// 初始化第七个结点的右孩子为权重为 11 的结点
HT[i].weight = HT[s1].weight + HT[s2].weight;
// 初始化第五个结点的权重为 2+4 = 6
// 初始化第六个结点的权重为 5+6 = 11
// 初始化第七个结点的权重为 7+11 = 18
}
return HT;
}
int main(void){
HTNode *ht = NULL;
int weight[4] = {7, 5, 2, 4};
ht = CreateHuffmanTree(ht, weight, 4);
printf("树根结点权重:%d\n", ht[7].weight); // 树根结点权重:18
return 0;
}
部分函数代码体!!!
// 两个结点和后续的所有未构建成树的叶子结点做比较
for(int j = i+1; j <= end; j++){ // j=3(直到 j = end)
// 如果有父结点,直接跳过,进行下一个
if(HT[j].parent != 0){
continue;
}
// 如果比最小的还小,将 min2=min1,min1赋值新的结点的下标
if(HT[j].weight < min1){
min2 = min1;
min1 = HT[j].weight;
*s2 = *s1;
*s1 = j;
}else if(HT[j].weight >= min1 && HT[j].weight < min2){
// 如果介于两者之间,min2赋值为新的结点的位置下标
min2 = HT[j].weight;
*s2 = j;
}
}
如上函数(Select)中查找权重值最小的两个结点的思想是:从数组起始位置开始,首先找到两个无父结点的结点(说明还未使用其构建成树),然后和后续无父结点的结点依次做比较,有两种情况需要考虑:
- 如果比两个结点中较小的那个还小,就保留这个结点,删除原来较大的结点
- 如果介于两个结点权重值之间,替换原来较大的结点