思路来源:https://www.bilibili.com/video/BV18t411U7Tb?from=search&seid=13776480377358559786
https://www.bilibili.com/video/BV18t411U7eD?from=search&seid=13776480377358559786
代码借鉴:https://blog.csdn.net/feynman1999/article/details/71178357
一,哈夫曼树的构建
要实现哈夫曼树首先有个问题摆在眼前,那就是用什么数据结构 ?
通常我们为了方便都是用数组储存的,那么此时又有两个新的问题出现了
权值放在哪? 父子关系如何表示 ?
第一个问题我们可以很轻松的用结构体数组实现,
第二个问题有些人会想到用完全二叉树那种利用数组之间的位置关系实现,
但问题是你一开始所给的树叶的位置无法确定,就不知道放哪了,
要是先随便放,那你必须等到找到根节点 。这时候菜都凉了。
既然第一个问题想用结构体数组处理,还不如在结构体添加父节点子节点的位置。如此便可。
typedef struct { double w; int p, lc, rc; }HTNode, *HuffmanTree; HTNode HT[100];
例题: 用8个数,构造最优二叉树。
7 19 2 6 32 3 21 10
左边是我画的结果,右边是将最优二叉树存入结构体数组的结果,
其中,前八点为树叶,后六点为内点,最后一点为树根。
1,构造森林全是根
这一步就是把这 n 个点放入结构体数组中:
有 n 个点,每一个点用一次,共产生 n-1 个点,所以用到的数组长度为 2n-1
在实现的时候不用下标为 0 的位置,比较方便。
int m = 2 * n - 1; for (int i = 1; i <= m; i++) HT[i].lc = 0, HT[i].rc = 0, HT[i].p = 0; // 初始化为 0 printf("请输入树叶的权值: "); for (int i = 1; i <= n; i++) scanf("%lf", &HT[i].w); // 赋值
2,选择两小造新树
就是在剩下没用过的点找到最小的两个数,即在那些没有父亲的结点中到最小的两个数
这些结点包括树叶也包括新出现的未来的内点。
int min(int k) { int min; //用来存放weight最小且parent为0的元素的下标 double min_t; //用来存放weight最小且parent为0的元素的weight值的暂时值 int i = 1; //从 1 开始,0 放弃掉 while (HT[i].p != 0) // 看一下第一个父亲健在的位置 i++; min_t = HT[i].w; //将第一个 parent==0 的元素的 w 值赋给 min_t min = i; //选出 w 最小且 p==0 的元素,并将其序号赋给 min for (; i <= k; i++) { if (HT[i].w < min_t&&HT[i].p == 0) { min_t = HT[i].w; min = i; } } //注意,选出weight最小的元素后,将其parent置1,使得下一次求第二小时将其排除在外 HT[min].p = 1; return min;//返回下标 } void select(int k, int *min1, int *min2)//&为引用 { *min1 = min(k); *min2 = min(k); } for (int i = n + 1; i <= m; i++) // 产生 n-1 个结点 { select(i - 1, &s1, &s2); // 选择两小 }
3,删除两小添新人
删除两小就是: 给找到的结点 认好父亲,这样就不会在下次中被找到
添新人就是:给最小的没用过的结点 与 找到的结点 建立联系
HT[s1].p = i; HT[s2].p = i; HT[i].lc = s1; HT[i].rc = s2; HT[i].w = HT[s1].w + HT[s2].w;
4,重复 2,3操作
可以发现 我们从 n-1 个结点循环,
这样要找的结点正好是 循环变量 i 的前面的结点
so:
int s1, s2; for (int i = n + 1; i <= m; i++) // 产生 n-1 个结点 { select(i - 1, &s1, &s2); // 选择两小 HT[s1].p = i; HT[s2].p = i; HT[i].lc = s1; HT[i].rc = s2; HT[i].w = HT[s1].w + HT[s2].w; }
完整代码
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> typedef struct { int w; int p, lc, rc; }HTNode, *HuffmanTree; HTNode HT[100]; int min(int k) { int min; //用来存放weight最小且parent为0的元素的下标 int min_t; //用来存放weight最小且parent为0的元素的weight值的暂时值 //注意应先将第一个 parent==0 的元素的 w 值赋给 min_t ,留作以后比较用 //不要直接将HT[0].wt赋给 min_t 了(这是个常用的技巧) int i = 1; //从 1 开始,0 放弃掉 while (HT[i].p != 0) // 看一下第一个父亲健在的位置 i++; min_t = HT[i].w; min = i; //选出 w 最小且 p==0 的元素,并将其序号赋给 min for (; i <= k; ++i) { if (HT[i].w < min_t&&HT[i].p == 0) { min_t = HT[i].w; min = i; } } //注意,选出weight最小的元素后,将其parent置1,使得下一次求第二小时将其排除在外 HT[min].p = 1; return min;//返回下标 } void select(int k, int *min1, int *min2)//&为引用 { *min1 = min(k); *min2 = min(k); } void creatHT(int n) { if (n <= 1) // 只有一个人没有父亲时结束 return; int m = 2 * n-1; for (int i = 1; i <= m; i++) HT[i].lc = 0, HT[i].rc = 0, HT[i].p = 0; // 初始化为 0 printf("请输入树叶的权值: "); for (int i = 1; i <= n; i++) scanf("%d", &HT[i].w); // 赋值 // 从 p=0 的结点中 选择两小造新树 int s1, s2; for (int i = n + 1; i <= m; i++) // 产生 n-1 个结点 { select(i - 1, &s1, &s2); // 选择两小 HT[s1].p = i; HT[s2].p = i; HT[i].lc = s1; HT[i].rc = s2; HT[i].w = HT[s1].w + HT[s2].w; } } void show(int n) { for (int i = 1; i <= 2 * n - 1; i++) { printf("权值为 %2d 的点的 父节点为 %2d ;子节点为 %2d %2d ", HT[i].w, HT[i].p, HT[i].lc, HT[i].rc); } printf("(其中结点为 0 代表没有这个结点.) "); } int main(void) { printf("请输入树叶的个数:"); int n; scanf("%d", &n); creatHT(n); show(n); system("pause"); return 0; } /* 测试数据 1: 6 5 8 4 11 9 13 测试数据 2: 8 7 19 2 6 32 3 21 10 */
二,哈夫曼树编码
这个思路挺简单的:
首先是可以确定树高最高为为 n-1,自己画一下就知道了
这样就可以用 n 给数组赋长了,这里用动态二维数组,比较专业,也可以不用动态,数组开大点就好了
然后从树叶回溯到根,每一个结点判断一下是 左节点 还是 右节点 就好了
因为由树高可知编码最长为 n-1,这样动用 下标为 0 的位置,可知 n-1 位为 ' ' ,
然后往前赋值就可以了。
最后真正编码长度可以用回溯时的循环次数确定
完整代码:
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> typedef struct { double w; int p, lc, rc; }HTNode, *HuffmanTree; HTNode HT[100]; int min(int k) { int min; //用来存放weight最小且parent为0的元素的下标 double min_t; //用来存放weight最小且parent为0的元素的weight值的暂时值 int i = 1; //从 1 开始,0 放弃掉 while (HT[i].p != 0) // 看一下第一个父亲健在的位置 i++; min_t = HT[i].w; //将第一个 parent==0 的元素的 w 值赋给 min_t min = i; //选出 w 最小且 p==0 的元素,并将其序号赋给 min for (; i <= k; i++) { if (HT[i].w < min_t&&HT[i].p == 0) { min_t = HT[i].w; min = i; } } //注意,选出weight最小的元素后,将其parent置1,使得下一次求第二小时将其排除在外 HT[min].p = 1; return min;//返回下标 } void select(int k, int *min1, int *min2)//&为引用 { *min1 = min(k); *min2 = min(k); } void creatHT(int n) { if (n <= 1) // 只有一个人没有父亲时结束 return; int m = 2 * n - 1; for (int i = 1; i <= m; i++) HT[i].lc = 0, HT[i].rc = 0, HT[i].p = 0; // 初始化为 0 printf("请输入树叶的权值: "); for (int i = 1; i <= n; i++) scanf("%lf", &HT[i].w); // 赋值 // 从 p=0 的结点中 选择两小造新树 int s1, s2; for (int i = n + 1; i <= m; i++) // 产生 n-1 个结点 { select(i - 1, &s1, &s2); // 选择两小 HT[s1].p = i; HT[s2].p = i; HT[i].lc = s1; HT[i].rc = s2; HT[i].w = HT[s1].w + HT[s2].w; } } void show(int n) { for (int i = 1; i <= 2 * n - 1; i++) { printf("权值为 %.1lf 的点的 父节点为 %2d ;子节点为 %2d %2d ", HT[i].w, HT[i].p, HT[i].lc, HT[i].rc); } printf("(其中结点为 0 代表没有这个结点.) "); } void HuffmanCoding(char** HC, int n) { //二维动态数组 用来保存指向每个霍夫曼编码串的指针(指针的指针) HC = (char**)malloc(n * sizeof(char *)); if (!HC) { printf("HuffmanCode malloc failed! "); exit(-1); } //临时空间,用来保存每次求得的一个赫夫曼编码串 char *code = (char *)malloc(n * sizeof(char)); if (!code) { printf("code malloc failed! "); exit(-1); } code[n - 1] = '