二叉树的应用——赫夫曼树与赫夫曼编码
1、赫夫曼树
赫夫曼(David Huffman,也译为“哈夫曼”),美国数学家,他在1952年发明了赫夫曼树与赫夫曼编码。赫夫曼编码是当代压缩和解压缩技术的基础。
我们通过一个具体的示例来体会一下什么叫赫夫曼树(Huffman Tree)。
给定一个成绩(0~100),输出所对应的分数段。我们可以通过以下代码来实现:
if(a<60)
printf("不及格\n");
else if(a<70)
printf("及格\n");
else if(a<80)
printf("中等\n");
else if(a<90)
printf("良\n");
else
printf("优\n");
粗略来看没什么问题,但在通常情况下,学生成绩大致都分布在“良”和“中等”的分数段,处于两端的“优”和“不及格”反而很少:
不及格:5%
及格:15%
中等:40%
良:30%
优:10%
在这种情况下,若采用上面的程序,则判断数量最大的“中等”和“良”分数段时,都需要至少3次的比较才能得出结果,大大影响了程序的执行效率。
如果我们改进算法,将比利最高的“中等”或“良”作为首个判断条件,则可大大加快程序的执行效率。
if(a<80)
{
if(a<70)
{
if(a<60)
printf("不及格\n");
else
printf("及格\n");
}
else
printf("中等\n");
}
else
{
if(a<90)
printf("良\n");
else
printf("优\n");
}
2、赫夫曼树的定义
我们可以将上文中的程序画成二叉树的形式,每个if分支都作为二叉树的两个子树,将概率标记到父节点到子节点的路径上。这样的树叫做带权(Weight)树。
从树中一个节点到另一个节点之间的边构成了两点之间的路径,路径上的权值之和叫做路径长度。树的路径长度就是从根节点出发到每一个节点的路径长度之和。如果考虑带权的二叉树,节点的带权路径长度等于从该点到根节点的路径长度乘以该点的权值的乘积。带权二叉树的路径长度为所有叶子节点的带权路径长度之和。
针对同一问题,不同的二叉树画法路径长度也不同,其中最小路径长度的树就叫做赫夫曼树。
3、构造一棵赫夫曼树
构造一棵赫夫曼树的流程如下:
⒈将所有叶子节点按权值大小有序排列。例如上文的成绩占比,我们可以排列成:
A5 E10 B15 D30 C40
⒉取前面两个最小的叶子节点作为兄弟节点,权值小的做左孩子,权值大的做右孩子,它们两个共用一个父节点N1,其父节点的权值为两个叶子节点的权值的和。得到:
N115 B15 D30 C40
⒊重复步骤2,将N1与B作为新的两个子节点,其父节点为N2,权值为30
N230 D30 C40
⒋重复步骤2,将N2与D作为新的两个子节点,其父节点为N3,权值为60
C40 N360
⒌重复步骤2,将C与N3作为新的两个子节点,由于这时所有节点都已构造完毕,因此其父节点就是该二叉树的根节点。
最后得到构造完成的赫夫曼树如图:
此时该二叉树的带权路径长度为WPL=40*1+30*2+15*3+10*4+5*4=205。
4、赫夫曼编码
其实构造赫夫曼树可不是为了解决程序效率低的问题,赫夫曼树的主要作用是用来构造赫夫曼编码,而赫夫曼编码则是为了解决当年远距离通信(主要是电报)的数据传输最优化问题。
例如:若有一个编码规则:
A 000
B 001
C 010
D 011
E 100
F 101
可以看出是简单的以二进制的方式进行编码。
若有以下报文:BADCADFEED
则按照以上编码规则编码后,获得编码为:001000011010000011101100100011
对方接收到报文编码后,按编码规则3位一分来解码即可。
事实上,英语中字母的使用频率是不同的,常用字母(如a e i o u等)使用频率特别高,而不常用字母(如j v z等)使用频率就低得多。如果我们能人为减少常见字母的编码长度,则整个报文长度都会缩短,这样既方便传输又节省了存储空间。
假设上文的编码规则中,各字母出现的额频率如下:
可以看到,如果我们能缩短字母A和E的编码长度,则我们就可以大大缩短报文长度。这种压缩效果在文本长度很长时效果会更加明显。
但是我们看到,所有编码都是以二进制(0/1)进行编码的,贸然缩短字母A和E的编码长度很容易引起混淆,若要设计这种长短不等的编码,必须使得任意字符的编码不能是其他字符编码的前缀。
我们可以使用上文构造赫夫曼树的方法来构造一个赫夫曼树:
假设在一个报文中,6个字母的出现频率如下:
A 27%
B 8%
C 15%
D 15%
E 30%
F 5%
则我们可以构造出如下的赫夫曼树:
构造完毕后,若从根节点出发,规定走向左子树的边编码为0,走向右子树的边编码为1,则我们就可以得到每个字母新的编码:
A 01
B 1001
C 101
D 00
E 11
F 1000
则使用新的编码规则后,对报文BADCADFEED获得新的报文编码如下:
1001010010101001000111100(新)
001000011010000011101100100011(旧)
大概缩短了17%。
而且我们发现,通过赫夫曼树构造出的赫夫曼编码,不会存在因编码长短不一而混淆的情况。
现代计算机程序中大部分的压缩文件算法都是基于赫夫曼编码改进而来的。1977年,Lempel-Ziv在对赫夫曼编码进行深度研究后,发表了“顺序数据压缩的一个通用算法(A Universal Algorithm for Sequential Data Compression )”的论文,论文中描述的算法被后人称为LZ77算法。LZ77算法以及后续的衍生算法是现在一些通行的压缩包模式(ARJ,PKZip,WinZip,LHArc,RAR,GZip,ACE,ZOO,TurboZip,Compress,JAR,7z等)的编码基础算法。