Huffman(哈夫曼)编码的C语言实现
本文将给出C语言的Huffman编码的原理,示例及C语言仿真结果,代码。
一、Huffman编码原理及举例
Huffman编码是一种信源编码,其编码目的在于以最高的编码效率利用信道容量。
例如,假定消息有五种字符序列构成,各字符出现的概率是给定的,设为a,b,c,d,e。出现概率为0.12,0.40,0.15,0.08,0.25。下面给出两种编码(映射):
字符 | 符号概率 | 编码方式1 | 编码方式2 |
---|---|---|---|
a | 0.12 | 000 | 000 |
b | 0.40 | 001 | 11 |
c | 0.15 | 010 | 01 |
d | 0.08 | 011 | 001 |
e | 0.25 | 100 | 10 |
编码方式1中任意3位二进制数字串都不是另一个3位二进制数字串的前缀,故其有前缀性毋庸置疑。在译码时,每次取3位二进制数字串,每3位译码为1个字符。
编码方式2其实也具有前缀性。但由于其编码的比特长度不同,难以看出其是否具有前缀性。我们不妨用二叉树来表示其编码(前缀编码均可以用二叉树表示):
由此,可以将前缀编码看作二叉树中的路径。每个结点的左分支附0,右分支附1。将字符作为叶结点的标号。从根结点到叶结点的路径上遇到的0或1构成的序列就是对应叶结点字符的编码。
对于编码方式1,所有字符编码长度为3,则其平均编码长度为3。但编码方式2的平均编码长度为2.2。显然编码方式2的效率更高。
采用Huffman算法可以得到最优前缀编码。
首先,从给定的字符集里选取两个出现概率最小的两个字符,以之前的例子为字符a,d。构造一个父结点,符号设为x,其对应概率为a,d符号概率之和,他的子结点分别为a,d。然后对其余结点和新结点组成的字符集和概率集按同样方式递归得到前缀编码二叉树。遍历二叉树即可得到最优前缀编码。
其构造的顺序如下:
通过以上方式,可以得到最优的编码方式:
字符 | 符号概率 | Huffman编码 |
---|---|---|
a | 0.12 | 1111 |
b | 0.40 | 0 |
c | 0.15 | 110 |
d | 0.08 | 1110 |
e | 0.25 | 10 |
经过计算可知,其平均编码长度为2.15。
二、Huffman编码的C语言实现
符号分别为A~P,共16个符号,其出现概率如下:
符号 | 概率 |
---|---|
A | 0.06 |
B | 0.12 |
C | 0.15 |
D | 0.05 |
E | 0.06 |
F | 0.02 |
G | 0.07 |
H | 0.03 |
I | 0.13 |
J | 0.09 |
K | 0.07 |
L | 0.06 |
M | 0.02 |
N | 0.02 |
O | 0.01 |
P | 0.04 |
1、初始化
首先输入初始信息并设置结点树,这里不使用指针变量,而是以索引为地址。
struct Huffman
{
double weight;
int lchild;
int rchild;
int parent;
}; //Huffman tree结构体定义
结构体表示一个二叉树结点,设置的结点数量应该为node_num个,直接定义为H。
首先对二叉树初始化。对二叉树的大小(结点数量)应为符号数的2倍减1。为修改方便,对结点数,符号数,概率空间作宏定义:
#define symbol_num 16
#define node_num 2 * (symbol_num) - 1
double symbol_P[symbol_num] = {
0.06, 0.12, 0.15, 0.05, 0.06, 0.02, \
0.07, 0.03, 0.13, 0.09, 0.07, 0.06, 0.02, 0.02, 0.01, 0.04};
初始化前symbol_node(16)个结点的权值weight为对应的概率,为后续排序方便,设置其余结点权值为1。其余子结点,父结点编号参照数据结构设置为 -1。为后续提取符号序号,构造list数组存储排序后的序号。为避免破坏原概率空间,可以设置新的初始概率空间以供更改。
for (int i = 0; i < node_num; i++)
{
if (i < symbol_num)
symbol_Ptemp[i] = symbol_P[i];
else
symbol_Ptemp[i] = 1;
}
for (int i = 0; i < node_num; i++)
{
list[i] = i;
}
for (int i = 0; i < node_num; i++)
{
H[i].parent = -1;
H[i].lchild = -1;
H[i].rchild = -1;
if (i < symbol_num)
{
H[i].weight = symbol_P[i];
code_len[i] = 0; //为输出方便而设置,后续会解释
}
else
H[i].weight = 0;
}
2、冒泡排序
每次都需要重新排序并取出最小的两个权值weight,返回值为最小值的编号。而且,需要保存好排序后的序列,此时便体现出设置list数组的原因了。这里的排序为冒泡排序法从小到大,这里不再赘述。
int i, j, min0, min1;
double temp;
int temp1;
for (i = 0; i < node_num - 1; i++)
{
for (j = 0; j < node_num - 1 - i; j++)
{
if (symbol_Ptemp[j] > symbol_Ptemp[j + 1])
{
temp = symbol_Ptemp[j];
symbol_Ptemp[j] = symbol_Ptemp[j + 1];
symbol_Ptemp[j + 1] = temp;
temp1 = list[j];
list[j] = list[j + 1];
list[j + 1] = temp1;
}
}
}
min0 = list[0];
min1 = list[1];
3、构造结点
这里的逻辑很简单,已经拿到了最小的两个结点序号,则对其进行构造父结点即可。考虑到每次完成一步父结点构造,这一父结点的两个子节点便不能再参与比较,不妨在提取后对两个子结点的权值进行赋1,使其不影响升序冒泡。
H[i].weight = H[min0].weight + H[min1].weight;
symbol_Ptemp[i] = H[i].weight;
H[i].lchild = min0;
H[i].rchild = min1;
H[min0].parent = i;
H[min1].parent = i;
symbol_Ptemp[0] = 1.0;
symbol_Ptemp[1] = 1.0;
Bubble();
对以上函数从第一个父结点递归至根节点即可完成。
三、仿真结果
注意,这一实验的编码方式并非唯一,这与树的形状有关。但是得到的编码效率必定相同且最小。
四、仿真代码
输出函数较为复杂且对仿真没有过多用处,故不在解释。
// Huffman Coding
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#define symbol_num 16
#define node_num 2 * (symbol_num) - 1
double symbol_P[symbol_num] = {
0.06, 0.12, 0.15, 0.05, 0.06, 0.02, \
0.07, 0.03, 0.13, 0.09, 0.07, 0.06, 0.02, 0.02, 0.01, 0.04}; //概率空间
double symbol_Ptemp[node_num];//便于比较大小,对于新节点叠加在后
int list[node_num];
int min1, min0;
char code[symbol_num];
int code_len[symbol_num];
struct Huffman
{
double weight;
int lchild;
int rchild;
int parent;
}; //Huffman tree结构体定义
void Initsymbol_P(); //概率空间初始化
void InitHT(Huffman H[node_num]); //节点初始化
void Bubble(); //冒泡排序
void Nodeproduce(); //哈夫曼树构建
void OutputTree();
Huffman H[node_num];
int main()
{
Initsymbol_P();
Bubble();
InitHT(H);
Nodeproduce();
OutputTree();
return 0;
}
void Initsymbol_P()
{
for (int i = 0; i < node_num; i++)
{
if (i < symbol_num)
symbol_Ptemp[i] = symbol_P[i];
else
symbol_Ptemp[i] = 1;
}
for (int i = 0; i < node_num; i++)
{
list[i] = i;
}
}
void InitHT(Huffman H[node_num]) //初始化哈夫曼树
{
for (int i = 0; i < node_num; i++)
{
H[i].parent = -1;
H[i].lchild = -1;
H[i].rchild = -1;
if (i < symbol_num)
{
H[i].weight = symbol_P[i];
code_len[i] = 0;
}
else
H[i].weight = 0;
}
}
void Bubble()
{
int i, j;
double temp;
int temp1;
for (i = 0; i < node_num - 1; i++)
{
for (j = 0; j < node_num - 1 - i; j++)
{
if (symbol_Ptemp[j] > symbol_Ptemp[j + 1])
{
temp = symbol_Ptemp[j];
symbol_Ptemp[j] = symbol_Ptemp[j + 1];
symbol_Ptemp[j + 1] = temp;
temp1 = list[j];
list[j] = list[j + 1];
list[j + 1] = temp1;
}
}
}
min0 = list[0];
min1 = list[1];
}
void Nodeproduce()
{
for (int i = symbol_num; i < node_num; i++)
{
H[i].weight = H[min0].weight + H[min1].weight;
symbol_Ptemp[i] = H[i].weight;
H[i].lchild = min0;
H[i].rchild = min1;
H[min0].parent = i;
H[min1].parent = i;
symbol_Ptemp[0] = 1.0;
symbol_Ptemp[1] = 1.0;
Bubble();
}
}
void OutputTree()
{
for (int i = 0; i < node_num; i++)
{
printf("%d, %f, %d, %d, %d\n",i, H[i].weight, H[i].lchild, H[i].rchild, H[i].parent);
}
for (int i = 0; i < symbol_num; i++)
{
int temp_i = i;
int temp_iup = H[temp_i].parent;
while (temp_iup != -1)
{
code_len[i]++;
temp_i = temp_iup;
temp_iup = H[temp_i].parent;
}
code[code_len[i]] = '\0';
temp_i = i;
temp_iup = H[temp_i].parent;
int j = 1;
while (temp_iup != -1)
{
if (H[temp_iup].lchild == temp_i)
{
code[code_len[i] - j] = 48;
}
if (H[temp_iup].rchild == temp_i)
{
code[code_len[i] - j] = 49;
}
temp_i = temp_iup;
temp_iup = H[temp_i].parent;
j++;
}
printf("'%c': Pro = %f Huffman Code length: %d ", i + 65, H[i].weight, code_len[i]);
printf("Huffman Code -> %s \n", code);
}
}
五、参考资料
- 哈夫曼(huffman)树和哈夫曼编码
- 谭浩强: C程序设计(第四版)
- 张岩, 李秀坤, 刘显敏: 数据结构与算法(第5版)