贪心算法之哈夫曼树和哈夫曼编码
一.引例
一般的学生成绩评定分为五个等级:优秀、良好、中等、及格、不及格,常见的代码描述如下:
可以把上面的代码流程画成下面的树状流程图
由于大部分学生成绩都是出于70到90之间,所以按照上面的流程大部分人都需要至少三次判断。因此我们可以对评价流程做出一点优化,如下所示:
通过先与80分做对比,使得大部分人(分数处于70-90之间)只需要两次判断。
如何能设计出这种效率最高的树结构?答案就是哈夫曼树。
二.哈夫曼树
首先理解几个概念
1. 权:树中结点相关的数值;
2. 森林:m棵互不相交的树的集合;
3. 路径长度:从树中某个结点到另一个结点之间的分支数目(经过的边数);
4. 带权路径长度:从树的根结点到任意结点的路径长度(经过的边数)与该结点上权值的乘积称为该结点的带权路径长度。
树中所有叶结点的带权路径长度之和称为该树的带权路径长度,称为WPL(Weighted Path Length),且
其中Wi是第i个叶结点所带的权值,Ii是该叶结点到根结点的路径长度
比如下面图a、图b、图c分别是三个构造好的不同形态的树,数值代表字母a、b、c、d出现的频率
可以计算出
WPL(左)=7*2+5*2+2*2+4*2=36
WPL(中)=4*2+7*3+5*3+2*1=46
WPL(右)=7*1+5*2+2*3+4*3=35
哈夫曼树:含有N个带权叶子结点的二叉树中,WPL最小的二叉树,也称为最优二叉树。
哈夫曼树构造过程(哈夫曼算法):
1. 将N个结点分别作为N棵仅含一个结点的二叉树,构成森林F。
2. 构造一个新结点,并从F中选取两棵结点权值最小的树作为新结点的左右子树,并且将新结点的权值设置为左右子树结点的权值之和;
3. 从F中删除刚才选择的两棵树,同时将新得到的树加入F中;
4. 重复步骤2和3,直到F中只剩下一棵树为止。
举个例子:
对下图中的六个带权叶子结点来构造一棵哈夫曼树,步骤如下
哈夫曼树的特点:
1. 每个初始结点最终成为叶结点,且权值大的结点用短路径,权值小的结点用长路径。
2. 构造过程中一共新建了N-1个结点,加上之前的N个结点,因此哈夫曼树中的结点总数为2N-1。
整体来说,哈弗曼算法属于贪心算法的一种,其基本思路是:编码以字符出现的频率作为权重,每次选权重最小的两个节点作为生成最优二叉树的左右孩子,并将权重之和作为根节点的权重(概率),自底向上生成一颗带权路径长度最短的最优二叉树。
三.哈夫曼编码
哈夫曼编码最早用于解决远距离电报通信的数据传输最优化问题
比如ABCDE这一段文字,通过网络传输要用到二进制的数字0和1。按照下面的方式保存ABCDE五个字母就可以了。
但是如果信息很长,那么译码也会很长
例如 AABBCCDDEE...
对应 000 000 001 001 010 010 011 011 100 100...
实际上,不同的字符和字母出现的频率是不一样的,比如英语中的元音字母 A、E、I、O、U和汉字中的“有”、“是”、“在”...相对来说出现频率更高,所以我们可以借助这些特点和哈夫曼算法对信息进行编码。
举例来说:
假设字母出现的频率为A:20,B:10,C:30,D:25,E:15,则对应的哈夫曼编码可以按照步骤得出:
1. 首先构造哈夫曼树
2. 将权值左分支改为0,右分支改为1
3. 每个字母的哈夫曼编码为从根结点走到相应叶子结点
哈夫曼编码的优点
首先我们要知道三个概念
1. 等长编码
例如 AABBCCDDEE...
对应 000 000 001 001 010 010 011 011 100 100...
2.前缀编码
如果没有一个编码是另一个编码的前缀。
例如 A:0、B:101、C:100
就是一个前缀编码
3.非前缀编码
存在一个编码是另一个编码的前缀。
例如 A:10、B:101、C:110
就是一个非前缀编码
非前缀编码会造成翻译时的歧义
比如10110可以翻译成两个结果:
(1) 10 110 AC
(2) 101 10 BA
而哈夫曼编码是一种变长且前缀编码,对于同样的ABCDE进行编码,哈夫曼编码和等长编码结果如下:
等长编码一共有15位,哈夫曼编码一共有12位,节省约20%的空间成本,当信息内容很多时,哈夫曼编码的优势会更明显。
四.哈夫曼算法伪代码
算法:Huffman(C)
输入:C={x1,x2,...,xn},f(xi),i=1,2,3..n
输出:Q # 队列
1.n <—— |C|
2.Q <—— C # 频率递增队列Q
3.for i <—— 1 to n-1 do
4. z <—— Allocate-None() # 生成结点z
5. z.left <—— Q中最小元 # 最小作z左结点
6. z.right <—— Q中最小元 # 最小作Z右结点
7. f(z) <—— f(x)+f(y)
8. Insert(Q,z) # 将z插入Q
9.return Q
END
往期回顾