哈夫曼树的相关概念
结点的权值: 某种特定含义的数值
结点的带权路径长度: 根到结点路径长度×结点的权值
树的带权路径长度: 所有叶子结点的带权路径长度之和
哈夫曼树: 在给定叶子结点权值的二叉树中带权路径长度最小的二叉树
固定长度编码: 在数据通信中,对每个字符用相等长度的二进制位表示的编码方式
可变长度编码: 对频率高的字符短编码,对频率低的字符长编码从而使平均编码长度减短的编码方式
哈夫曼编码: 由哈夫曼树得到的一种被广泛应用且非常有效的数据压缩编码
哈夫曼树的存储结构
采用二叉树的顺序存储方式来存储哈夫曼树。
代码
//哈夫曼树的结点
typedef struct HTNode {
int weight; //权值
int parent; //双亲下标
int lChild; //左孩子下标
int rChild; //右孩子下标
} HTNode;
//哈夫曼树
typedef struct HT {
int numLeaf; //叶子结点树
int WPL; //带权路径长度
char **code; //哈夫曼编码
HTNode *node; //树的结点
} HT;
初始化哈夫曼树
过程
给定5个结点权值分别为1,3,2,7,2,初始化后数据结构如下表:
结点下标 | 结点权值 | 父结点下标 | 左孩子下标 | 右孩子下标 |
---|---|---|---|---|
0 | 1 | -1 | -1 | -1 |
1 | 3 | -1 | -1 | -1 |
2 | 2 | -1 | -1 | -1 |
3 | 7 | -1 | -1 | -1 |
4 | 2 | -1 | -1 | -1 |
代码
//初始化哈夫曼树
void initHT(HT *ht) {
printf("叶子结点数:");
scanf("%d", &ht->numLeaf); //获取叶子结点树
ht->node = (HTNode *) malloc(sizeof(HTNode) * (2 * ht->numLeaf - 1)); //哈夫曼树总结点数始终为叶子结点数的两倍减一
for (int i = 0; i < ht->numLeaf; i++) {
//循环给每个叶子结点赋予权值
printf("第%d个叶子结点权值:", i + 1);
scanf("%d", &ht->node[i].weight); //给第i个叶子结点赋予权值
ht->node[i].parent = ht->node[i].lChild = ht->node[i].rChild = -1; //将所有叶子结点的双亲下标初始化为-1
}
ht->code = (char **) malloc(sizeof(char *) * ht->numLeaf); //存储每一个叶子结点的哈夫曼编码
for (int i = 0; i < ht->numLeaf; i++) {
//初始化哈夫曼编码数组
ht->code[i] = (char *) malloc(sizeof(char) * ht->numLeaf); //哈夫曼编码不会超过叶子结点数量减一最后一位存'\0'
memset(ht->code[i], '\0', sizeof(char *)); //给每个字符串附上初值
}
ht->WPL = 0; //初始化带权路径长度
}
构造哈夫曼树
过程
哈夫曼树构建完成后上述例子的数据结构如下表:
结点下标 | 结点权值 | 父结点下标 | 左孩子下标 | 右孩子下标 |
---|---|---|---|---|
0 | 1 | 5 | -1 | -1 |
1 | 3 | 6 | -1 | -1 |
2 | 2 | 5 | -1 | -1 |
3 | 7 | 8 | -1 | -1 |
4 | 2 | 6 | -1 | -1 |
5 | 3 | 7 | 0 | 2 |
6 | 5 | 7 | 4 | 1 |
7 | 8 | 8 | 5 | 6 |
8 | 15 | -1 | 3 | 7 |
代码
//构造哈夫曼树
void createHT(const HT *ht) {
for (int i = 0; i < ht->numLeaf - 1; i++) {
//一共要合并叶子结点数减一次
int minNode1 = -1, minNode2 = -1, minWeight1 = INT_MAX, minWeight2 = INT_MAX; //记录没有双亲的最小的两个结点的下标和权值
for (int j = 0; j < ht->numLeaf + i; j++) {
//合并后的新结点也要参与筛选
if (ht->node[j].weight < minWeight2 && ht->node[j].parent == -1) {
minWeight2 = ht->node[j].weight;
minNode2 = j;
if (minWeight1 > minWeight2) {
//如果之前的权值比新选的大交换它们
minWeight2 = minWeight1;
minNode2 = minNode1;
minWeight1 = ht->node[j].weight;
minNode1 = j;
}
}
}
ht->node[minNode1].parent = ht->node[minNode2].parent = ht->numLeaf + i; //确定双亲结点的下标
ht->node[ht->numLeaf + i].weight = minWeight1 + minWeight2; //给双亲结点赋予权值
ht->node[ht->numLeaf + i].parent = -1; //将双亲结点的双亲结点下标初始化为-1
ht->node[ht->numLeaf + i].lChild = minNode1; //确定双亲结点的左孩子下标
ht->node[ht->numLeaf + i].rChild = minNode2; //确定双亲结点的右孩子下标
}
}
计算带权路径长度
过程
带权路径长度等于所有叶子结点的带权路径长度之和
W
P
L
=
1
×
80
+
2
×
10
+
3
×
(
2
+
8
)
=
130
WPL=1×80+2×10+3×(2+8)=130
WPL=1×80+2×10+3×(2+8)=130
代码
//计算带权路径长度
void calculateWPL(HT *ht) {
for (int i = 0; i < ht->numLeaf; i++) {
//从每个叶子结点开始寻找根结点
const HTNode *p = &ht->node[i]; //依次记录每个叶子结点地址
int length = 0; //记录叶子结点到根结点的路径长度
while (p->parent != -1) {
//没有找到根结点继续向上搜寻
p = &ht->node[p->parent]; //p指向当前结点的双亲
length++; //路径长增加
}
ht->WPL += ht->node[i].weight * length; //带权路径长度等于所有叶子结点的带权路径长度之和
}
}
构建哈夫曼树编码
过程
固定长度编码
结点 | 编码 |
---|---|
A | 00 |
B | 01 |
C | 10 |
D | 11 |
哈夫曼树编码
结点 | 编码 |
---|---|
A | 00 |
B | 010 |
C | 1 |
D | 011 |
对于上述两种编码方式
压缩率
=
W
P
L
哈夫曼树编码
W
P
L
固定长度编码
×
100
%
=
130
200
×
100
%
=
65
%
压缩率=\frac{WPL_{哈夫曼树编码}}{WPL_{固定长度编码}} ×100\%= \frac{130}{200} ×100\%=65\%
压缩率=WPL固定长度编码WPL哈夫曼树编码×100%=200130×100%=65%
代码
//构建哈夫曼编码
void createHC(const HT *ht) {
for (int i = 0; i < ht->numLeaf; i++) {
//从每个叶子结点开始寻找根结点
const HTNode *p = &ht->node[i]; //依次记录每个叶子结点地址
for (int j = 0; p->parent != -1; j++) {
//没有找到根结点继续向上搜寻
if (p == &ht->node[ht->node[p->parent].lChild]) {
ht->code[i][j] = '0'; //如果当前结点是双亲的左孩子编码为0
} else {
ht->code[i][j] = '1'; //如果当前结点是双亲的右孩子编码为1
}
p = &ht->node[p->parent]; //p指向当前结点的双亲
}
strrev(ht->code[i]); //反转字符串
}
}
完整实现代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//哈夫曼树的结点
typedef struct HTNode {
int weight; //权值
int parent; //双亲下标
int lChild; //左孩子下标
int rChild; //右孩子下标
} HTNode;
//哈夫曼树
typedef struct HT {
int numLeaf; //叶子结点树
int WPL; //带权路径长度
char **code; //哈夫曼编码
HTNode *node; //树的结点
} HT;
//初始化哈夫曼树
void initHT(HT *ht) {
printf("叶子结点数:");
scanf("%d", &ht->numLeaf); //获取叶子结点树
ht->node = (HTNode *) malloc(sizeof(HTNode) * (2 * ht->numLeaf - 1)); //哈夫曼树总结点数始终为叶子结点数的两倍减一
for (int i = 0; i < ht->numLeaf; i++) {
//循环给每个叶子结点赋予权值
printf("第%d个叶子结点权值:", i + 1);
scanf("%d", &ht->node[i].weight); //给第i个叶子结点赋予权值
ht->node[i].parent = ht->node[i].lChild = ht->node[i].rChild = -1; //将所有叶子结点的双亲下标初始化为-1
}
ht->code = (char **) malloc(sizeof(char *) * ht->numLeaf); //存储每一个叶子结点的哈夫曼编码
for (int i = 0; i < ht->numLeaf; i++) {
//初始化哈夫曼编码数组
ht->code[i] = (char *) malloc(sizeof(char) * ht->numLeaf); //哈夫曼编码不会超过叶子结点数量减一最后一位存'\0'
memset(ht->code[i], '\0', sizeof(char *)); //给每个字符串附上初值
}
ht->WPL = 0; //初始化带权路径长度
}
//构造哈夫曼树
void createHT(const HT *ht) {
for (int i = 0; i < ht->numLeaf - 1; i++) {
//一共要合并叶子结点数减一次
int minNode1 = -1, minNode2 = -1, minWeight1 = INT_MAX, minWeight2 = INT_MAX; //记录没有双亲的最小的两个结点的下标和权值
for (int j = 0; j < ht->numLeaf + i; j++) {
//合并后的新结点也要参与筛选
if (ht->node[j].weight < minWeight2 && ht->node[j].parent == -1) {
minWeight2 = ht->node[j].weight;
minNode2 = j;
if (minWeight1 > minWeight2) {
//如果之前的权值比新选的大交换它们
minWeight2 = minWeight1;
minNode2 = minNode1;
minWeight1 = ht->node[j].weight;
minNode1 = j;
}
}
}
ht->node[minNode1].parent = ht->node[minNode2].parent = ht->numLeaf + i; //确定双亲结点的下标
ht->node[ht->numLeaf + i].weight = minWeight1 + minWeight2; //给双亲结点赋予权值
ht->node[ht->numLeaf + i].parent = -1; //将双亲结点的双亲结点下标初始化为-1
ht->node[ht->numLeaf + i].lChild = minNode1; //确定双亲结点的左孩子下标
ht->node[ht->numLeaf + i].rChild = minNode2; //确定双亲结点的右孩子下标
}
}
//计算带权路径长度
void calculateWPL(HT *ht) {
for (int i = 0; i < ht->numLeaf; i++) {
//从每个叶子结点开始寻找根结点
const HTNode *p = &ht->node[i]; //依次记录每个叶子结点地址
int length = 0; //记录叶子结点到根结点的路径长度
while (p->parent != -1) {
//没有找到根结点继续向上搜寻
p = &ht->node[p->parent]; //p指向当前结点的双亲
length++; //路径长增加
}
ht->WPL += ht->node[i].weight * length; //带权路径长度等于所有叶子结点的带权路径长度之和
}
}
//构建哈夫曼编码
void createHC(const HT *ht) {
for (int i = 0; i < ht->numLeaf; i++) {
//从每个叶子结点开始寻找根结点
const HTNode *p = &ht->node[i]; //依次记录每个叶子结点地址
for (int j = 0; p->parent != -1; j++) {
//没有找到根结点继续向上搜寻
if (p == &ht->node[ht->node[p->parent].lChild]) {
ht->code[i][j] = '0'; //如果当前结点是双亲的左孩子编码为0
} else {
ht->code[i][j] = '1'; //如果当前结点是双亲的右孩子编码为1
}
p = &ht->node[p->parent]; //p指向当前结点的双亲
}
strrev(ht->code[i]); //反转字符串
}
}
//展示哈夫曼树信息
void showHT(const HT *ht) {
printf("结点下标 结点权值 双亲下标 左孩子下标 右孩子下标 哈夫曼编码\n");
for (int i = 0; i < 2 * ht->numLeaf - 1; i++) {
printf("%-12d%-12d%-12d%-12d%-12d", i, ht->node[i].weight, ht->node[i].parent, ht->node[i].lChild,
ht->node[i].rChild); //打印哈夫曼树结点的信息
if (i < ht->numLeaf) {
printf("%-12s", ht->code[i]); //打印每个叶子结点的哈夫曼编码
}
printf("\n");
}
printf("带权路径长度WPL=%d\n", ht->WPL); //打印带权路径长度
}
//测试代码
void test() {
HT ht; //定义哈夫曼树
initHT(&ht); //初始化哈夫曼树
createHT(&ht); //构造哈夫曼树
createHC(&ht); //计算哈夫曼编码
calculateWPL(&ht); //计算带权路径长度
showHT(&ht); //展示哈夫曼树信息
}
//主函数
int main() {
test(); //测试代码
system("pause"); //暂停
return 0;
}