数据结构与算法——第6章-树-哈夫曼编码(6.17)

一 概述

1
2
3
4
5
1.什么是哈夫曼编码
2.哈夫曼编码图示
3.哈夫曼编码的两种方式
4.哈夫曼编码的两种代码实现
5.完整代码

二 什么是哈夫曼编码

1
2
3
4
5
6
哈夫曼编码就是在哈夫曼树的基础上构建的,这种编码方式最大的优点就是用最少的字符包含最多的信息内容,
进而实现信息的压缩存储。

根据发送信息的内容,通过统计文本中相同字符的个数作为每个字符的权值,建立哈夫曼树。
对于树中的每一个子树,统一规定其左孩子标记为 0 ,右孩子标记为 1 。
这样,用到哪个字符时,从哈夫曼树的根结点开始,依次写出经过结点的标记,最终得到的就是该结点的哈夫曼编码。

三 哈夫曼编码图示

3.1 图示

文本中字符出现的次数越多,在哈夫曼树中的体现就是越接近树根。编码的长度越短。

3.2 说明

1
2
3
4
5
6
7
8
举个例子,如图 1 所示,这是用权值分别为 7、5、2、4 的字符 a、b、c、d 构建的哈夫曼树。
显然,字符 a 用到的次数最多,所以它对应的哈弗曼编码应最短,这里用 0 表示;
其次,是字符 b 用的多,因此字符 b 编码为 10 ,

以此类推,字符 c 的编码为 110 ,字符 d 的编码为 111。

权值越大,表示此字符在文件中出现的次数越多,那么,为了实现用最少的字符包含最多的内容,
就应该给出现次数越多的字符,分配的哈弗曼编码越短。

四 哈夫曼编码的两种方式

1
2
3
4
5
6
7
使用程序求哈夫曼编码有两种方法:
1.从叶子结点一直找到根结点,逆向记录途中经过的标记。
例如,图 1 中字符 c 的哈夫曼编码从结点 c 开始一直找到根结点,结果为:0 1 1 ,
所以字符 c 的哈夫曼编码为:1 1 0(逆序输出)。

2.从根结点出发,一直到叶子结点,记录途中经过的标记。
例如,求图 1 中字符 c 的哈夫曼编码,就从根结点开始,依次为:1 1 0。

五 哈夫曼编码的两种代码实现

5.1 采用方法 1 的实现代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//HT为哈夫曼树,HC为存储结点哈夫曼编码的二维动态数组,n为结点的个数
void HuffmanCoding(HuffmanTree HT, HuffmanCode* HC, int n) {
*HC = (HuffmanCode)malloc((n + 1) * sizeof(char*));
memset(*HC, NULL, n + 1);
char* cd = (char*)malloc(n * sizeof(char)); //存放结点哈夫曼编码的字符串数组
memset(cd, 0, n);
for (int i = 1; i <= n; i++) {
//从叶子结点出发,得到的哈夫曼编码是逆序的,需要在字符串数组中逆序存放
int start = n - 1;
//当前结点在数组中的位置
int c = i;
//当前结点的父结点在数组中的位置
int j = HT[i].parent;
// 一直寻找到根结点
while (j != 0) {
// 如果该结点是父结点的左孩子则对应路径编码为0,否则为右孩子编码为1
if (HT[j].left == c)
cd[--start] = '0';
else
cd[--start] = '1';
//以父结点为孩子结点,继续朝树根的方向遍历
c = j;
j = HT[j].parent;
}
//跳出循环后,cd数组中从下标 start 开始,存放的就是该结点的哈夫曼编码
(*HC)[i] = (char*)malloc((n - start) * sizeof(char));
strcpy((*HC)[i], &cd[start]);
}
//使用malloc申请的cd动态数组需要手动释放
free(cd);
}

