引子
期末考试成绩出来了,具体分数不公布,只公布等级,A,B,C,D,E
老师要根据具体成绩算出每个同学的等级,规则如下
[85, 100]: A;
[70, 85): B;
[60, 70): C;
[0, 60]: D;
完成这种映射,可以用下面这个函数来实现
function rank(score) { if (score >= 85) return 'A'; else if (score >= 70) return 'B'; else if (score >= 60) return 'C'; else return 'D'; }
这样算是完成了,但是结合实际,我们还有一个条件没利用上,那就是一张合理的试卷,最后结果应该是大多数人中等,高分和低分的人是比较少的。如果我们把第一个if语句的条件改为if(score >= 70 && score <85),那么大多数的成绩只要走一个if,就能够得出结果,统计全部学生等级的时间就会减少。其实,这是一棵普通树转为哈夫曼数的过程。
分析
一开始的程序,如果看成一棵树,那么是这样子的。
而后来的树,是这样子的
那么如何画出第二棵树,即如何决定哪个判定条件在上面呢?光凭感觉还不行,要有实际的数据去支持。
遵从一个原则,让数量越多的结果,走越短的路,比如说等级B,中等成绩的学生数量是比较多的,所以我们希望它能在树的上方。
下面是步骤
首先要知道每个结果的比重,每个结果是一个叶子节点。
把叶子节点按照升序排列,取出前两个,构造成一棵新的树,孩子节点就是刚才取出的两个,树根节点的比重是孩子节点比重之和,用这个新的根节点取代刚才的两个孩子节点,与剩下的节点升序排列,重复直到没有叶子节点。
上面这个例子其实还不是哈夫曼树的最佳应用,把原先的树转化为哈夫曼树,是有缺点的,虽然走的分支少了,但是判定条件变复杂了。
还有另外的应用,为利用哈夫曼树把字符编码为二进制。
假设一篇文章里只有ABCDEFGH这八个字母,为了传输这篇文章,要把它转化为二进制。
容易想到,八个字母,是2的3次方,用三位二进制数就可以表示这八个字母了,而解析的时候,只要按照每3位去解析,就可以还原了。按照这种编码方式(以下称为等长编码),这篇文章编码后的总长度会变为原来字母数量的3倍。要知道,长度越长,传输时间就越长,有没有办法去减少编码后的总长度呢?试想一下一种稍微极端的情况,如果这篇文章中,字母A占据了大多数,如果按照刚才的方法,那么总长度还是不变,如果换一种方法,用0表示A,其他的还是三位,那么显然总长度会变小,把这种想法具体化,就是哈夫曼树了。先统计好每个字母出现的次数,算出每个字母的比重,然后构造哈夫曼树。对每个分支,都左边的设置为0,右边的设置为1,从根结点到叶子节点,走过的分支编号序列,就是该节点对应字符的编码了。假设统计出来的结果是
A: 40%,B: 20%,C: 10%,D: 10%;E: 5%F:5%;G: 5%H: 5%
那么构造出来的哈夫曼树是这样子的(结果不唯一)
各个字符的编码(编码表)
A:0
B:100
C:1010
D:1011
E: 1100
F: 1101
G:1110
H: 1111
如果总共有100个字母,那么用一开始的等长编码方式,长度是300
用哈夫曼编码,最后的长度是 40*1 + 20*3 + 10 * 4 + 10 * 4 + (5 * 4)*4 = 260
(300 - 260)/300 = 0.1333 即便是后面的一些字母用4位,最后的长度还是比原来减少了13%。
字母的编码长度不同,如何去解析呢?实际上,这种编码是前缀码,从左到右扫描编码后的字符串,如果是0,那么该字符就是A,如果是1,那么得继续,直到得到一个在编码表中的编码。
结语
要充分地去发掘数据隐含的信息,等长编码的方法因为没有利用字符百分比的信息,所以效果不如哈夫曼编码
参考:大话数据结构