赫夫曼树——最优二叉树
书本上百度上均有解释其特点,在此不再一一道来,就由一道题目做索引,直接来谈谈它的构建过程吧额。
赫夫曼编码
时间限制: 1 Sec | 内存限制: 128 MB
输入
输入包含多组数据(不超过100组)
每组数据第一行一个整数n,表示字符个数。接下来n行,每行有一个字符ch和一个整数weight,表示字符ch所对应的权值,中间用空格隔开。
输入数据保证每组测试数据的字符不会重复。
输出
对于每组测试数据,按照输入顺序输出相应的字符以及它们的哈弗曼编码结果,具体格式见样例。
样例输入
3
A 10
B 5
C 8
4
A 1
B 1
C 1
D 1
样例输出
A:0
B:10
C:11
A:00
B:01
C:10
D:11
Huffman树的构建
大致思路 就是一次次的寻找最小权值结点构成一个二叉树,同时父结点将两子结点的权值相加并插入,然后将父结点的权值插入到权值数组里,如此重复的 查找结点、生成二叉树、插入结点,直到数组里只剩下最后一个结点,它就是这棵赫夫曼树的根结点。
构建过程
首先这里有一个权值数组,将其存入到一棵树的叶子结点里
选出两个权值最小的结点构成一棵简单的二叉树,父结点权值即为两者之子结点之和
随后将其父结点插入到结点数组里
再次选择结点,构建二叉树,插入结点, 重复操作,直到只剩下一个结点。
如图所示,最终结点就是这棵树的根结点,赫夫曼树构建完毕。
当时看完书之后思路很清晰啊,就一头扎进去开始写代码,中间也遇到了不少的错误,最后结合别人的博客文章、视频课程、书本,才将其顺利实现。
构建过程代码如下
//构建哈夫曼树
void CreateHuffmanTree(HuffmanTree &HT, int w[], int n)
{
//赫夫曼结点数
int m = 2 * n - 1;
//选取两个最小权值的结点
int s1;
int s2;
int i;
//为树结点分配内存 这里从1忽略了0号元素
HT = (HuffmanTree)malloc((m + 1) * sizeof(Node));
//初始化树叶结点
for(i = 1; i <= n; i++)
{
HT[i].weight = w[i];
HT[i].lchild = 0;
HT[i].parent = 0;
HT[i].rchild = 0;
}
//非叶子结点的初始化
for(i = n + 1; i <= m; i++)
{
HT[i].weight = 0;
HT[i].lchild = 0;
HT[i].parent = 0;
HT[i].rchild = 0;
}
//创建非叶子结点即开始构建赫夫曼树
for(i = n + 1; i <= m; i++)
{
Select(HT, i-1, &s1, &s2);
//选出的两个权值最小的叶子结点组成一个新的二叉树
//根为 i 结点
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;
}
}
完成了赫夫曼树的构建,就要用它做点正事了,由此开始探讨 赫夫曼编码的生成 。
首先了解一下什么是赫弗曼编码——其实就是前缀码的生成,只不过这里要保证每一个生成的前缀码都不是其他编码的前缀,这就可以引入上述探讨得出的赫夫曼树了。
为什么赫夫曼树可以用来生成这样的前缀码呢?
我的理解:
-
因为赫夫曼树为每一个权值分配了一条路径,从根结点到某一权值结点的路径上不会出现其他其他权值结点,其实也就是最初的权值结点都成为了最终赫夫曼树的叶子结点。
-
赫夫曼树中所有叶结点的带权路径长度之和——WPL,它的值是比较小的,这也就是它被称作最优二叉树的原因。
这一特性使其生成的编码不会彼此间的前缀,而且长度非常的短。
赫夫曼编码的生成
我们可以将 左路径设置为 0,右路径设置为 1, 依次将每条路径上的0,1编码存入字符数组里;
在上图中的赫夫曼树,从根结点出发,一次向叶结点探索,如果要得到A的前缀码,路径为 右——右,因此它的编码即为 11。
再如 11 010 10 011 00 ,它的译码即为 ABCDE。
这里应题目要求,提供了一个逆向生成的赫夫曼编码
代码段如下
//开始逆向生成赫夫曼编码
void CreatHuffmanCode(HuffmanTree &HT, HuffmanCode *HC, char *name, int n)
{
int i;
//编码的起始指针
int start;
//标记父结点
int p;
//标记子结点
unsigned int c;
//给n个编码的分配头指针
HC=(HuffmanCode *)malloc((n+1) * sizeof(char *));
//当前编码的空间
char *cd = (char *)malloc(n * sizeof(char));
//从右向左
cd[n-1] = '\0';
for(i = 1; i <= n; i++)
{
//初始化编码起始指针
start = n - 1;
//从叶子到根结点求编码
for(c = i, p = HT[i].parent; p != 0; c = p, p = HT[p].parent)
{
//从右到左的顺序编码入数组内
if( HT[p].lchild == c)
cd[--start] = '0'; //左分支标0
else
cd[--start] = '1'; //右分支标1
}
//为第i个编码分配空间并保存生成的编码
HC[i] = (char *)malloc((n - start) * sizeof(char));
strcpy(HC[i], &cd[start]);
}
//释放cd开始下一组编码的生成
free(cd);
//打印编码序列
for(i = 1; i <= n; i++)
printf("%c:%s\n", name[i], HC[i]);
}
不多喷了, 这道题题完整的AC代码是这样的
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
//haffman 树的结构
typedef struct
{
//叶子结点权值
unsigned int weight;
//指向双亲,和孩子结点的指针
unsigned int parent;
unsigned int lchild;
unsigned int rchild;
} Node, *HuffmanTree;
//这里选择了动态分配数组
typedef char *HuffmanCode;
//找出两个最小权值结点并用指针带回
void Select(HuffmanTree HT, int n, int *s1, int *s2)
{
int i = 0;
int min;
for(i = 1; i <= n; i++)
{
//找到第一个单结点
if(HT[i].parent == 0)
{
min = i;
break;
}
}
//继续遍历全部结点,找出权值最小的单节点
for(i = 1; i <= n; i++)
{
//结点的父亲为空
if(HT[i].parent == 0)
{
if(HT[i].weight < HT[min].weight)
{
min = i;
}
}
}
//找到了最小权值的结点,s1指向
*s1 = min;
//重复操作注意s1的干扰
for(i = 1; i <= n; i++)
{
if(HT[i].parent == 0 && i != (*s1))
{
min = i;
break;
}
}
for(i = 1; i <= n; i++)
{
if(HT[i].parent == 0 && i != (*s1))
{
if(HT[i].weight < HT[min].weight)
{
min = i;
}
}
}
*s2 = min;
}
//构建哈夫曼树
void CreateHuffmanTree(HuffmanTree &HT, int w[], int n)
{
//赫夫曼结点数
int m = 2 * n - 1;
//选取两个最小权值的结点
int s1;
int s2;
int i;
//为树结点分配内存 这里从1忽略了0号元素
HT = (HuffmanTree)malloc((m + 1) * sizeof(Node));
//初始化树叶结点
for(i = 1; i <= n; i++)
{
HT[i].weight = w[i];
HT[i].lchild = 0;
HT[i].parent = 0;
HT[i].rchild = 0;
}
//非叶子结点的初始化
for(i = n + 1; i <= m; i++)
{
HT[i].weight = 0;
HT[i].lchild = 0;
HT[i].parent = 0;
HT[i].rchild = 0;
}
//创建非叶子结点即开始构建赫夫曼树
for(i = n + 1; i <= m; i++)
{
Select(HT, i-1, &s1, &s2);
//选出的两个权值最小的叶子结点组成一个新的二叉树
//根为 i 结点
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;
}
}
//开始逆向生成赫夫曼编码
void CreatHuffmanCode(HuffmanTree &HT, HuffmanCode *HC, char *name, int n)
{
int i;
//编码的起始指针
int start;
//标记父结点
int p;
//标记子结点
unsigned int c;
//给n个编码的分配头指针
HC=(HuffmanCode *)malloc((n+1) * sizeof(char *));
//当前编码的空间
char *cd = (char *)malloc(n * sizeof(char));
//从右向左
cd[n-1] = '\0';
for(i = 1; i <= n; i++)
{
//初始化编码起始指针
start = n - 1;
//从叶子到根结点求编码
for(c = i, p = HT[i].parent; p != 0; c = p, p = HT[p].parent)
{
//从右到左的顺序编码入数组内
if( HT[p].lchild == c)
cd[--start] = '0'; //左分支标0
else
cd[--start] = '1'; //右分支标1
}
//为第i个编码分配空间并保存生成的编码
HC[i] = (char *)malloc((n - start) * sizeof(char));
strcpy(HC[i], &cd[start]);
}
//释放cd开始下一组编码的生成
free(cd);
//打印编码序列
for(i = 1; i <= n; i++)
printf("%c:%s\n", name[i], HC[i]);
}
int main(void)
{
HuffmanTree HT;
HuffmanCode HC;
int *w, i, n, m;
char *name;
while(~scanf("%d", &n))
{
w=(int *)malloc((n+1)*sizeof(int));
name=(char *)malloc((n+1)*sizeof(char));
for(i=1; i<=n; i++)
{
getchar();
scanf("%c %d",&name[i], &w[i]);
}
CreateHuffmanTree(HT, w, n);
CreatHuffmanCode(HT, &HC, name, n);
}
return 0;
}
注释:
首先、
这篇博客里的些许内容借鉴了其他人的文章,毕竟数据结构的题目无论是设计,还是Debug,都太费劲了, 我选择了 最优路径。。。。。(流汗)
第二、
自从学习树的第一讲开始,就意识到以后的数据结构也会愈渐复杂,遇到的问题解决起来会越来越费劲,有些东西实现起来简直是肝到爆,一次次的调试更是弄得人心态爆炸,但不管怎么说——不能草草了事。
最后、
老规矩,AC了就赶紧睡觉, 头发最重要。。。。。。。