深圳幻海软件技术有限公司 欢迎您!

哈夫曼树编码的实现+图解(含全部代码)

2023-04-26

目录哈夫曼树的基本概念------------哈夫曼树的构造方法 ------------------------哈夫曼编码------------------------------------全部代码 哈夫曼树的基本概念    &nbs

目录


哈夫曼树的基本概念

------------哈夫曼树的构造方法 

------------------------哈夫曼编码

------------------------------------全部代码 


哈夫曼树的基本概念

        哈夫曼树通常以二叉树的形式出现,所以也称最优二叉树,是一类带权路径长度最短的树

首先得知道下以下几个术语:

        路径:从树中的一个结点到另一个结点之间的分支构成这两点之间的路径

        路径长度:路径上的分支数目称作路径长度

        树的路径长度:从树根到每一个结点的路径长度之和

        :赋予某个实体的一个量

        结点的带权路径长度:从该结点到树根之间的路径长度与结点上权的乘积

        树的带权路径长度:树中所有叶子结点的带权路径长度之和

哈夫曼树的构造方法 

        哈夫曼树的实现利用了贪心算法的理念,就是先给定的若干结点的权进行分割,让它们变为单个的森林或者说是单个的树,因为树肯定是森林,而后在其中选择两个权值最小的结点生成新的结点,而后删除被选择的结点,让生成的结点参与森林中进行选拔,直至无结点进行选拔。

        不好意思结点搞的太多了。。。 每次生成都是到最后的原因是因为后面的代码,不然最后的结果不是一样的,当然哈夫曼树并不唯一,但是为了让最后答案一样,我选择这样画,这个过程也是后面构建哈夫曼树代码的过程,后面代码看不懂的可以结合这个图一起理解。

        接下来就是哈夫曼树的存储结构了,因为每个结点需要权值,孩子结点与双亲结点的地址和结点的数据,所以我们需要用一个结构体来存储。而且这是二叉树的顺序存储。

  1. typedef struct
  2. {
  3. char data; //结点的数据
  4. int parent,lch,rch; //双亲结点和孩子结点的下标
  5. int weight; //结点的权值
  6. }htNode,*HuffmanTree;

                         

        这个就是这个结构体的内部图,有了存储结构后,我们要做的就是初始化,因为是顺序存储,所以我们要知道给的这些结点构成哈夫曼树需要多少空间,因为我们要节省空间,所以需要用动态分配的方法来解决。

        而我们知道,假设有n个结点构成哈夫曼树则会生成2n-1个结点,这是因为每个结点都会有个双亲,而根结点没有双亲,生成结点和边数相同为n-1,所以为2n-1,而我们在构成哈夫曼树时0下标是不用的,所以我们真正要申请的空间为2n个。

        知道这些后我们就可以来初始化哈夫曼树了,我们在各自输入权值和结点数据后还需把各个结点的孩子与双亲的值置为-1(置为-1的好处就是能当个flag,也就是现在都没有双亲,都是森林,而且在生成的过程中不会出现和它相同的值):以下是初始化的代码

  1. int initHuffmanTree(huffmanTree& HT)
  2. {
  3. HT = (htNode*)malloc(sizeof(htNode) * (2 * NODENUM));//给HT分配2 * NODENUM个htNOde大小的htNode类型的数组
  4. for (int i = 1; i <= 2 * NODENUM - 1; i++)//下标从1开始到2 * NODENUM
  5. {
  6. HT[i].parent = HT[i].lch = HT[i].rch = -1;//双亲和孩子的值都置为-1
  7. }
  8. printf("please input some weight!\n");
  9. for (int i = 1; i <= NODENUM; i++)//权值只有1-n个
  10. {
  11. scanf("%d",&HT[i].weight);//给每个结点赋予权值
  12. }
  13. char c = getchar();//这个来接收上面的回车
  14. printf("please input some data!\n");
  15. for (int i = 1; i <= NODENUM; i++)
  16. {
  17. //scanf("%c ",&HT[i].data);
  18. char a = getchar();
  19. if(a == '\n')//遇到回车就结束
  20. break;
  21. else
  22. HT[i].data = a;//给每个结点赋予数据
  23. }
  24. return 1;
  25. }

        

          这是初始化后的结果。初始化完后我们就可以开始构建哈夫曼树啦,构建的思想和刚才那张图一样,就是找到最小的两个结点,然后权值相加,然后最小的两个结点的parent的值为新生成结点的下标,新生成结点的孩子的值为两个最小结点的下标。举个例子你就懂了

          eq:现在最小的为1,4,则它们相加的结果要放在下标为11的位置,并且parent的值改为11,而11的孩子的值为1,2。更新后如下:

        经过n-1次操作后,最终就成了哈夫曼树 ,下面为最终结果,有颜色的为生成结点部分:

         可以直观的看到,在最后一个结点,下标为19parent值为-1,所以这个就是根结点,表示没有双亲。

        现在的问题是,我们知道是怎么回事,那怎么用代码来实现它呢

        首先先设置2个变量存储最小值,2个变量存储最小值的下标,因为要生成n-1个结点,所以我们要操作n-1次,且生成一次我们要比较的次数就会增加1,而且不难看出来最后一次不用比较了,所以我们比较的次数要比现总结点数小1,按我们的例子来说,我们要生成19个结点,且比较18次,那我们剩下的工作就是如何找最小值并且生成新的结点,我们的逻辑思维应该是利用4个变量去记住值和下标

        并且只合并parent为-1的结点,parent不为-1的我们就不用再去判断了,代码如下:

  1. #define MAXVALUE 32767
  2. void creatHuffmanTree(huffmanTree& HT, int n)
  3. {
  4. if (n <= 1)//如果结点数小于等于1,不创建
  5. return;
  6. int min1, min2;//定义两个数,来存储每次选取最小两个结点的权值
  7. int rnode, lnode;//定义两个下标值,来存储每次选取最小两个结点的下标
  8. for (int i = n + 1; i <= 2 * n -1; i++)//要生成n-1个结点,所以要操作n—1次且从下标为n+1开始存储
  9. {
  10. int min1 = MAXVALUE; int lnode = -1;//让最小值初始化为极大值,这样叶子结点的最大值再大也不会超过这个值了
  11. int min2 = MAXVALUE; int rnode = -1;
  12. for (int j = 1; j <= i - 1; j++)//因为起先是在前n个中选择最小的两个结点的权值,但新生成一个后就得在前n+1个中选择最小的两个结点的权值
  13. {//假设n = 10 总结点数就得为19,那我们就只要比较18次就可以得出结果了,记住比较的次数比生成的总结点数少1
  14. if (HT[j].weight < min1 && HT[j].parent == -1)//这个小于就使得当出现相同的权值时优先考虑先出现的值,可以假设下
  15. {
  16. min2 = min1;rnode = lnode;//碰到比min1小的,那min1的值就给第二小的min2,下标也给它
  17. min1 = HT[j].weight; lnode = j;//然后最小的给min1,下标同理
  18. }
  19. else if (HT[j].weight < min2 && HT[j].parent == -1)//这是第二小的判断
  20. {
  21. min2 = HT[j].weight;
  22. rnode = j;
  23. }
  24. }
  25. HT[lnode].parent = HT[rnode].parent = i;//最小两个结点的parent变为生成结点的下标
  26. HT[i].lch = lnode; HT[i].rch = rnode;//生成结点的左孩子为最小的min1的下标,右孩子同理
  27. HT[i].weight = HT[lnode].weight + HT[rnode].weight;//生成结点的权值等于最小结点的权值相加
  28. }
  29. }

         在此我给出第一次循环的图示,接下来就是同理了:

         构成哈夫曼树就在此结束并完成了,而后就是编码。

