哈夫曼树
在介绍哈夫曼编码前,我们先来了解一下哈夫曼树。
美国科学家哈夫曼在1952年发现了哈夫曼编码,为了纪念他的成就,于是把他在编码中用到的特殊二叉树称之为哈夫曼树,这种编码方法称之为哈夫曼编码。
那在介绍哈夫曼树之前在补充一些基础知识
结点的路径长度:从根结点到该结点的路径上分支的数目。
树的路径长度:树中每个结点的路径长度之和。
结点的带权路径长度:该结点到根结点之间的路径长度与结点上权的乘积。
树的带权路径长度(WPL):树中所有叶子结点的带权路径长度之和。
上图的树路径长度就是:1+2+3+3+2+1+2+2 = 16,WPL:5x3+15x3+40x2+30x2+10x2 = 220。
哈夫曼树就是WPL最小的二叉树,那上图所示的二叉树是不是最优的二叉树呢?就让我们一起来来验证一下吧。
1. 先把有权值的叶子结点从小到大排序
(A5,E10,B15,D30,C40)
2. 每次取最小的两个结点合并为一个新的结点
3. 删除最小的两个结点,将新的结点插入到序列中并保证序列有序
4. 重复2,3步,直至所有叶子结点都已被合并,找到根结点
WPL = 40x1+30x2+15x3+10x4+5x4 = 205,比之前的220小了15,显然此时的这颗二叉树才是哈夫曼树。
哈夫曼编码
在理解了哈夫曼树之后,我们来看一下哈夫曼编码。
哈夫曼编码发明的时侯的目的是为了解决当年远距离通信的数据传输的最优化问题。该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字。
在网络上我们要传输一段内容给对方(显然要通过二进制的方式),假设这段内容中只有A,B,C,D,E,F六个字母,那么可以表示为:
字母 | A | B | C | D | E | F |
---|---|---|---|---|---|---|
二进制 | 000 | 001 | 010 | 011 | 100 | 101 |
如果我们要传输的是 BADCADFEED,那么转化过后为 001000011010000011101100100011。那么问题来了,如果要传输的内容很大,那转化后的二进制串将是非常长…,这种方式效率实在是有些可怕。
假设六个字母出现的频率(权值)为A 27,B 8,C 15,D 15,E 30,F 5,我们按照和夫曼树的规则来规划它们。
构建哈夫曼树。
将左分支都改为0,右分支改为1(也可右0左1)。
这样用0,1表示后,每个字符的编码就变成了下表:
字母 | A | B | C | D | E | F |
---|---|---|---|---|---|---|
二进制 | 01 | 1001 | 101 | 00 | 11 | 1000 |
二进制串:1001010010101001000111100
可以看到,出现频率越高的字符转化后的二进制编码就越短,而这也是哈夫曼编码节省资源的要点所在(权重越大,编码越短)。
这节约了大概17%的资源,而随着字符的增多和多字符权重的不同,这种压缩的优势会更加明显。
前缀编码
编码中非0即1,长短不等的话是很容易混淆的,所以若要设计长短不等的编码,则必须是任一字符的编码都不是另一个字符的编码的前缀,这在编码成为前缀编码,而哈夫曼编码便是最优的前缀码。
注:既然我们已经编码成功发送给对方,那对方如何解码呢?当然还是通过哈夫曼树,所以双方应该提前约定好同样的哈夫曼编码规则。
简单实现
#include<stdio.h>
#include<stdlib.h>
#include<bits/stdc++.h>
using namespace std;
#define Lnum 99
/* 哈夫曼编码 */
typedef struct
{
double weight; //权值
int parent,lchild,rchild; //父结点,孩子结点
}HuffmanTree;
typedef struct
{
int node_num;
double weight;
}Node;
typedef struct
{
char ch; //储存字符
char bits[Lnum]; //编码位串
}HuffmanCode;
int n,m; //全局变量(n—叶子结点树,m—总结点数)
bool cmp(Node& a, Node& b)
{
return a.weight < b.weight;
}
//寻找权值最小和次小的结点
void SearchMin(HuffmanTree *T, int flag, int *p1, int *p2)
{
Node N[m];
int j = 0;
for(int i = 0; i < flag; ++i)
{
if(T[i].parent == -1) //如果还没有被合并
{
N[j].node_num = i;
N[j].weight = T[i].weight;
++j;
}
}
sort(N, N + j, cmp); //按权值从小到大排序
//前面两个是所需要的结点
*p1 = N[0].node_num;
*p2 = N[1].node_num;
}
//创建哈夫曼树
void CreatHuffmanTree(HuffmanTree *T)
{
int p1,p2;
//初始化哈夫曼树
//将2n-1个结点里的三个指针均置为空(即置为-1),权值置为0
for(int i = 0; i < m; ++i)
{
T[i].weight = 0;
T[i].parent = -1;
T[i].lchild = -1;
T[i].rchild = -1;
}
cout << "请输入" << n << "个叶子结点的权值:";
for(int i = 0; i < n; i++)
cin >> T[i].weight;
for(int i = n; i < m; i++)
{
//合并结点(每次取权值最小和次小的结点),合并m - n(==n - 1)次
SearchMin(T, i, &p1, &p2);
T[p1].parent = T[p2].parent = i;
T[i].lchild = p1; //权值最小的结点为左孩子
T[i].rchild = p2;
T[i].weight = T[p1].weight + T[p2].weight;
}
}
//哈夫曼编码
void HuffmanCoding(HuffmanTree *T, HuffmanCode *C)
{
int p_child,p_parent; //指向孩子及父结点
char temp[n]; //临时存放编码
int begin; //标记编码在temp中开始的位置(因为是从下往上回溯)
temp[n - 1] = '\0';
cout << "请输入" << n << "个叶子结点所代表的字符:";
for(int i = 0; i < n; i++)
{
cin >> C[i].ch;
begin = n - 1;
//开始从下往上回溯
p_child = i;
while((p_parent = T[p_child].parent) != -1)
{
if(T[p_parent].lchild == p_child) //左孩子
temp[--begin] = '0';
else
temp[--begin] = '1';
p_child = p_parent; //继续往上回溯
}
strcpy(C[i].bits, &temp[begin]); //复制格式正确的编码
}
}
int main()
{
cout << "确定有几个叶子结点:";
cin >> n;
m = 2 * n - 1; //总结点数
HuffmanTree T[m];
HuffmanCode C[n];
CreatHuffmanTree(T);
HuffmanCoding(T, C);
cout << "哈夫曼编码:" << endl;
for(int i = 0; i < n; i++)
printf("字符: %c, 编码: %s\n", C[i].ch, C[i].bits);
return 0;
}