5.2 采用第二种算法的实现代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//HT为哈夫曼树,HC为存储结点哈夫曼编码的二维动态数组,n为结点的个数
void HuffmanCoding(HuffmanTree HT, HuffmanCode* HC, int n) {
int i, m, p, cdlen;
char* cd = NULL;
*HC = (HuffmanCode)malloc((n + 1) * sizeof(char*));
memset(*HC, NULL, n + 1);
m = 2 * n - 1;
p = m;
cdlen = 0;
cd = (char*)malloc(n * sizeof(char));
memset(cd, 0, n);
//将各个结点的权重用于记录访问结点的次数,首先初始化为0
for (i = 1; i <= m; i++) {
HT[i].weight = 0;
}
//一开始 p 初始化为 m,也就是从树根开始。一直到p为0
while (p) {
//如果当前结点一次没有访问,进入这个if语句
if (HT[p].weight == 0) {
HT[p].weight = 1;//重置访问次数为1
//如果有左孩子,则访问左孩子,并且存储走过的标记为0
if (HT[p].left != 0) {
p = HT[p].left;
cd[cdlen++] = '0';
}
//当前结点没有左孩子,也没有右孩子,说明为叶子结点,直接记录哈夫曼编码
else if (HT[p].right == 0) {
(*HC)[p] = (char*)malloc((cdlen + 1) * sizeof(char));
cd[cdlen] = '\0';
strcpy((*HC)[p], cd);
}
}
//如果weight为1,说明访问过一次,即是从其左孩子返回的
else if (HT[p].weight == 1) {
HT[p].weight = 2;//设置访问次数为2
//如果有右孩子,遍历右孩子,记录标记值 1
if (HT[p].right != 0) {
p = HT[p].right;
cd[cdlen++] = '1';
}
}
//如果访问次数为 2,说明左右孩子都遍历完了,返回父结点
else {
HT[p].weight = 0;
p = HT[p].parent;
--cdlen;
}
}
free(cd);
}

六 完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
//哈夫曼树结点结构
typedef struct {
int weight;//结点权重
int parent, left, right;//父结点、左孩子、右孩子在数组中的位置下标
}HTNode, * HuffmanTree;

//动态二维数组,存储哈夫曼编码
typedef char** HuffmanCode;

//HT数组中存放的哈夫曼树,end表示HT数组中存放结点的最终位置,s1和s2传递的是HT数组中权重值最小的两个结点在数组中的位置
void Select(HuffmanTree HT, int end, int* s1, int* s2)
{
int min1, min2;
int i = 1, j;
//找到还没构建树的结点
while (HT[i].parent != 0 && i <= end) {
i++;
}
min1 = HT[i].weight;
*s1 = i;

i++;
while (HT[i].parent != 0 && i <= end) {
i++;
}
//对找到的两个结点比较大小,min2为大的,min1为小的
if (HT[i].weight < min1) {
min2 = min1;
*s2 = *s1;
min1 = HT[i].weight;
*s1 = i;
}
else {
min2 = HT[i].weight;
*s2 = i;
}
//两个结点和后续的所有未构建成树的结点做比较
for (j = i + 1; j <= end; j++)
{
//如果有父结点,直接跳过,进行下一个
if (HT[j].parent != 0) {
continue;
}
//如果比最小的还小,将min2=min1,min1赋值新的结点的下标
if (HT[j].weight < min1) {
min2 = min1;
min1 = HT[j].weight;
*s2 = *s1;
*s1 = j;
}
//如果介于两者之间,min2赋值为新的结点的位置下标
else if (HT[j].weight >= min1 && HT[j].weight < min2) {
min2 = HT[j].weight;
*s2 = j;
}
}
}

