文章目录
一种最基本的压缩编码方式——哈夫曼编码。为了介绍哈夫曼编码,首先我们要了解哈夫曼树。
🌅一、哈夫曼树的引入
首先,我们考虑一个非常基本的问题,我们想要写一个程序帮助数学老师判断每个同学的成绩在什么区间,可以用以下代码:
if(score<60)
remark="不及格";
else if(score<70)
remark="及格";
else if(score<80)
remark="中等";
else if(score<90)
remark="良好";
else
remark="优秀";
这个分支语句可以看做是如下二叉树:
是不是乍一看还感觉效率还挺高的?
但是根据我们的常识,班里的成绩分布都是符合一定的正态规律的,分数段的大致比例如下表:
分数 | 0~59 | 60~69 | 70~79 | 80~89 | 90~100 |
---|---|---|---|---|---|
所占比例 | 5% | 15% | 40% | 30% | 10% |
观察二叉树发现,如果输入的成绩是不及格,那么需要判断的次数(也就是需要从头结点到达这个节点所经过的分支数)是1,如果输入的成绩是及格,那么需要判断的次数是2,如果输入的成绩是中等,那么需要判断的次数是3,如果输入的成绩是良好,那么需要判断的次数是4,如果输入的成绩是优秀,那么需要判断的次数是4.
那么问题就出现了,占总成绩40%的7079分数段的判断需要经过的次数是3,占总成绩%5的059分数段的判断需要经过的次数却是1,这从效率上讲不合理。
我们给出这由这些节点(不及格、及格、中等、良好、优秀)和他们所占比例构成的哈夫曼树:
可以看到占总成绩40%的7079分数段的判断需要经过的次数是1,占总成绩%5的059分数段的判断需要经过的次数是4,这不就合理多了。
更近一步,我们可以从统计学的角度上考虑这个问题,假如我们输入了10000个成绩,那么如果使用像第一个二叉树那样的判断结构来进行判断,统计意义上来说,需要进行的判断次数等于
10000
∗
(
0.05
∗
1
+
0.15
∗
2
+
0.4
∗
3
+
0.3
∗
4
+
0.1
∗
4
)
=
31500
10000*(0.05*1+0.15*2+0.4*3+0.3*4+0.1*4)=31500
10000∗(0.05∗1+0.15∗2+0.4∗3+0.3∗4+0.1∗4)=31500
使用huffman树形成的判断结构所需要的的判断次数为(一个圆方块视为一次判断)
10000
∗
(
0.4
∗
1
+
0.3
∗
2
+
0.15
∗
3
+
0.10
∗
4
+
0.05
∗
4
)
=
20500
10000*(0.4*1+0.3*2+0.15*3+0.10*4+0.05*4)=20500
10000∗(0.4∗1+0.3∗2+0.15∗3+0.10∗4+0.05∗4)=20500
这效率提升的不是一点半点啊。
由此,我们引出huffmantree。
🛕二、哈夫曼树的定义与原理
🎡1.权
每个叶子结点我们给他一个正整数,这个正整数被称为该结点的权。
🌇2.路径长度
对于一个叶子结点来说,从树的根结点到达此叶子结点经过的分支个数被称为该结点的路径长度。
🕍3.带权路径长度
对于一个带权叶结点来说,该结点的路径长度*权得到的值就是该结点的带权路径长度,整棵树的带权路径长度是每个叶子结点带权路径长度的求和。
🏰4.哈夫曼树的定义
对给定的带权叶子结点组成的二叉树中,带权路径长度最小的树被称为哈夫曼树。
🎢三、创建哈夫曼树
🪂1.Huffmantree的求法
根据哈夫曼树的定义很容易就能引出哈夫曼树的求法:
- 根据给定的n个权值{w1,w2,w3…,wn}的n个结点,构造的n棵树的集合F={T1,T2,T3,…,Tn},其中的每棵二叉树Ti就是一个只有带权wi的结点构成的树。
- 在F中选取两棵根结点权值最小的树作为左右子树构造一棵新的二叉树,并且置新的二叉树的根节点的权值为其左右字树上根节点的权值的和。
- 在F中删除这两棵树,同时将新得到的二叉树加入F中。
- 重复步骤2 3,直到F只含一棵树为止。
我们遵循了每次都选出最权值最小的节点放在下面,把权值大的节点放在上面的原则。
🎇2.Huffmantree的存储结构
我们考虑用一个数组来存储整个huffman树,数组的每个元素如下:
typedef enum {
USED,
NOTUSED
}State;
typedef struct {
char data[WORDMAX];//存单词
int weight;//存这个节点的权重
int leftchild;//存它的左孩子
int rightchild;//存它的右孩子
int parent;//方便打印哈夫曼编码
State state;//表示这个结点是否已经被选过然后删除了
}HFNode;
🛸3.创建Huffmantree
void createHuffmantree(HFNode hf[], int n)
{
int a, b;
for (int i = n; i < 2 * n - 1; i++)
{
min2(hf, i, &a, &b);
//返回当前结点集合中还没被选取过的最小的两个结点的下标,利用指针带回
hf[i].weight = hf[a].weight + hf[b].weight;
hf[a].parent = i;
hf[b].parent = i;
hf[i].leftchild = a;
hf[i].rightchild = b;
}
}
void min2(HFNode hf[], int n, int* p1, int* p2)
{
int arr[MAXSIZE] = { 0 };
for (int i = 0; i < n; i++)
{
arr[i] = hf[i].weight;
}
for (int i = 0; i < n; i++)
{
if (hf[i].state != USED)
{
*p1 = i;
break;
}
}
for (int i = 0; i < n; i++)
{
if (arr[*p1] > arr[i] && hf[i].state!=USED)
{
*p1 = i;
}
}
hf[*p1].state = USED;
arr[*p1] = INT_MAX;//把找过的那个最小元素置成正无穷,这样就可以如法炮制找第二小的元素了。
for (int i = 0; i < n; i++)
{
if (hf[i].state != USED)
{
*p2 = i;
break;
}
}
for (int i = 0; i < n; i++)
{
if (arr[*p2] > arr[i] && hf[i].state!=USED)
{
*p2 = i;
}
}
hf[*p2].state = USED;
}
🚇四、哈夫曼编码
哈夫曼当年研究这种最优树的目的是为了解决当年远距离同心(主要是电报)的数据传输的最优化问题。我们得到huffmantree后,得到huffman编码的过程如下图:
这样得到的编码使得我们要传输的信息在统计意义上编码长度最短。
如果我们接到编码后想要解编码,需要得到原本的那颗哈夫曼树,然后根据每个字母对应的编码表来求解(注意到哈夫曼编码中,任一元素的编码都不是另一个字符的编码的前缀,所以不存在长度不等的编码可能会存在的混淆问题。
🌁五、由哈夫曼树求解哈夫曼编码
我们的求解思路是为每个根结点创建一个字符数组,利用每个结点中的parent指针回溯到其父母结点,然后利用父母结点的leftchild指针和rightchild指针来判断这个结点是父母结点的左孩子还是右孩子,如果是左孩子,则在它的字符数组中填上一个‘1’,如果是右孩子,就填上一个’0’,然后将这个字符数组进行一个逆序,然后加上一个’\0’以便输出。
🕋1.哈夫曼编码的存储结构
typedef struct {
char code[MAXSIZE];
}HFCode;
🏖️2.哈夫曼编码的创建
void createhuffmancode(HFNode hf[], HFCode hfcode[], int n)
{
for (int i = 0; i < n; i++)
{
int p = i;
int k = 0;
while (p!=root)
{
int tmp2 = p;
p = hf[p].parent;
if (tmp2 == hf[p].leftchild)
{
hfcode[i].code[k] = '0';
}
else
{
hfcode[i].code[k] = '1';
}
k++;
}
reverse(hfcode[i].code, k);
hfcode[i].code[k] = '\0';
}
}
🏟️3.哈夫曼编码的展示
void displayhuffmancode(HFNode hf[], HFCode hfcode[], int n)
{
for (int i = 0; i < n; i++)
{
printf("%s的huffman编码是:%s\n", hf[i].data, hfcode[i].code);
}
}
🗻六、代码汇总与测试函数
//Huffmantree.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <LIMITS.H>
#define MAXSIZE 50
//顺序存储的哈夫曼树
#define WORDMAX 20
#define root (2*n-2)
typedef enum {
USED,
NOTUSED
}State;
typedef struct {
char data[WORDMAX];//存单词
int weight;//存这个节点的权重
int leftchild;//存它的左孩子
int rightchild;//存它的右孩子
int parent;//方便打印哈夫曼编码
State state;
}HFNode;
typedef struct {
char code[MAXSIZE];
}HFCode;
void createHuffmantree(HFNode hf[], int n);//利用n个节点创建哈夫曼树
void min2(HFNode hf[], int n, int* p1, int* p2);
void createhuffmancode(HFNode hf[],HFCode hfcode[], int n);//利用创建完的哈夫曼树创建哈夫曼编码
void reverse(char str[], int k);
void displayhuffmancode(HFNode hf[],HFCode hfcode[], int n);
//huffmantree.c
#include "Huffmantree.h"
void reverse(char str[], int k)
{
if (k == 1)
return;
int left = 0;
int right = k - 1;
while (left < right)
{
char tmp = str[left];
str[left] = str[right];
str[right] = tmp;
left++;
right--;
}
}
void min2(HFNode hf[], int n, int* p1, int* p2)
{
int arr[MAXSIZE] = { 0 };
for (int i = 0; i < n; i++)
{
arr[i] = hf[i].weight;
}
for (int i = 0; i < n; i++)
{
if (hf[i].state != USED)
{
*p1 = i;
break;
}
}
for (int i = 0; i < n; i++)
{
if (arr[*p1] > arr[i] && hf[i].state!=USED)
{
*p1 = i;
}
}
hf[*p1].state = USED;
arr[*p1] = INT_MAX;
for (int i = 0; i < n; i++)
{
if (hf[i].state != USED)
{
*p2 = i;
break;
}
}
for (int i = 0; i < n; i++)
{
if (arr[*p2] > arr[i] && hf[i].state!=USED)
{
*p2 = i;
}
}
hf[*p2].state = USED;
}
void createHuffmantree(HFNode hf[], int n)
{
int a, b;
for (int i = n; i < 2 * n - 1; i++)
{
min2(hf, i, &a, &b);
hf[i].weight = hf[a].weight + hf[b].weight;
hf[a].parent = i;
hf[b].parent = i;
hf[i].leftchild = a;
hf[i].rightchild = b;
}
}
void createhuffmancode(HFNode hf[], HFCode hfcode[], int n)
{
for (int i = 0; i < n; i++)
{
int p = i;
int k = 0;
while (p!=root)
{
int tmp2 = p;
p = hf[p].parent;
if (tmp2 == hf[p].leftchild)
{
hfcode[i].code[k] = '0';
}
else
{
hfcode[i].code[k] = '1';
}
k++;
}
reverse(hfcode[i].code, k);
hfcode[i].code[k] = '\0';
}
}
void displayhuffmancode(HFNode hf[], HFCode hfcode[], int n)
{
for (int i = 0; i < n; i++)
{
printf("%s的huffman编码是:%s\n", hf[i].data, hfcode[i].code);
}
}
//test.c
#include "Huffmantree.h"
int main()
{
HFNode hf[MAXSIZE];
HFCode hfcode[MAXSIZE];
printf("请输入此轮huffman编码中的单词个数:");
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
printf("请输入第%d个单词与它出现的频数:", i + 1);
scanf("%s %d", &(hf[i].data), &(hf[i].weight));
}
createHuffmantree(hf, n);
createhuffmancode(hf, hfcode, n);
system("cls");
displayhuffmancode(hf, hfcode, n);
return 0;
}