给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也叫作哈夫曼树(HuffmanTree)。
- 路径长度:若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1;
- 结点的带权路径长度:从根结点到该结点之间的路径长度与该结点权值的乘积;
- 树的带权路径长度:所有叶子结点的带权路径长度之和,记为WPL。
如对于下面这颗二叉树,其包含了4个权值:2 4 5 13作为叶子结点,树的带权路径长度为5*1+2*2 +4*3+13*3=60。该树并非一颗哈夫曼树,因为该树的带权路径长度并非为最短,例如交换结点2、4的位置,树的路径长度变为5*1+4*2+1*3+13*3=55<60,但此时该树仍然不是哈夫曼树。那么要如何得到一颗哈夫曼树呢?我们继续向下分析。
由上面二叉树的示例,我们可以看到,每层结点的路径长度是固定的,第二层的路径长度为1,第三层为2等等以此类推,结点带权长度则为权值*路径长度,在路径长度不变的情况下,权值越小则结点带权长度越小,而树的带权路径长度等于叶子结点的带权长度之和,当所有叶子结点的带权路径长度达到最小时,它也会达到最小。这等价于,权重越大的结点的层数要越低,越接近根结点,这样得到的二叉树才是哈夫曼树。
于是我们得到哈夫曼树的构造方法如下,每次我们都需要选出权重最小的两个结点,将它们的权重之和作为父结点,它们分别作为左儿子和右儿子。如果你对数据结构足够熟悉,很容易想到这就是一个二叉堆的典型应用,甚至如果你做过一道名为《合并果子》的竞赛题的话,你会发现该题的过程与构建哈夫曼树的过程几乎是完全相同的。利用java集合的优先队列,将其设置为小根堆,将所有结点存入优先队列中,每次取出优先级最高的两个结点分别保存并从优先队列中删除,以它们的权值之和创建一个新的结点,将取出的两个结点分别作为它的左儿子与右儿子,再将其添加到优先队列中,重复操作直到优先队列中只剩下一个结点,该结点就是哈夫曼树的根结点。
public class Node {
char data; //字符数据
int weight;//权重
Node left; //左儿子
Node right;//右儿子
public Node(char data, int weight) {
this.data = data;
this.weight = weight;
}
public Node(Node left, Node right, int weight) {
this.left = left;
this.right = right;
this.weight = weight;
}
}
Node root;//根结点
PriorityQueue q;
public HuffmanTree(ArrayList nodelist) {
q = new PriorityQueue((a,b)->a.weight-b.weight);
if(nodelist!=null)
for(int i=0;i1) {
Node a = q.poll();
Node b = q.poll();
Node father = new Node(a,b,a.weight+b.weight);
q.add(father);
}
return q.poll();
}
按照上面的构造方法,我们再对权值:2 4 5 13进行二叉树构造,得到如下形式的二叉树,其树的带权路径长度为13*1+5*2+4*3+2*3=41,无论再怎么交换叶子结点,树的带权路径长度都不会小于41,所以该二叉树是一颗哈夫曼树。
在数据通信、数据压缩问题中,需要将数据文件转换成由二进制字符0、1组成的二进制串,称之为编码。假设待压缩的数据为“abcdabcdacdddcdadddadddd”,文件中只包含a、b、c、d四种字符,如果采用等长编码,每个字符编码取两位即可(2^2=4),可能的一种编码方案如:a:00、b:01、c:10、d:11。上述数据为24个字符,其编码总长度为48位。但这并非最优的编码方案,因为每个字符出现的频率不同,如果在编码时考虑字符出现的频率,使频率高的字符采用尽可能短的编码,频率低的字符采用稍长的编码,来构造一种不等长编码,则会获得更好的空间效率,这也是文件压缩技术的核心思想。但对于不等长编码,如果设计得不合理,便会给解码带来困难。例如,单独的一个编码能译码成一个数据,而它与其它编码组合在一起又能译码成另一个数据。因此,若要设计长短不等的编码,必须满足一个条件:任何一个字符的编码都不是另一个字符的编码的前缀。那么如何设计有效的用于数据压缩的二进制编码呢?上文介绍的数据结构——哈夫曼树就可以实现。
哈夫曼编码的规则为,经过左儿子的路径都编码为0,经过右儿子的路径都编码为1。从根结点出发,到达叶子结点的路径上的“0”“1”编码组合起来就是该叶子结点对应字符的哈夫曼编码。对数据“abcdabcdacdddcdadddadddd”按每个字符出现的次数构建哈夫曼二叉树,并进行哈夫曼编码,结果如下图所示,a的哈夫曼编码为00,b的哈夫曼编码为010,c的哈夫曼编码为011,d的哈夫曼编码为1,这样最终的编码长度为13*1+5*2+4*3+2*3=41,即为树的带权路径长度,而哈夫曼树的带权路径长度最短,因此这样编码得到的二进制串也最短。
哈夫曼编码为什么能满足任何一个字符的编码都不是另一个字符的编码的前缀的要求?
因为要编码的字符都是哈夫曼树的叶子结点,不会有一条根到叶子的路径完全包含了另一条根到叶子的路径,因此任何一个字符的编码都不是另一个字符的编码的前缀。
public void printHfmCode(Node node, String code){
if(node.left!=null)
printHfmCode(node.left,code+"0");
if(node.right!=null)
printHfmCode(node.right,code+"1");
if(node.left==null && node.right==null)
System.out.println(node.data+"的哈夫曼编码为:"+code);
}
在包下创建txt文件,粘贴进一段英文文章,以每个字符出现的次数作为权重进行哈夫曼树的构建,之后打印每个字符的哈夫曼编码,示例代码如下:
public class MainTest {
@SuppressWarnings({ "null", "resource" })
public static void main(String[] args){
ArrayList nodelist = new ArrayList();
int[] weight = new int[128];
for(int i=0;i