题目:要传输一则报文内容如下:
“AAAAAAAAAAAAAAABBBBBBBBBCCCCCCCCDDDDDDDDDDDDEEEEEEEEEEFFFFF”
请为这段报文设计哈夫曼编码,要求如下:
- 请计算出每个字符出现的概率,并以概率为权重来构造哈夫曼树,写出构造过程、画出最终的哈夫曼树,得到每个字符的哈夫曼编码。
- 请将上述设计哈夫曼编码的过程,用代码来实现,并输出各个字母的哈夫曼编码。(有代码,有运行结果的截图)
- 请分析算法的效率,至少包括时间复杂度和空间复杂度等。
一、构造过程
① 统计可知,字符串全长共59个字符,其中字符‘A’-‘F’的出现频数及对应概率(保留两位小数)如图所示:
②将每个字符出现概率作为权重,则对于上述给定的6个权值{25,15,14,20,17,9},依次构造6棵只有根节点的二叉树,共同构成森林F;
③在森林F中,选取权重最小和次小的两颗树,分别作为新二叉树的左子树、右子树,且该树根节点权值赋左右子树权值之和;
④从森林F中删除已选中的两棵子树,把新得到的二叉树加入F中;
⑤重复③④,直到森林F中仅留下一棵树时,得到最终的哈夫曼树HF如图所示:
⑥对上述哈夫曼树,将其树中的每个左分支赋0、右分支赋1,则从根结点开始到叶子结点,各分支路径分别得到对应的二进制串,即为给定的每个字符的哈夫曼编码。
二、代码实现
编译环境:Visual Studio 2019 16.6.3,代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define N 6 //指定的编码字符数
#define M (2 * N - 1) //HT结点数
#define MAXWEIGHT 100
typedef struct { //哈夫曼树的存储表示
int weight;
int parent, lchild, rchild;
}HTNode;
typedef HTNode HuffmanTree[M + 1];
typedef char* HuffmanCode[N + 1]; //哈夫曼编码表
void Select(HuffmanTree HT, int n, int& s1, int& s2);
void CreateHuffmanTree(HuffmanTree& HT, int* w, int n);
void CreateHuffmanCode(HuffmanTree HT, HuffmanCode& HC);
void PrintHC(HuffmanCode HC, char ch[]);
int main() {
HuffmanTree HT;
HuffmanCode HC;
int w[N + 1];
char ch[N + 1];
printf("Please input %d characters & its weight(e.g. A 25):\n", N);
for (int i = 1; i <= N; i++) {
scanf("%c %d", &ch[i], &w[i]);
getchar();
}
CreateHuffmanTree(HT, w, N);
CreateHuffmanCode(HT, HC);
PrintHC(HC, ch);
return 0;
}
void Select(HuffmanTree HT, int n, int& s1, int& s2) {
int i, min = 0, temp = MAXWEIGHT;
for (i = 1; i <= n; i++) {
if (!HT[i].parent) {
if (temp > HT[i].weight) {
temp = HT[i].weight;
min = i; //最小值作为新树左子树
}
}
}
s1 = min;
for (i = 1, min = 0, temp = MAXWEIGHT; i <= n; i++) {
if ((!HT[i].parent) && i != s1) {
if (temp > HT[i].weight) {
temp = HT[i].weight;
min = i; //次小值作为新树右子树
}
}
}
s2 = min;
}
void CreateHuffmanTree(HuffmanTree& HT, int* w, int n) { //创建哈夫曼树HT
int i, s1, s2;
if (n <= 1) return; //树为空或仅有根节点
for (i = 1; i <= M; ++i) { //初始化
HT[i].weight = w[i];
HT[i].parent = 0;
HT[i].lchild = 0;
HT[i].rchild = 0;
}
for (i = n + 1; i <= M; ++i) { //创建新二叉树
Select(HT, i - 1, s1, s2);
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 CreateHuffmanCode(HuffmanTree HT, HuffmanCode& HC) {
int i, start, c, f;
char cd[N];
cd[N - 1] = '\0'; //编码结束符
for (i = 1; i <= N; i++) { //对于第i个待编码字符即第i个带权值的叶子节点
start = N - 1; //开始start指向结束符位置
c = i;
f = HT[i].parent; //f指向当前结点的双亲
while (f) { //从叶上溯,到根节点跳出
if (HT[f].lchild == c)
cd[--start] = '0'; //左孩子
else
cd[--start] = '1'; //右孩子
c = f;
f = HT[f].parent;
}
HC[i] = new char[N - start];
strcpy(HC[i], &cd[start]); //复制结果到编码表,用于输出
}
}
void PrintHC(HuffmanCode HC, char ch[]) { //打印哈夫曼编码表
for (int i = 1; i <= N; i++)
printf("\n%c: %s\n", ch[i], HC[i]);
}
输出各个字母的哈夫曼编码:
三、算法分析
1、哈夫曼编码是最优前缀编码:对包括N个字符的数据文件,分别以它们的出现概率构造哈夫曼树,利用该树对应的哈夫曼编码对报文进行编码,得到压缩后的最短二进制编码;
2、算法自底而上地构造出对应最优编码的二叉树HT,它从n个叶子结点开始,识别出最低权重的两个对象,并将其合并;当合并时,新对象的权重设置为原两个对象权值之和,一共执行了|n| - 1次合并操作;
3、是贪心算法:每次选择均为当下的最优选择,通过合并来构造最优树得到对应的哈夫曼编码;
4、复杂度:
①时间复杂度:有n个终端节点,构成的哈夫曼树总共有2n-1个节点,Select函数的时间复杂度为O(n),嵌套for循环,因此建立哈夫曼树的时间复杂度为O(n^2), 创建哈弗曼编码表的时间复杂度为O(n^2);
②空间复杂度:算法中临时占用存储空间大小与初始森林F中树的个数n线性相关,n越大问题规模也越大,其耗费的空间复杂度为O(n)。