哈夫曼树编码的实现+图解(含全部代码)
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
目录
------------哈夫曼树的构造方法
------------------------哈夫曼编码
------------------------------------全部代码
哈夫曼树的基本概念
哈夫曼树通常以二叉树的形式出现所以也称最优二叉树是一类带权路径长度最短的树
首先得知道下以下几个术语
路径从树中的一个结点到另一个结点之间的分支构成这两点之间的路径
路径长度路径上的分支数目称作路径长度
树的路径长度从树根到每一个结点的路径长度之和
权赋予某个实体的一个量
结点的带权路径长度从该结点到树根之间的路径长度与结点上权的乘积
树的带权路径长度树中所有叶子结点的带权路径长度之和
哈夫曼树的构造方法
哈夫曼树的实现利用了贪心算法的理念就是先给定的若干结点的权进行分割让它们变为单个的森林或者说是单个的树因为树肯定是森林而后在其中选择两个权值最小的结点生成新的结点而后删除被选择的结点让生成的结点参与森林中进行选拔直至无结点进行选拔。
不好意思结点搞的太多了。。。 每次生成都是到最后的原因是因为后面的代码不然最后的结果不是一样的当然哈夫曼树并不唯一但是为了让最后答案一样我选择这样画这个过程也是后面构建哈夫曼树代码的过程后面代码看不懂的可以结合这个图一起理解。
接下来就是哈夫曼树的存储结构了因为每个结点需要权值孩子结点与双亲结点的地址和结点的数据所以我们需要用一个结构体来存储。而且这是二叉树的顺序存储。
typedef struct
{
char data; //结点的数据
int parent,lch,rch; //双亲结点和孩子结点的下标
int weight; //结点的权值
}htNode,*HuffmanTree;
这个就是这个结构体的内部图有了存储结构后我们要做的就是初始化因为是顺序存储所以我们要知道给的这些结点构成哈夫曼树需要多少空间因为我们要节省空间所以需要用动态分配的方法来解决。
而我们知道假设有n个结点构成哈夫曼树则会生成2n-1个结点这是因为每个结点都会有个双亲而根结点没有双亲生成结点和边数相同为n-1所以为2n-1而我们在构成哈夫曼树时0下标是不用的所以我们真正要申请的空间为2n个。
知道这些后我们就可以来初始化哈夫曼树了我们在各自输入权值和结点数据后还需把各个结点的孩子与双亲的值置为-1置为-1的好处就是能当个flag也就是现在都没有双亲都是森林而且在生成的过程中不会出现和它相同的值以下是初始化的代码
int initHuffmanTree(huffmanTree& HT)
{
HT = (htNode*)malloc(sizeof(htNode) * (2 * NODENUM)); //给HT分配2 * NODENUM个htNOde大小的htNode类型的数组
for (int i = 1; i <= 2 * NODENUM - 1; i++) //下标从1开始到2 * NODENUM
{
HT[i].parent = HT[i].lch = HT[i].rch = -1; //双亲和孩子的值都置为-1
}
printf("please input some weight!\n");
for (int i = 1; i <= NODENUM; i++) //权值只有1-n个
{
scanf("%d",&HT[i].weight); //给每个结点赋予权值
}
char c = getchar(); //这个来接收上面的回车
printf("please input some data!\n");
for (int i = 1; i <= NODENUM; i++)
{
//scanf("%c ",&HT[i].data);
char a = getchar();
if(a == '\n') //遇到回车就结束
break;
else
HT[i].data = a; //给每个结点赋予数据
}
return 1;
}
这是初始化后的结果。初始化完后我们就可以开始构建哈夫曼树啦构建的思想和刚才那张图一样就是找到最小的两个结点然后权值相加然后最小的两个结点的parent的值为新生成结点的下标新生成结点的孩子的值为两个最小结点的下标。举个例子你就懂了
eq现在最小的为14则它们相加的结果要放在下标为11的位置并且parent的值改为11而11的孩子的值为12。更新后如下
经过n-1次操作后最终就成了哈夫曼树 下面为最终结果有颜色的为生成结点部分
可以直观的看到在最后一个结点下标为19parent值为-1所以这个就是根结点表示没有双亲。
现在的问题是我们知道是怎么回事那怎么用代码来实现它呢
首先先设置2个变量存储最小值2个变量存储最小值的下标因为要生成n-1个结点所以我们要操作n-1次且生成一次我们要比较的次数就会增加1而且不难看出来最后一次不用比较了所以我们比较的次数要比现总结点数小1按我们的例子来说我们要生成19个结点且比较18次那我们剩下的工作就是如何找最小值并且生成新的结点我们的逻辑思维应该是利用4个变量去记住值和下标
并且只合并parent为-1的结点parent不为-1的我们就不用再去判断了代码如下
#define MAXVALUE 32767
void creatHuffmanTree(huffmanTree& HT, int n)
{
if (n <= 1) //如果结点数小于等于1不创建
return;
int min1, min2; //定义两个数来存储每次选取最小两个结点的权值
int rnode, lnode; //定义两个下标值来存储每次选取最小两个结点的下标
for (int i = n + 1; i <= 2 * n -1; i++) //要生成n-1个结点所以要操作n—1次且从下标为n+1开始存储
{
int min1 = MAXVALUE; int lnode = -1; //让最小值初始化为极大值这样叶子结点的最大值再大也不会超过这个值了
int min2 = MAXVALUE; int rnode = -1;
for (int j = 1; j <= i - 1; j++) //因为起先是在前n个中选择最小的两个结点的权值但新生成一个后就得在前n+1个中选择最小的两个结点的权值
{ //假设n = 10 总结点数就得为19那我们就只要比较18次就可以得出结果了记住比较的次数比生成的总结点数少1
if (HT[j].weight < min1 && HT[j].parent == -1) //这个小于就使得当出现相同的权值时优先考虑先出现的值可以假设下
{
min2 = min1; rnode = lnode; //碰到比min1小的那min1的值就给第二小的min2下标也给它
min1 = HT[j].weight; lnode = j; //然后最小的给min1下标同理
}
else if (HT[j].weight < min2 && HT[j].parent == -1) //这是第二小的判断
{
min2 = HT[j].weight;
rnode = j;
}
}
HT[lnode].parent = HT[rnode].parent = i; //最小两个结点的parent变为生成结点的下标
HT[i].lch = lnode; HT[i].rch = rnode; //生成结点的左孩子为最小的min1的下标右孩子同理
HT[i].weight = HT[lnode].weight + HT[rnode].weight; //生成结点的权值等于最小结点的权值相加
}
}
在此我给出第一次循环的图示接下来就是同理了
构成哈夫曼树就在此结束并完成了而后就是编码。
哈夫曼编码
哈夫曼编码基于哈夫曼树而产生的一种好编码具体干嘛的我不说了百度一下你就知道因为现在已经很晚了我想一夜干完哈哈哈
上面已经完成哈夫曼树的构成了那么编码就是左子树上的为0右子树上的为1再自根结点扫描下来到叶子结点输出的值就为哈夫曼编码。
那我们第一步得确定装编码的存储结构可以从图中看出需要一个大数组装很多装编码的小数组那我们就选择指针数组因为指针变量就类似一个数组指针数组是装指针的数组那就符合我们的需求了。
typedef char** huffmanCode; //第一个*是代表它是指针变量说明它是数组
//第二个*说明它是指针数组代表这个char类型数组里每个元素都是*huffmanCode变量
那我们的思路就是搞个临时数组把编码记录下来然后再给这个大数组里的小数组那我们怎么求编码呢这就用到刚才看到那个最后一个结点的parent了因为它的值是-1所以它被我们定义为根结点所以我们只要顺着parent存着的下标一步一步找上去就行了而后就是左孩子为0右孩子为1。下面为代码
void createHuffmanCode(huffmanTree HT, huffmanCode& HC, int n)
{
HC = (huffmanCode)malloc(sizeof(huffmanCode) * n + 1); //申请n + 1个huffmanCode大小huffmanCode类型的临时空间
//因为下标是从一开始所以我们要申请比结点多一个的结点和哈夫曼树的结构对应方便输出
char* cd = (char*)malloc(sizeof(char) * n); //申请n个char大小char类型的临时空间这个临时数组记录每次遍历出来的编码
int start = 0,c = 0,f = 0; //start为cd数组记录下标c初始为叶子结点下标而后就是孩子结点的下标f记录双亲结点的下标
cd[n - 1] = '\0'; //这个就是给printf留着的因为printf不会生成'\0'如果用puts就不用这句语句了
for (int i = 1; i <= n; i++) //只要叶子结点的编码
{
start = n - 1; //这句要赋值n的话start--要写在判断后方
c = i;
f = HT[c].parent;
while (f != -1) //根节点没有双亲
{
start--;
if (HT[f].lch == c) //是左孩子就是0右孩子就为1
cd[start] = '0';
else
cd[start] = '1';
c = f; f = HT[c].parent; //向根结点接近
}
HC[i] = (char*)malloc(sizeof(char) * (n - start)); //给数组里的数组申请n - start个char大小的char*类型的临时空间
strcpy(HC[i], &cd[start]); //cd里记录的编码给HC的第i个数组
}
free(cd); //释放临时空间
}
出一个循环的图这图画的我累死。
全部代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <malloc.h>
#include <string.h>
#define MAXVALUE 32767 //极大值相当于无穷大
#define NODENUM 10 //叶子结点数
typedef struct
{
char data; //数据域
int weight; //结点的权值
int parent, lch, rch; //双亲与孩子的下标
}htNode,*huffmanTree;
typedef char** huffmanCode; //第一个*是代表它是指针变量说明它是数组
//第二个*说明它是指针数组代表这个char类型数组里每个元素都是*huffmanCode变量
int initHuffmanTree(huffmanTree& HT); //初始化哈夫曼树
void creatHuffmanTree(huffmanTree& HT, int n); //构建哈夫曼树
void createHuffmanCode(huffmanTree HT, huffmanCode &HC, int n); //编写哈夫曼编码
int main()
{
huffmanTree HT ;
initHuffmanTree(HT);
huffmanCode HC;
creatHuffmanTree(HT,NODENUM);
createHuffmanCode(HT,HC,NODENUM);
/*for (int i = NODENUM + 1; i <= 2 * NODENUM - 1; i++)
printf("%d ", HT[i].weight);*/
for (int i = 1; i <= NODENUM; i++) //遍历输出编码
{
printf("%c:\t",HT[i].data);
printf("%s\n", HC[i]);
}
return 0;
}
int initHuffmanTree(huffmanTree& HT)
{
HT = (htNode*)malloc(sizeof(htNode) * (2 * NODENUM)); //给HT分配2 * NODENUM个htNOde大小的htNode类型的数组
for (int i = 1; i <= 2 * NODENUM - 1; i++) //下标从1开始到2 * NODENUM
{
HT[i].parent = HT[i].lch = HT[i].rch = -1; //双亲和孩子的值都置为-1
}
printf("please input some weight!\n");
for (int i = 1; i <= NODENUM; i++) //权值只有1-n个
{
scanf("%d",&HT[i].weight); //给每个结点赋予权值
}
char c = getchar(); //这个来接收上面的回车
printf("please input some data!\n");
for (int i = 1; i <= NODENUM; i++)
{
//scanf("%c ",&HT[i].data);
char a = getchar();
if(a == '\n') //遇到回车就结束
break;
else
HT[i].data = a; //给每个结点赋予数据
}
return 1;
}
void creatHuffmanTree(huffmanTree& HT, int n)
{
if (n <= 1) //如果结点数小于等于1不创建
return;
int min1, min2; //定义两个数来存储每次选取最小两个结点的权值
int rnode, lnode; //定义两个下标值来存储每次选取最小两个结点的下标
for (int i = n + 1; i <= 2 * n -1; i++) //要生成n-1个结点所以要操作n—1次且从下标为n+1开始存储
{
int min1 = MAXVALUE; int lnode = -1; //让最小值初始化为极大值这样叶子结点的最大值再大也不会超过这个值了
int min2 = MAXVALUE; int rnode = -1;
for (int j = 1; j <= i - 1; j++) //因为起先是在前n个中选择最小的两个结点的权值但新生成一个后就得在前n+1个中选择最小的两个结点的权值
{ //假设n = 10 总结点数就得为19那我们就只要比较18次就可以得出结果了记住比较的次数比生成的总结点数少1
if (HT[j].weight < min1 && HT[j].parent == -1) //这个小于就使得当出现相同的权值时优先考虑先出现的值可以假设下
{
min2 = min1; rnode = lnode; //碰到比min1小的那min1的值就给第二小的min2下标也给它
min1 = HT[j].weight; lnode = j; //然后最小的给min1下标同理
}
else if (HT[j].weight < min2 && HT[j].parent == -1) //这是第二小的判断
{
min2 = HT[j].weight;
rnode = j;
}
}
HT[lnode].parent = HT[rnode].parent = i; //最小两个结点的parent变为生成结点的下标
HT[i].lch = lnode; HT[i].rch = rnode; //生成结点的左孩子为最小的min1的下标右孩子同理
HT[i].weight = HT[lnode].weight + HT[rnode].weight; //生成结点的权值等于最小结点的权值相加
}
}
void createHuffmanCode(huffmanTree HT, huffmanCode& HC, int n)
{
HC = (huffmanCode)malloc(sizeof(huffmanCode) * n + 1); //申请n + 1个huffmanCode大小huffmanCode类型的临时空间
//因为下标是从一开始所以我们要申请比结点多一个的结点和哈夫曼树的结构对应方便输出
char* cd = (char*)malloc(sizeof(char) * n); //申请n个char大小char类型的临时空间这个临时数组记录每次遍历出来的编码
int start = 0,c = 0,f = 0; //start为cd数组记录下标c初始为叶子结点下标而后就是孩子结点的下标f记录双亲结点的下标
cd[n - 1] = '\0'; //这个就是给printf留着的因为printf不会生成'\0'如果用puts就不用这句语句了
for (int i = 1; i <= n; i++) //只要叶子结点的编码
{
start = n - 1; //这句要赋值n的话start--要写在判断后方
c = i;
f = HT[c].parent;
while (f != -1) //根节点没有双亲
{
start--;
if (HT[f].lch == c) //是左孩子就是0右孩子就为1
cd[start] = '0';
else
cd[start] = '1';
c = f; f = HT[c].parent; //向根结点接近
}
HC[i] = (char*)malloc(sizeof(char) * (n - start)); //给数组里的数组申请n - start个char大小的char*类型的临时空间
strcpy(HC[i], &cd[start]); //cd里记录的编码给HC的第i个数组
}
free(cd); //释放临时空间
}
还得加紧学习只能写到这了有问题的请指出问问题的可以评论哦然后我有空再在有问题的地方再讲细点。
从严老师的教材中学来------------------------------------------------------------------------------------------