//HT为地址传递的存储哈夫曼树的数组,w为存储结点权重值的数组,n为结点个数
void CreateHuffmanTree(HuffmanTree* HT, int* w, int n)
{
int m, i;
if (n <= 1) return; // 如果只有一个编码就相当于0
m = 2 * n - 1; // 哈夫曼树总节点数,n就是叶子结点
*HT = (HuffmanTree)malloc((m + 1) * sizeof(HTNode)); // 0号位置不用
HuffmanTree p = *HT;
// 初始化哈夫曼树中的所有结点
for (i = 1; i <= n; i++)
{
(p + i)->weight = *(w + i - 1);
(p + i)->parent = 0;
(p + i)->left = 0;
(p + i)->right = 0;
}
//从树组的下标 n+1 开始初始化哈夫曼树中除叶子结点外的结点
for (i = n + 1; i <= m; i++)
{
(p + i)->weight = 0;
(p + i)->parent = 0;
(p + i)->left = 0;
(p + i)->right = 0;
}
//构建哈夫曼树
for (i = n + 1; i <= m; i++)
{
int s1, s2;
Select(*HT, i - 1, &s1, &s2);
(*HT)[s1].parent = (*HT)[s2].parent = i;
(*HT)[i].left = s1;
(*HT)[i].right = s2;
(*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight;
}
}
//HT为哈夫曼树,HC为存储结点哈夫曼编码的二维动态数组,n为结点的个数
void HuffmanCoding(HuffmanTree HT, HuffmanCode* HC, int n) {
int i, m, p, cdlen;
char* cd = NULL;
*HC = (HuffmanCode)malloc((n + 1) * sizeof(char*));
memset(*HC, NULL, n + 1);
m = 2 * n - 1;
p = m;
cdlen = 0;
cd = (char*)malloc(n * sizeof(char));
memset(cd, 0, n);
//将各个结点的权重用于记录访问结点的次数,首先初始化为0
for (i = 1; i <= m; i++) {
HT[i].weight = 0;
}
//一开始 p 初始化为 m,也就是从树根开始。一直到p为0
while (p) {
//如果当前结点一次没有访问,进入这个if语句
if (HT[p].weight == 0) {
HT[p].weight = 1;//重置访问次数为1
//如果有左孩子,则访问左孩子,并且存储走过的标记为0
if (HT[p].left != 0) {
p = HT[p].left;
cd[cdlen++] = '0';
}
//当前结点没有左孩子,也没有右孩子,说明为叶子结点,直接记录哈夫曼编码
else if (HT[p].right == 0) {
(*HC)[p] = (char*)malloc((cdlen + 1) * sizeof(char));
cd[cdlen] = '\0';
strcpy((*HC)[p], cd);
}
}
//如果weight为1,说明访问过一次,即是从其左孩子返回的
else if (HT[p].weight == 1) {
HT[p].weight = 2;//设置访问次数为2
//如果有右孩子,遍历右孩子,记录标记值 1
if (HT[p].right != 0) {
p = HT[p].right;
cd[cdlen++] = '1';
}
}
//如果访问次数为 2,说明左右孩子都遍历完了,返回父结点
else {
HT[p].weight = 0;
p = HT[p].parent;
--cdlen;
}
}
free(cd);
}
//打印哈夫曼编码的函数
void PrintHuffmanCode(HuffmanCode htable, int* w, int n)
{
printf("Huffman code : \n");
for (int i = 1; i <= n; i++) {
printf("%d code = %s\n", w[i - 1], htable[i]);
}

}

void DelHuffmanCode(HuffmanCode htable, int n) {
int i;
for (i = 0; i < n + 1; i++) {
if (htable[i]) {
free(htable[i]);
}
}
free(htable);
}

int main(void)
{
int w[5] = { 2, 8, 7, 6, 5 };
int n = 5;
HuffmanTree htree;
HuffmanCode htable;
CreateHuffmanTree(&htree, w, n);
HuffmanCoding(htree, &htable, n);
PrintHuffmanCode(htable, w, n);
//释放申请的堆内存
free(htree);
DelHuffmanCode(htable, n);
system("pause");
return 0;
}

运行结果

1
2
3
4
5
6
Huffman code :
2 code = 100
8 code = 11
7 code = 01
6 code = 00
5 code = 101

本节中介绍了两种遍历哈夫曼树获得哈夫曼编码的方法,同时也给出了各自完整的实现代码的函数,在完整代码中
使用的是第二种正序遍历哈夫曼树的方法

七 参考

  • C语言中文网—哈夫曼编码