1 、原理
Huffman编码是一种信源编码,而信源编码的含义是:以提高通信有效性为目的的编码。通常通过压缩信源的冗余度来实现。采用的一般方法是压缩每个信源符号的平均比特数或信源的码率。即同样多的信息用较少的码率传送,使单位时间内传送的平均信息量增加,从而提高通信的有效性。
Huffman编码是要实现前缀编码(任意一个码字都不是其他码字的前缀部分),这种码在信息论中也称为即时码(相应的也有非即时码,即时码和非即时码统称为唯一可译码)。判断唯一可译码是否存在是要使用克拉夫特(Kraft)不等式,
Huffman编码属于离散信源编码(无失真信源编码)中变长编码方式的一种,其余常见的编码方式还有香农码和费诺码(最后会简单介绍。
1.1 霍夫曼编码
在霍夫曼编码过程中,利用了构成信息序列的符号出现的概率统计特性,对出现概率较大的符号赋予短码,出现概率较小的符号赋予长码,使平均码长最短,是一种编码效率很高的变长编码方法。
霍夫曼编码方法的具体步骤如下。
① 将n个信源消息符号按概率大小依次进行排列。
② 对两个概率最小的符号分别赋予0和1两个码元,然后将这两个概率相加作为一个新的符号的概率,与其他符号重新按概率大小排列。
③ 对重排后两个概率最小的符号重复步骤(2)的过程。
④ 不断继续上述过程,直到最后两个符号分别赋予0和1为止。
⑤ 从最后一级开始,向前返回,就得到各个信源符号所对应的码元序列。
由于霍夫曼编码方法给出的码字不是唯一的。因为每次赋予两个概率最小的符号0和1时,也可以赋予1和0,这样编码后的各符号的码字将会不同,但码字的长度是一样的,平均脉冲也是一样的。另外,在将两个概率最小的符号合并后的新符号的概率与其他信源符号的概率相等时,重新排列时新符号的位置次序可以放在相等概率符号的上面,也可以放在下面。不同的放法就会得到不同的霍夫曼码字,相应的码字的质量是不一样的。通常将合并后的符号放在上面,以减小符号合并的次数,得到码字的方差较小的霍夫曼码。
为什么huffman编码提高了效率?
我们通过计算上述信息的熵和平均码长,然后比较定长编码和huffma编码的编码效率解释。
1.1 霍夫曼树(最优二叉树)
刚才的编码方式是信息论中的方法,我们在数据结构中的实现的结果主要是以二叉树的方式体现。下面是实现二叉树的方式。
二叉树的带权路径长度:设二叉树具有n个带权值的叶子节点,从根节点到各个叶子结点的路径长度与相应叶子节点权值的乘积只和。记为
W
P
L
=
∑
k
=
1
n
w
k
l
k
WPL=\sum_{k=1}^{n}{w_kl_k}
WPL=∑k=1nwklk(第k个节点)
huffman树具有以下特性:
1、每个初始节点最终都是叶子节点,而且权值越小的节点到根结点的路径长度越大。
2、构造过程中新创建的了N-1个节点,因此总共的节点数为2*N-1个。
3、每次构造都是选择两个树作为新节点的左右分支,所以huffman树不存在度为1的节点。
下面说明huffman的代码实现的思想
首席要进行初始化链式,即定义如下类型的结构体。
typedef struct {
int weight;
int parent,lchild,rchild;
}HTNode,*HuffmanTree;
构造huffman树,初始化节点的parent,lchild,rchld,weight.
根据huffman树构造huffman编码表huffcode(这里要借助于一个临时数据tmp)从叶子节点网上回溯到根节点
2、代码(对26的英文字母编码)
#if 1
#include<iostream>
#include<stdio.h>
#include<string.h>
using namespace std;
int weight[] = { 817,149,278,425,1270,223,202,609,15,77,403,241,675,751,193,10,599,633,906,278,46,489,651,23,654,79};
const int LENGTH = 26;
typedef struct {
int weight;
int parent,lchild,rchild;
}HTNode,*HuffmanTree;
//2、select找出两个权值最小的字符
void Select(HuffmanTree HT, int cur, int &min1, int &min2) {
min1 = min2 = 0;
for (int i = 1; i < cur; ++i)
{
//略过已加入节点
if (HT[i].parent != 0)
{
continue;
}
if(min1==0)
min1 = min2 = i;
else {
if ((HT[i].weight <= HT[min1].weight)) {
min2 = min1;
min1 = i;
}
else if (HT[i].weight < HT[min2].weight) {
min2 = i;
}
else if (HT[i].weight > HT[min2].weight) {
if (min1 == min2)
min2 = i;
}
}
}
}
//创建HuffmanTree
void CreateHuffmanTree(HuffmanTree &HT, int weight[], int n) {
if (n < 1) {
return;
}
int m = 2 * n - 1;
HT = new HTNode[m + 1];
for (int i = 1; i <= m; i++) //初始化节点
{
HT[i].parent = 0;
HT[i].lchild = 0;
HT[i].rchild = 0;
}
for (int i = 1; i <= n; i++)//初始化前n个节点的权值
{
HT[i].weight = weight[i-1];
}
for (int i = n + 1; i <= m; i++) {
int min1, min2;
Select(HT, i, min1, min2);//找到parent=0的最小的两个节点
HT[i].weight = HT[min1].weight + HT[min2].weight;
HT[i].lchild = min1;
HT[i].rchild = min2;
HT[min1].parent = HT[min2].parent = i;
}
}
//HuffmanCodeTable
typedef char** HuffmanCodeTable;
void CreateHuffmanCode(HuffmanTree HT, HuffmanCodeTable &HC, int n) {
//0号不用
HC = new char*[n+1];//Huffman编码表数组
char *tmp = new char[n];
tmp[n - 1] = '\0';
for (int i = 1; i <= n; i++) {//对每个叶子节点编码
int start = n - 1;
int pos = i;
int parent = HT[i].parent;
while (parent != 0) {
start--;
if (HT[parent].lchild == pos) {
tmp[start] = '0';
}
else
{
tmp[start] = '1';
}
pos = parent;
parent = HT[parent].parent;
}
HC[i] = new char[n - start];
strcpy(HC[i], &tmp[start]);
}
delete[] tmp;
}
int main()
{
HuffmanTree HT;
CreateHuffmanTree(HT, weight, LENGTH);
for (int i = 1; i < 2 * LENGTH - 1; i++)
{
HTNode node = HT[i];
cout << i + 1 << "\t|" << node.weight << "\t|"
<< node.parent << "\t|" << node.lchild << "\t|" << node.rchild << "\t|" << endl;
}
//验证编码表
HuffmanCodeTable hct;
CreateHuffmanCode(HT, hct, LENGTH);
for (int i = 1; i <= LENGTH; i++)
{
cout << i << ": " << weight[i - 1] << "--> " << hct[i] << endl;
}
//编码
//输入单词
char text[100];
cout << "=============input word=================" << endl;
cout << "================编码====================" << endl;
gets_s(text);
int text_len = strlen(text);
char *encoded_text = new char[text_len*LENGTH];
int start = 0; //每个字母编码的起始位置
for (int i = 0; i <text_len; i++) {
char c = text[i];
int index = c - 'a' + 1;
char *huffman_code = hct[index];
strcpy(encoded_text + start, huffman_code);
start += strlen(huffman_code);
}
cout << "编码完成: " << encoded_text << endl;
//3、解码 找到根节点,然后顺着根节点按照编码的顺序依次往下找,直到找到叶子节点
cout << "================解码====================" << endl;
int root_index = 1;
while (HT[root_index].parent != 0)
{
root_index++;
}
char decoded_text[100];
int cur = 0;
int encode_len = strlen(encoded_text);
int j = 0;
while (j < encode_len) {
int p = root_index;
while (HT[p].lchild != 0 || HT[p].rchild != 0) {
char code = encoded_text[j++];
if (code == '0')
{
p = HT[p].lchild;
}
else {
p = HT[p].rchild;
}
}
decoded_text[cur] = 'a' + p - 1;
cur++;
}
decoded_text[cur] = '\0';
cout << decoded_text << endl;
return 0;
}
#endif
3、香农码和费诺码
3.1香农码
利用香农编码方法得到的码字多余度稍大,其实用性不大,但是具有很重要的理论意义。
香农码主要是先计算出码字长度后再求具体的码。
首先了解香农第一定律,香农第一定理指出了平均码长与信源之间的关系,也指出了可以通过编码使平均码长达到极限值。那么,如何构造这种码呢?
编码的具体步骤如下。
一般情况下,按照香农编码方法编出来的码,其平均码长不是最短的,也即不是紧致码(最佳码)。只有当信源符号的概率分布使不等式左边的等号成立时,编码效率才达到最高。
3.1费诺码
2.费诺编码
费诺编码方法的具体步骤如下
(1)把信源消息符号按其概率递减顺序进行排列
(2)将依次排列的信源符号分为两大组,使各组的概率之和尽可能接近相等,对各组赋予一个二进制码元“0”和“1”。
(3)将每一组的信源符号进一步再分成两组,使划分后的两个组的概率之和接近相等,再对各组赋予一个二进制码元“0”和“1”。
(4)重复这个步骤,直到每个组只剩下一个信源符号为止。
(5)从左至右,各信源符号所对应的码字即为编好的费诺码。
费诺码比较适合于每次分组概率都很接近的信源。特别是对每次分组概率都相等的信源进行编码时,可达到理想的编码效率。
4、总结
香农码、费诺码、哈夫曼码都考虑了信源的统计特性,使经常出现的信源符号对应较短的码字,使信源的平均码长缩短,从而实现了对信源的压缩;
香农码有系统的、惟一的编码方法,但在很多情况下编码效率不是很高;
费诺码和哈夫曼码的编码方法都不惟一;
费诺码比较适合于对分组概率相等或接近的信源编码;
哈夫曼码对信源的统计特性没有特殊要求,编码效率比较高,对编码设备的要求也比较简单,因此综合性能优于香农码和费诺码。