数据结构与算法 -- 哈夫曼树&哈夫曼编码
前言
上一篇了解学习了线索化二叉树
的一些知识,这一篇,对哈夫曼树
和哈夫曼编码
来做一个了解学习。
首先,我们先看一个经典的问题,
等级 | 优秀 | 良好 | 中等 | 及格 | 不及格 |
---|---|---|---|---|---|
考分 | 90 <= 分数 <= 100 | 80 <= 分数 < 90 | 70 <= 分数 < 80 | 60 <= 分数 < 70 | 0<=分数<60 |
一般我们会这样判断:
那么,在二叉树中是这样的
一般情况下,每个班的成绩及格不及格有一定的分布概率
那这样就会造成对中等
和良好
的成绩判断时,会经过很多步,当成绩比较多时,就会出现效率问题。
那么怎么能优化解决一下这个问题呢?其实前辈们已经解决了这个问题,我们接下来学习一下
1. 哈夫曼树
在上面的成绩算法,
节点D
的路径为4
,
树的总路径为:1 + 1 + 2 + 2 + 3 + 3 + 4 + 4 = 20
。
根据上面成绩的分布这棵树的WPL
为:WPL = 1 * 5 + 2 * 15 + 3 * 40 + 4 * 30 + 4 * 10 = 315
假如我们对成绩的判断顺序进行如下的调整:
那么节点D
的路径长度为2
树的总路径为1 + 2 + 3 + 3 + 2 + 1 + 2 + 2 = 16
根据上面成绩的分布这棵树的WPL
为:WPL = 5 * 3 + 3 * 15 + 2 * 40 + 2 * 30 + 2 * 10 = 220
第二种情况,明显比第一种情况更优。
那么怎么构建一个最优的树呢,我们接下来看一下哈夫曼树
的构建。
假设:A B C D E
的权重如下:
A | B | C | D | E |
---|---|---|---|---|
5 | 15 | 40 | 30 | 10 |
首先,找到权重最小的两个A
和E
,组成二叉树:
此时N1
的权重相当于 15
,依次找到 最小的 B
,组建二叉树:
此时N2
的权重相当于 30
,依次找到 最小的 D
,组建二叉树:
此时N3
的权重相当于 60
,依次找到 最小的 C
,组建二叉树(小的在左子树位置):
该二叉树的WPL
:40 * 1 + 30 * 2 + 15 * 3 + 10 * 4 + 5 * 4 = 205
这种排列组成的二叉树的WPL
,明显小于上面两种,以这种方式排列的二叉树即为哈夫曼树
小结:
先对给出的节点的权重排序
依次找出最小的两个节点,组成二叉树,小的在左子树位置
2. 哈夫曼编码
假设如下:
字符 | A | B | C | D | E | F |
---|---|---|---|---|---|---|
权重 | 27 | 8 | 15 | 15 | 30 | 5 |
编码 | 000 | 001 | 010 | 011 | 100 | 101 |
如上表中,每个字符的编码位数都是一样的,但是每个字符出现的权重是不同的,这样就会造成权重低的字符的空间浪费
我们可以利用 哈夫曼树 找到最佳的二叉树排列,然后生成 哈夫曼编码 来解决这个问题。
首先,对上面的字符生成哈夫曼树:
- 找到权重最小的 B 和 F,组成二叉树(小的在左边)
- 找到 C,组成二叉树(小的在左边)
- 依次查找
最终的 哈夫曼树 如下:
然后,开始哈夫曼编码
哈夫曼编码的原则:左子树上的权重,替换为 0 ,右子树权重替换为 1
然后从根节点,到各个叶子节点,组成每个叶子节点的编码,如下:
字符 | A | B | C | D | E | F |
---|---|---|---|---|---|---|
权重 | 27 | 8 | 15 | 15 | 30 | 5 |
编码 | 01 | 1001 | 101 | 00 | 11 | 1000 |
那么对于同一个字符串BADCADFEED
的新旧二进制编码如下:
旧编码: 001 000 011 010 000 011 101 100 100 011 (共30个字符)
新编码: 1001 01 00 101 01 00 1001 11 11 00 (共25个字符)
可以明显看出,新编码(哈夫曼编码)比旧编码,有更高的存储利用率。
小结:
先生成哈夫曼树
从根节点开始,左子树的权值为0,右子树的权值 为 1
从根节点开始,到叶子节点,组成叶子节点的编码
3. 哈夫曼树 & 哈夫曼编码 的实现
可以定义哈夫曼树(顺序)的节点如下:
const int MaxValue = 10000;//初始设定的权值最大值
const int MaxBit = 4;//初始设定的最大编码位数
const int MaxN = 10;//初始设定的最大结点个数
// 定义节点
typedef struct HaffNode{
int weight; // 权值
int flag; // 标记该节点是否合并到哈夫曼树 0:未合并,1:合并
int parent; // 双亲节点下标
int leftChild; // 左孩子下标
int rightChild; // 右孩子下标
}HaffNode;
// 存放哈夫曼编码的数据元素结构
typedef struct Code {
int bit[MaxBit]; // 数组
int start; // 编码的起始下标
int weight; // 字符的权重
}Code;
// 构建哈夫曼树
void Haffman(int weight[],int n,HaffNode *haffTree){
int j, m1, m2, x1, x2;
// 哈夫曼树初始化
// n个叶子节点,有(2n - 1)个节点
for (int i = 0; i < 2*n - 1; i++) {
if (i < n) {
haffTree[i].weight = weight[i]; // 叶子节点赋值权重
} else {
haffTree[i].weight = 0;
}
haffTree[i].parent = 0; // 无双亲,下标为0
haffTree[i].flag = 0; // 未合并到哈夫曼树,为0
haffTree[i].leftChild = -1; // 无左右孩子,给特殊在-1
haffTree[i].rightChild = -1;
}
// 构建哈夫曼树
for (int i = 0; i < n - 1; i++) {
m1 = m2 = MaxValue;
x1 = x2 = 0;
// 循环找到所有权重中最小的两个
for (j = 0; j < n + i; j++) {
if (haffTree[j].weight < m1 && haffTree[j].flag == 0) {
m2 = m1;
x2 = x1;
m1 = haffTree[j].weight;
x1 = j;
} else if(haffTree[j].weight < m2 && haffTree[j].flag == 0){
m2 = haffTree[j].weight;
x2 = j;
}
}
// 将找到权重最小的两个子树合并为一个子树
haffTree[x1].parent = n + i;
haffTree[x2].parent = n + i;
haffTree[x1].flag = 1;
haffTree[x2].flag = 1;
// 修改合并后的子树(n+i)的权重
haffTree[n+i].weight = haffTree[x1].weight + haffTree[x2].weight;
// 修改合并后的子树(n+i)的左右子树
haffTree[n+i].leftChild = x1;
haffTree[n+i].rightChild = x2;
}
}
// 哈夫曼编码
void HaffmanCode(HaffNode haffTree[], int n, Code haffCode[])
{
// 创建一个结点
Code *cd = (Code *)malloc(sizeof(Code));
int child, parent;
// 求n个结点的哈夫曼编码
for (int i = 0; i < n; i++) {
// 从0开始计数
cd->start = 0;
// 取得编码对应权值的字符
cd->weight = haffTree[i].weight;
// 当前叶子节点为 i
child = i;
// 根据叶子节点找到双亲节点
parent = haffTree[i].parent;
// 由叶子节点向上到根节点
while (parent != 0) {
if (haffTree[parent].leftChild == child) {
cd->bit[cd->start] = 0; //左孩子结点编码0
} else {
cd->bit[cd->start] = 1; //右孩子结点编码1
}
// 编码自增
cd->start++;
// 修改子节点为当前双亲节点(向上查找)
child = parent;
// 根据子节点找到双亲
parent = haffTree[child].parent;
}
// 此时的编码是倒序的,翻转 011 start = 3
int temp = 0;
for (int j = cd->start - 1; j >= 0; j--) {
temp = cd->start-j-1;
haffCode[i].bit[temp] = cd->bit[j];
}
//把cd中的数据赋值到haffCode[i]中.
//保存好haffCode 的起始位以及权值;
haffCode[i].start = cd->start;
//保存编码对应的权值
haffCode[i].weight = cd->weight;
}
}
// 调用
void show{
int i, j, n = 4, m = 0;
//权值
int weight[] = {2,4,5,7};
//初始化哈夫曼树, 哈夫曼编码
HaffNode *myHaffTree = malloc(sizeof(HaffNode)*2*n-1);
Code *myHaffCode = malloc(sizeof(Code)*n);
//当前n > MaxN,表示超界. 无法处理.
if (n>MaxN)
{
printf("定义的n越界,修改MaxN!");
exit(0);
}
//1. 构建哈夫曼树
Haffman(weight, n, myHaffTree);
//2.根据哈夫曼树得到哈夫曼编码
HaffmanCode(myHaffTree, n, myHaffCode);
//3.
for (i = 0; i<n; i++)
{
printf("Weight = %d\n",myHaffCode[i].weight);
for (j = 0; j<myHaffCode[i].start; j++)
printf("%d",myHaffCode[i].bit[j]);
m = m + myHaffCode[i].weight*myHaffCode[i].start;
printf("\n");
}
printf("Huffman's WPS is:%d\n",m);
}