哈夫曼树
一.哈夫曼树的基本概念及其构造
哈夫曼树是一种最优的二叉树。既然它也是二叉树,那么肯定就具有二叉树的基本特性。
但是,哈夫曼树的构造又有其独特性。
举个例子来说明一下:
比如说现在要用 12、74、6、9、28这5个数字来构造哈夫曼树
①.首先第一步是将这5个数字进行排序
结果是:6、9、12、28、74
②.第二步是以最小的两个数为叶节点来构造一个二叉树,二叉树根节点中的数据是两个叶节点之和。在数列中用这个根节点的数据来代替这两个叶节点所对应的数据
结果是:
构造好的二叉树:
新的数列是:12、15、28、74
③.之后在不断重复第二步,知道将所有的数据都加入到哈夫曼树上。
最终构造好的哈夫曼树是:
有画圈就是原始的那几个数字。
二.最优二叉树的判断
在第一项中讲了,哈夫曼树是最优的二叉树,那么二叉树的优劣又是怎么判断的呢?
这时,涉及到了带权路径长度(WPL)的概念。
带权路径就是所有的叶子节点的权值乘以它到根节点的距离之和。
在之前举得例子中,带权路径长度WPL=6*4+9*4+12*3+28*2+74*1=226
与其他的二叉树比较:
WPL=6*3+9*3+12*3+28*3+74*1=239. WPL=6*3+9*3+12*2+28*2+74*2=273
可见,哈夫曼树的带权路径长度确实比其他的二叉树要小
但是为什么哈夫曼树的带权路径长度就是最小的呢? 或许我们不会比较严格的数学证明,但是,我们可以从一个比较直观的角度去理解,就是要让带权路径尽可能地小,关键在于让权值大的叶子节点距离根节点尽可能近。这样就需要将每个节点进行排序之后,再将节点从小到大依次往上排,这样就可以实现权值大的节点离根节点近。
三.哈夫曼树的应用
可以根据左0右1的原则给哈夫曼树的每个叶子节点加上指定一个编码
如下图:
各个数字的哈夫曼编码分别是:6---0000
9---0001
12---000
28---01
74---1
到这里我们不禁要想:获得的这个哈夫曼编码到底有什么用嘞?
其实,联系之前做过的一个统计字符串中每个字符个数的练习,不难想到,每个字符可以看作是哈夫曼树里面的一个叶子节点,而每个字符的个数就可以看作是这个节点的权值。然后,我们可以给每一个节点一个哈夫曼编码,用来区分不同的节点。这样,我们是不是可以将一串字符串翻译成只有0和1组成的字符串呢? 如果再将这个01串每隔8位就用一个byte型数来代替(如果位数不够就在01串尾端添加0),这样一来,一长串字符串就转化成了一段长度上短很多的由byte型数组成的字符串。 这样,是不是可以实现把一个文本文件压缩的效果呢?解压的话,只要利用哈夫曼编码将字符串翻译出来即可。
其实,适当联想一下,我感觉这个哈夫曼树还可以用在文件的加密上。只要把整个过程中的一些小步骤略做改变,比如转换成的byte数每个都加1……。
当然,真正涉及到实践操作时,肯定没有上面讲的这么简单。比如说,当我们把一个字符串中的字符个数都统计出来之后,我们肯定要把不同的字符存到不同的节点里面,然后再把所有的节点再都添加到一个队列中或者链表中。同时还要求队列中的节点是按照节点中存储的字符在被统计的字符串中出现的次数。
那么,我们该怎么办呢? 这个时候,出现了一个叫优先队列的东西,名为java.lang.Comparable<T>. 当然,这只是一个接口,我们要用定义的节点类去实现它,并且重写其中的int compareTo(T o){}方法。 顾名思义,compareTo方法指的就是两个节点之间比较大小先后的标准。比如说一个节点中,有Data和times两个数据,你希望根据times的大小来对节点进行排序,这样,你就可以在方法中这样写:
public int compareTo(Object o) {
int k=0;
if(o instanceof hfmNode)
{
hfmNode node=(hfmNode)o;
if(times>node.times)
k=1;
if(times<node.times)
k=-1;
if(times==node.times)
k=0;
}
return k;
}
这样,当你把节点添加到优先队列中的时候,系统会自动调用compareTo的方法来比较节点之间的大小,然后把节点按大小顺序排好。
当然,这之后树的构造,哈夫曼编码的获取感觉还挺漫长,也会涉及到许多技巧,这里就不详细展开了。