前言
又快一周没有更新了,最近事情比较多,回家几天拖欠了一些作业和实验,所以最近小杨在疯狂的补作业和实验,所以就没有更新新文章,希望大家理解哈~
有没有细心的小伙伴发现本人改了昵称又换了头像呢,没有的话就让本人来带大家稍微了解一下叭:
之前在刷最右的时候偶然看到了一个关于 山野下 的帖子,看完之后又去仔细的了解了一下,真的很有感触,现在我也是小榆的眼睛啦,想带她看看有关计算机的知识,希望她不会觉得枯燥呀。
哈夫曼树与哈夫曼编码介绍
先来了解一下哈夫曼树:
给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。(定义来自百度百科)
简单来说就是:为了得到最短的路径,按顺序将权值小的结点结合成权值比较大的结点,一直到只有一个根节点,就得到了哈夫曼树,如图:
那么什么又是哈夫曼编码呢?
百度给的定义不是很清楚,所以使用老师给的定义:
在以Wi(i=1,2,3,……,n)为权值构造的哈夫曼树中,把每个结点到其左孩子的边标记为二进制的“0”,把每个结点到其右孩子的边标记为二进制的“1”,将根结点到每个外部结点路径上的二进制数连起来,就构造了该外部结点的最优前缀编码,也就是哈夫曼编码。
如下图:
权值为19的结点的哈夫曼编码为:00;权值为2的结点的哈夫曼编码为:11010
思路
哈夫曼树的建立
根据哈夫曼算法可知:要使得权值之和最小,权值越大的结点就要离根结点月近,权值越小的结点要离根结点越远。所以对于给定的n个权值,每次选取权值最小值和次小值建立一棵新的二叉树,新二叉树的权值等于左右孩子的权值之和。
说明:为什么让权值小的结点离根结点比较远呢?
肯定不是看不起权值小的结点嗷,而是带权路径之和:WPL=W1L2+W2L2*……Wn*Ln,Wn表示第n个结点的权值,Ln表示第n个结点到根结点的路径长度。n肯定是越来越大,所以为了WPL小一点,就拿比较小的权值来跟L相乘啦。
说了那么多要怎么建立哈夫曼树呢?
先将2n-1个结点进行初始化,其中n个外部结点权值、data(是一个结构体成员,自行加入,类型为char,表示的是结点代表的字母)进行初始化,然后对余下的n-1个内部结点进行统一的初始化,将2n-1个结点使用顺序储存在结构体数组中。初始化完成之后开始建立哈夫曼树,每次找到权值最小的、次小且双亲结点为-1的两个结点,将他们的权值之和赋给n以后的结点,直到循环变量大于2n-1完成哈夫曼树的建立。
写了这么多估计小榆也不想看,emm……那我来画图举栗子吧:
上图中有双下划线的结点是外部结点,有5个,没有下划线的结点是内部结点,一共有四个。推导到n的话,哈夫曼树有n个外部结点的话,就有n-1个内部结点,一共有2n-1个结点。所以咱们在初始化的时候,就把原本的n个权值赋值到n个外部结点,至于内部结点,咋那么可以先将它们的权值赋值为-1,然后再根据外部结点构造内部结点。
对于那么多结点又该怎么储存呢?
嘿嘿,前面有说喔,肯定没有认认真真的看,那我再仔细一点解释一下吧:
采用结构体数组来对结点进行储存,每个结构体代表的也就是一个结点。
结点拥有以下成员变量:
(注意嗷,还只是结点,是零散的)
这个是哈夫曼树的结构体,其中有指向结点的指针 ht,咋们只需要在分配空间的时候,分配2n-1个HuffNode大小的空间给 ht,就可以直接将结构体按顺序当做数组来使用啦
对外部结点进行哈夫曼编码
既然咱们建立哈夫曼树使用的是顺序储存,那么在遍历哈夫曼树的时候就不需要像链式储存那样需要移动很多的指针,然后还很容易扑街阿巴阿巴的……在这个时候还是顺序储存比较香。
那么应该如何对哈夫曼树的外部结点进行哈夫曼编码呢?
有的小伙伴们可能就会说,肯定是从头开始这样慢慢找下去呀,按照先序(中序、后序)进行遍历,根左右,左根右……直到找到叶子结点,进行编码就好了,可是这样做的代价可能有些大,是不是光是想想就觉得有一些可怕,而且这样做话编码顺序怎么保证呢?要不就乱了顺序,要不就还得再排序,也不是很上乘。有没有更加舒服一些的办法呢?
咱们初始化的时候已经将外部结点的权值已经全部初始化好了的,而且建立哈夫曼树的时候是要严格按照权值大小进行建立的,而且外部结点的数量n是已知的,权值最小的那个结点在结构体数组里面的位置肯定是0,次小的位置为1……一次类推,直到给n个外部结点全部进行编码,然后就可以结束啦。
编码:上面已经说明了, 要给n个结点进行编码,这个时候就应该来一波for(int i=0;i<n;i++)来进行外部循环。那么内部如何操作呢? 从权值最小的结点,再找到该结点的父结点(合理利用结构体数组下标和成员变量),判断该结点是父结点的左孩子还是右孩子,若是做孩子的话编码为0,右孩子编码为1,将编码储存在一个整形数组code里面,如此反复,找到父结点的父结点,判断左右,储存0或1……一直到根节点时结束,那么内部循环就应该是 while (pht->ht[num].parent != -1),因为根结点没有父结点,所以父结点编号为-1.
上面则是进行哈夫曼编码的详细思路,补充说明一下数组code相关的。code就是一个普普通通的整形数组,数组大小为n(n个结点的哈夫曼树的层数最多为n-1,之前不知道为啥多留了一个,不管啦),按照顺序将每个二进制数放到数组里,到一个结点编码完成之后就将数组code逆向输出:因为咱们使用的是从外部结点遍历到根结点的方法,所以得到的哈夫曼编码是反的,所以需要逆着输出。
代码实现
好啦,说了那么多理论的东西,是时候来写代码啦~
定义和理论大致了解就行,重要的还是能不能理解算法并且写出代码。那就按照上面思路的顺序来给出代码叭,方便理解一些:
相关宏定义和结构体定义:
constexpr auto Maxn = 8; //定义题目中的结点个数
constexpr auto M = 10000; //定义一个相对于权值很大的值
typedef char Datatype;
struct HuffNode
{
Datatype data; //定义该结点的名字
int weight; //定义权值
int parent; //定义父节点位置
int leftchild; //定义左孩子位置
int rightchild; //定义右孩子位置
};
typedef struct HuffNode* HtNode;
typedef struct HuffTreeNode
{
int n; //定义哈夫曼树的叶子结点个数
int root; //定义哈夫曼树的树根
HtNode ht; //定义指向哈夫曼树的指针
}*HuffTree;
哈夫曼树建立:
HuffTree CreateHuffTree(int n,int w[],char data[])
{
HuffTree pht;
int i = 0, j = 0; //定义循环变量
int x1, x2, min1, min2; //定义最小值下标、次小值下标、最小值、次小值
pht = (HuffTree)malloc(sizeof(struct HuffTreeNode));
if (pht == NULL)
{
printf("pht malloc Out of space!\n");
return pht;
}
pht->ht = (HtNode)malloc(sizeof(struct HuffNode) * (2 * n - 1)); //为哈夫曼树分配2n-1个结点空间
if (pht->ht == NULL)
{
printf("ht malloc Out of space!\n");
return pht;
}
//初始化哈夫曼树
for (i = 0; i < 2 * n - 1; i++)
{
pht->ht[i].leftchild = -1; //初始化叶结点左孩子
pht->ht[i].rightchild = -1; //初始化叶结点右孩子
pht->ht[i].parent = -1; //初始化叶结点的父结点
if (i < n)
{
pht->ht[i].weight = w[i]; //赋值叶子结点的权值
pht->ht[i].data = data[i]; //初始化叶子结点的data
}
else
{
pht->ht[i].weight = -1; //初始化内部结点的权值为-1
}
}
//构造内部结点
for (i = 0; i < n - 1; i++)
{
min1 = M; //初始化最小值为100
min2 = M; //初始化次小值为100
x1 = -1; //初始化最小值下标为-1
x2 = -1; //初始化次小值下标为-1
for (j = 0; j < n + i; j++)
{
//找到最小值下标赋值给x1,最小值赋值给min1
if (pht->ht[j].weight < min1 && pht->ht[j].parent == -1 )
{
min2 = min1;
x2 = x1;
min1 = pht->ht[j].weight;
x1 = j;
}
//找到次小值下标赋值给x2,最次值赋值给min2
else if (pht->ht[j].weight < min2 && pht->ht[j].parent == -1 )
{
min2 = pht->ht[j].weight;
x2 = j;
}
}
pht->ht[n + i].weight = min1 + min2; //初始化父结点的权值
pht->ht[n + i].leftchild = x1; //给父结点左孩子赋值
pht->ht[n + i].rightchild = x2; //给父结点右孩子赋值
pht->ht[x1].parent = n + i; //给左孩子的父结点赋值
pht->ht[x2].parent = n + i; //给右孩子的父结点赋值
}
pht->root = 2 * n - 2; //根节点位置
pht->n = n; //哈夫曼树的叶子结点数
return pht;
}
对结点进行哈夫曼编码:
//该函数用于输出叶子结点的哈夫曼编码
void PrintHuffCode(int n,HuffTree pht)
{
int i = 0, num = 0, j = 0; //定义循环变量
int parent = 0; //定义父结点下标临时变量
int times = 0; //记录每个结点哈夫曼编码的长度
int code[Maxn];
for (i = 0; i < n; i++) //循环给每个叶子结点进行编码
{
num = i; //初始化用于控制循环的变量
j = 0; //初始化用于控制数组code的变量
times = 0;
while (pht->ht[num].parent != -1) //循环到当前结点的父结点为-1
{
parent = pht->ht[num].parent;
if (pht->ht[parent].leftchild == num) //当前结点为父结点的左孩子,编码为0
{
code[j] = 0;
j++;
times++; //编码长度加1
}
else if (pht->ht[parent].rightchild == num) //当前结点为父结点的右孩子,编码为1
{
code[j] = 1;
j++;
times++; //编码长度加1
}
num = parent; //将num赋值为父结点下标
}
printf("%c:", pht->ht[i].data); //输出第i个叶子结点的data
for (j = times-1; j >= 0; j--) //输出第i个叶子结点的哈夫曼编码
{
printf("%d", code[j]);
}
printf("\n");
}
return;
}
还有一个用于测试的函数,可以将所有的结点信息打印出来:
//该函数用于打印哈夫曼树的所有结点
void PrintHuffTree(HuffTree pht,int n)
{
for (int i = 0; i < 2 * n - 1; i++)
{
printf("第%d个结点:weight=%d parent= %d leftchild=%d rightchild=%d\n", i, pht->ht[i].weight, pht->ht[i].parent, pht->ht[i].leftchild, pht->ht[i].rightchild);
}
return;
}
最后就是咱们的主函数啦:
/*
测试输入:
ABCDEFGH
2 3 6 7 10 19 21 32
*/
//注意建立的哈夫曼树严格按照左小右次小的顺序,并且哈夫曼编码时严格按照左‘0’右‘1’进行编码
int main()
{
int i = 0;
int w[Maxn];
char data[Maxn];
//依次输入结点的名称,不需要加空格
for (i = 0; i < Maxn; i++)
{
scanf_s("%c", &data[i]);
}
//依次输入结点的权值
for (i = 0; i < Maxn; i++)
{
scanf_s("%d", &w[i]);
}
/*for (i = 0; i < Maxn; i++) //用于测试
{
printf("%c:%d\n", data[i], w[i]);
}*/
HuffTree pht = CreateHuffTree(i,w,data);
printf("\n哈夫曼树为:\n");
PrintHuffTree(pht,i);
printf("\n对外部结点进行哈夫曼编码:\n");
PrintHuffCode(i,pht);
return 0;
}
运行结果:
(可以自己适当增加提示来提升代码可读性)
注意:编译环境为visual studio2019,若是使用其他环境的小伙伴,记得修改相关函数喔(比如 scanf_s和constexpr auto),发现问题欢迎评论区或者私聊本人指正~
勉强算是总结
与其说是总结,倒不如说是声明。改这个ID和头像并不是为了博人眼球或者吸引关注,一是对小榆的一种怀念,想将自己学到的东西写成博客,给小榆分享一些不一样的东西。二则是对自己的一种激励,提醒自己记得按时更新博客。仅此而已嗷。
小榆是美丽的天使,只不过她回到了原来的地方,但是我们可以通过自己方式去成为她的眼睛,继续看世界的美好。在山野下,记得要热爱生活鸭~
本篇文章到这里就结束了,小伙伴们,掰掰~