哈夫曼树
哈夫曼树是一种应用广泛的二叉树,可用来构造最优编码,用于信息传输、数据压缩等方面。
我们先来了解一些基本概念
- 路径:路径是指从一个结点到另一个结点之间的分支序列。
- 路径长度:是指从一个结点到另一个结点经过的分支数目。
- 结点的权:实际应用中,人们常常给树的某个结点赋予一个具有某种实际意义的实数,称该实数为这个结点的权。
- 树路径长度:从树根到每一结点的路径长度之和
- 带权路径长度:从树根到该结点的路径长度与该结点的权的乘积。
- 树的带权路径长度:树的带权路径长度为树中从根到所有叶子结点的各个带权路径长度之和,记为WPL
如下图:该树根结点到D结点的路径长度为4,该树的路径长度20,该树的带权路径长度
WPL=51+152+403+304+10*4=315
研究树的路径长度PL和带权路径长度WPL,目的在于寻找最优。
带权路径长度WPL最小的二叉树称为哈夫曼树。
哈夫曼树是由n个带权叶子结点构成的所有二叉树中带权路径长度最短的二叉树,又称为最优二叉树。
构建哈夫曼树
(1)初始化:用给定的n个权值{w1,w2,……,wn}构造n棵二叉树并构成的森林F={T1,T2,……,Tn},其中每一棵二叉树Ti(1<=i<=n)都只有一个权值为wi的根结点,其左、右子树为空。
(2)找最小树。在森林F中选择两棵根结点权值最小的二叉树,作为一棵新二叉树的左、右子树,标记新二叉树的根结点权值为其左、右子树的根结点权值之和。
(3)删除与加入。从F中删除被选中的那两棵二叉树,同时把新构成的二叉树加入到森林F中。
(4)重复②③步,直到森林中只含有一棵二叉树为止,此时得到的二叉树就是哈夫曼树。
给定一组权值{5,10,15,30,40},构造哈夫曼树。
(1)先取最小的两个结点5,10,构成新的结点的左、右孩子,注意相对较小的为左孩子。
(2)这时新子树的权值为15,放入F中,再取最小的两个结点15,15构成新的结点,重复以上步骤
哈夫曼树算法实现
//创建结点类
public class HuffmanNode<T> {
//存储数据
public T data;
//结点的权值
public int weight;
//左右孩子结点
public HuffmanNode<T> left;
public HuffmanNode<T> right;
public HuffmanNode() {
super();
}
public HuffmanNode(T data, int weight) {
super();
this.data = data;
this.weight = weight;
}
public HuffmanNode(T data) {
super();
this.data = data;
}
public HuffmanNode(T data, int weight, HuffmanNode<T> left, HuffmanNode<T> right) {
super();
this.data = data;
this.weight = weight;
this.left = left;
this.right = right;
}
}
//实现创建哈夫曼树算法
//传入参数为结点集合
public static HuffmanNode createTree(List<HuffmanNode> nodes) {
int count=0;
// 只要nodes数组中还有2个以上的节点
while (nodes.size() > 1) {
//先对结点按照权值进行排序,排序算法可以自己实现
quickSort(nodes);
//获取权值最小的两个节点
HuffmanNode left = nodes.get(nodes.size()-1);
HuffmanNode right = nodes.get(nodes.size()-2);
//生成新节点,新节点的权值为两个子节点的权值之和
HuffmanNode parent = new HuffmanNode(++count, left.weight + right.weight);
//让新节点作为两个权值最小节点的父节点
parent.left = left;
parent.right = right;
//删除权值最小的两个节点
nodes.remove(nodes.size()-1);
nodes.remove(nodes.size()-1);
//将新节点加入到集合中
nodes.add(parent);
}
return nodes.get(0);
}
哈夫曼编码
了解完哈夫曼树后,我们来看看它的应用,哈夫曼树的出现当年是为了解决远距离通信(电报)的数据传输问题。
例如我们要传递”BADCADFEED“这段内容,数据是以二进制形式进行传递的。
我们假设字母对应的二进制编码如下 :
那么我们实际传递的数据就是001000011010000011101100100011,何况这才是几个字符,如果一篇非常长的文章,那么数据量就很可怕了。但每个字母的出现频率是不同的,哈夫曼编码的思想是:给使用频率较高的字符编以较短的编码。怎么理解呢?我们上面任何一个字符都是3位,都为定长编码,而我们就可以采用不定长的编码,然后让频率较高的字符编上较短的编码。
假设我们的六个字母出现的频率为A 27 , B 8 , C 15 , D 15 , E 30 , F 5,合起来为100%,然后我们构造哈夫曼树按照频率来排序这些字符。
然后我们将权值左分支改为0,右分支改为1.
此后我们对字符以从树根到结点所经过的路径来编码。 此时就会得到不定长编码,如下
此时我们再将刚才的字符进行编码,得到1001010010101001000111100,就会发现数据被压缩了
前缀编码:如果在一个编码系统中,任一字符的编码都不是其他任何字符编码的前缀,则称这种编码为前缀编码。例如一组编码1000,100,10,1001就不是前缀编码。
哈夫曼编码:一般地,设需要编码的字符集为{d1,d2,……dn},各个字符在电文中出现的频率集合为{w1,w2……wn},以d1,d2……dn作为结点,以其频率作为结点权值构造一棵哈夫曼树,对树中的左分支赋予0,右分支赋予1(也可规定左1右0),则从根结点到叶子结点经过的路径分支组成的01序列便作为该字符的编码,这就是哈夫曼编码。
哈夫曼编码的特性
- 哈夫曼编码是前缀编码。
- 哈夫曼编码是最优前缀编码。即哈夫曼编码能使各种报文(由这n种字符构成)对应的二进制串的平均长度最短。