哈夫曼编码

        哈夫曼编码基于哈夫曼树而产生的一种好编码,具体干嘛的我不说了,百度一下你就知道,因为现在已经很晚了,我想一夜干完,哈哈哈

        上面已经完成哈夫曼树的构成了,那么编码就是左子树上的为0,右子树上的为1,再自根结点扫描下来到叶子结点,输出的值就为哈夫曼编码。

        

        那我们第一步得确定装编码的存储结构,可以从图中看出,需要一个大数组装很多装编码的小数组,那我们就选择指针数组,因为指针变量就类似一个数组,指针数组是装指针的数组,那就符合我们的需求了。

  1. typedef char** huffmanCode;//第一个*是代表它是指针变量,说明它是数组
  2. //第二个*说明它是指针数组,代表这个char类型数组里每个元素都是*huffmanCode变量

         

        

         那我们的思路就是搞个临时数组把编码记录下来,然后再给这个大数组里的小数组,那我们怎么求编码呢,这就用到刚才看到那个最后一个结点的parent了,因为它的值是-1,所以它被我们定义为根结点,所以我们只要顺着parent存着的下标一步一步找上去就行了,而后就是左孩子为0,右孩子为1。下面为代码:

  1. void createHuffmanCode(huffmanTree HT, huffmanCode& HC, int n)
  2. {
  3. HC = (huffmanCode)malloc(sizeof(huffmanCode) * n + 1);//申请n + 1个huffmanCode大小huffmanCode类型的临时空间
  4. //因为下标是从一开始,所以我们要申请比结点多一个的结点,和哈夫曼树的结构对应,方便输出
  5. char* cd = (char*)malloc(sizeof(char) * n);//申请n个char大小char类型的临时空间,这个临时数组记录每次遍历出来的编码
  6. int start = 0,c = 0,f = 0;//start为cd数组记录下标,c初始为叶子结点下标,而后就是孩子结点的下标,f记录双亲结点的下标
  7. cd[n - 1] = '\0';//这个就是给printf留着的,因为printf不会生成'\0',如果用puts就不用这句语句了
  8. for (int i = 1; i <= n; i++)//只要叶子结点的编码
  9. {
  10. start = n - 1;//这句要赋值n的话,start--要写在判断后方
  11. c = i;
  12. f = HT[c].parent;
  13. while (f != -1)//根节点没有双亲
  14. {
  15. start--;
  16. if (HT[f].lch == c)//是左孩子就是0,右孩子就为1
  17. cd[start] = '0';
  18. else
  19. cd[start] = '1';
  20. c = f; f = HT[c].parent;//向根结点接近
  21. }
  22. HC[i] = (char*)malloc(sizeof(char) * (n - start));//给数组里的数组申请n - start个char大小的char*类型的临时空间
  23. strcpy(HC[i], &cd[start]);//cd里记录的编码给HC的第i个数组
  24. }
  25. free(cd);//释放临时空间
  26. }

         出一个循环的图,这图画的我累死。

        全部代码 

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include <stdio.h>
  3. #include <malloc.h>
  4. #include <string.h>
  5. #define MAXVALUE 32767//极大值相当于无穷大
  6. #define NODENUM 10//叶子结点数
  7. typedef struct
  8. {
  9. char data;//数据域
  10. int weight;//结点的权值
  11. int parent, lch, rch;//双亲与孩子的下标
  12. }htNode,*huffmanTree;
  13. typedef char** huffmanCode;//第一个*是代表它是指针变量,说明它是数组
  14. //第二个*说明它是指针数组,代表这个char类型数组里每个元素都是*huffmanCode变量
  15. int initHuffmanTree(huffmanTree& HT);//初始化哈夫曼树
  16. void creatHuffmanTree(huffmanTree& HT, int n);//构建哈夫曼树
  17. void createHuffmanCode(huffmanTree HT, huffmanCode &HC, int n);//编写哈夫曼编码
  18. int main()
  19. {
  20. huffmanTree HT ;
  21. initHuffmanTree(HT);
  22. huffmanCode HC;
  23. creatHuffmanTree(HT,NODENUM);
  24. createHuffmanCode(HT,HC,NODENUM);
  25. /*for (int i = NODENUM + 1; i <= 2 * NODENUM - 1; i++)
  26. printf("%d ", HT[i].weight);*/
  27. for (int i = 1; i <= NODENUM; i++)//遍历输出编码
  28. {
  29. printf("%c:\t",HT[i].data);
  30. printf("%s\n", HC[i]);
  31. }
  32. return 0;
  33. }
  34. int initHuffmanTree(huffmanTree& HT)
  35. {
  36. HT = (htNode*)malloc(sizeof(htNode) * (2 * NODENUM));//给HT分配2 * NODENUM个htNOde大小的htNode类型的数组
  37. for (int i = 1; i <= 2 * NODENUM - 1; i++)//下标从1开始到2 * NODENUM
  38. {
  39. HT[i].parent = HT[i].lch = HT[i].rch = -1;//双亲和孩子的值都置为-1
  40. }
  41. printf("please input some weight!\n");
  42. for (int i = 1; i <= NODENUM; i++)//权值只有1-n个
  43. {
  44. scanf("%d",&HT[i].weight);//给每个结点赋予权值
  45. }
  46. char c = getchar();//这个来接收上面的回车
  47. printf("please input some data!\n");
  48. for (int i = 1; i <= NODENUM; i++)
  49. {
  50. //scanf("%c ",&HT[i].data);
  51. char a = getchar();
  52. if(a == '\n')//遇到回车就结束
  53. break;
  54. else
  55. HT[i].data = a;//给每个结点赋予数据
  56. }
  57. return 1;
  58. }
  59. void creatHuffmanTree(huffmanTree& HT, int n)
  60. {
  61. if (n <= 1)//如果结点数小于等于1,不创建
  62. return;
  63. int min1, min2;//定义两个数,来存储每次选取最小两个结点的权值
  64. int rnode, lnode;//定义两个下标值,来存储每次选取最小两个结点的下标
  65. for (int i = n + 1; i <= 2 * n -1; i++)//要生成n-1个结点,所以要操作n—1次且从下标为n+1开始存储
  66. {
  67. int min1 = MAXVALUE; int lnode = -1;//让最小值初始化为极大值,这样叶子结点的最大值再大也不会超过这个值了
  68. int min2 = MAXVALUE; int rnode = -1;
  69. for (int j = 1; j <= i - 1; j++)//因为起先是在前n个中选择最小的两个结点的权值,但新生成一个后就得在前n+1个中选择最小的两个结点的权值
  70. {//假设n = 10 总结点数就得为19,那我们就只要比较18次就可以得出结果了,记住比较的次数比生成的总结点数少1
  71. if (HT[j].weight < min1 && HT[j].parent == -1)//这个小于就使得当出现相同的权值时优先考虑先出现的值,可以假设下
  72. {
  73. min2 = min1;rnode = lnode;//碰到比min1小的,那min1的值就给第二小的min2,下标也给它
  74. min1 = HT[j].weight; lnode = j;//然后最小的给min1,下标同理
  75. }
  76. else if (HT[j].weight < min2 && HT[j].parent == -1)//这是第二小的判断
  77. {
  78. min2 = HT[j].weight;
  79. rnode = j;
  80. }
  81. }
  82. HT[lnode].parent = HT[rnode].parent = i;//最小两个结点的parent变为生成结点的下标
  83. HT[i].lch = lnode; HT[i].rch = rnode;//生成结点的左孩子为最小的min1的下标,右孩子同理
  84. HT[i].weight = HT[lnode].weight + HT[rnode].weight;//生成结点的权值等于最小结点的权值相加
  85. }
  86. }
  87. void createHuffmanCode(huffmanTree HT, huffmanCode& HC, int n)
  88. {
  89. HC = (huffmanCode)malloc(sizeof(huffmanCode) * n + 1);//申请n + 1个huffmanCode大小huffmanCode类型的临时空间
  90. //因为下标是从一开始,所以我们要申请比结点多一个的结点,和哈夫曼树的结构对应,方便输出
  91. char* cd = (char*)malloc(sizeof(char) * n);//申请n个char大小char类型的临时空间,这个临时数组记录每次遍历出来的编码
  92. int start = 0,c = 0,f = 0;//start为cd数组记录下标,c初始为叶子结点下标,而后就是孩子结点的下标,f记录双亲结点的下标
  93. cd[n - 1] = '\0';//这个就是给printf留着的,因为printf不会生成'\0',如果用puts就不用这句语句了
  94. for (int i = 1; i <= n; i++)//只要叶子结点的编码
  95. {
  96. start = n - 1;//这句要赋值n的话,start--要写在判断后方
  97. c = i;
  98. f = HT[c].parent;
  99. while (f != -1)//根节点没有双亲
  100. {
  101. start--;
  102. if (HT[f].lch == c)//是左孩子就是0,右孩子就为1
  103. cd[start] = '0';
  104. else
  105. cd[start] = '1';
  106. c = f; f = HT[c].parent;//向根结点接近
  107. }
  108. HC[i] = (char*)malloc(sizeof(char) * (n - start));//给数组里的数组申请n - start个char大小的char*类型的临时空间
  109. strcpy(HC[i], &cd[start]);//cd里记录的编码给HC的第i个数组
  110. }
  111. free(cd);//释放临时空间
  112. }

         还得加紧学习,只能写到这了,有问题的请指出,问问题的可以评论哦,然后我有空再在有问题的地方再讲细点。

        从严老师的教材中学来------------------------------------------------------------------------------------------

文章知识点与官方知识档案匹配,可进一步学习相关知识
算法技能树首页概览45066 人正在系统学习中