哈夫曼树
给定 n 个权值作为 n 个叶子结点,构造一棵二叉树, 若该树的带权路径长度(wpl) 达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree), 也叫霍夫曼树。
哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
ps:
节点的带权路径长度:从根节点到该节点之间的路径长度与该节点的权的乘积
带权路径长度(WPL)是设二叉树有n个叶结点,每个叶结点带有权值w,从根结点root到各个叶结点的路径长度为l,则所有叶结点带权路径长度之和为Σwl=WPL
如:
哈夫曼树的构建思路为
1.从小到大对给定的数据进行排序;
2.取出权值最小的两个数据构成二叉树,两数之和为这两个数据的父节点;
3.再将两数之和放入数据中再排序,不断重复1-2-3步骤,直到数据中,所有数据都被处理,就得到了哈夫曼树。
我们都知道,计算机本质上来说,只能储存两种东西,即数字0和1。各种文件都是编码成二进制进行存储的。
哈夫曼编码
哈夫曼编码是利用哈夫曼树的特性形成的一种编码方式,区别于定长编码,是变长编码的一种。
比如当对“i like java”这串字符若用ASCII编码的话:
那么这一串字符串如果按ACSII编码就要花费 11×8=88(bit)。每个字符都用八位的二进制来表示是很浪费的方法。当然可以用三位的二进制,只需花费 11×3=33(bit)就可以了,大小仅为ASCII编码的大小的37.5%。
在编码中,i、a都出现了两次,而剩下均只出现了一次,有没有一种编码方式让前者所花费的存储空间更少而后者可以大一些,这样在倍数的作用下使得总内存占用最少?
可以想到变长编码
但这样就有一个问题,前面字符的编码会成为后面字符编码的前缀,这样就会出现二义性,不知道表示的到底是哪一个字符,比如0表示的是“i”,那在读入11时并不是“k”,而是“ii”
哈夫曼编码就解决了这个问题,保证了编码无二义性,又使得花费的空间更小了。
任何字符的编码都不是另一字符的前缀就可以避免二义性。
用二叉树表示编码,(1)左右支分别为0、1
(2)字符只能在叶结点上。这样就保证不会有二义性。
将字符和出现的频率列出来作为树的节点和权值,用上面的思路构建哈夫曼树,然后根据路径进行编码,对节点的左右两个方向编0或1(左边编0右边编1,或者左边编1右边编0)
如上述字符串,出现频率为
则构建的哈夫曼树为
这样我们就可以找到每个字符对应唯一的哈夫曼编码。
这种编码所占空间大小为是不是少了许多呢,而且也没有发生上述开头重复的问题,当然这个例子不是很突出,出现频率更多时,会出现一位或者两位的编码。
下面就具体用代码来实现试试哈夫曼编码
通过给定字符以及相应的频率,输出对应的哈夫曼编码
字符及频率: a:6 b:7 c:1 d:2 f:2 g:1
要建哈树,就要先定义一个哈树节点
class HTreeNode{
int count;//频率
String s;//代表的字符 只有叶节点才会有的
int code;
HTreeNode parent;
HTreeNode left;
HTreeNode right;
}
先将字符数组和权值数组合并到一起--节点数组,对节点数组的每个count值进行排序,每次都是取count最低的两个节点,把这两个节点之和,再放入要排序的数组中(当然用系统的ArrayList会更方便一些,不用老是新建数组进行数据的删除和添加)
所以要经常对节点数组的count进行排序,所以要先写个排序方法
//对数组进行排序(冒泡)
private HTreeNode[] sort(HTreeNode[] node){
for(int i=0;i<node.length;i++) {
for(int j=i+1;j<node.length;j++) {
if(node[i].count > node[j].count) {
HTreeNode temp = new HTreeNode();
temp = node[i];
node[i] = node[j];
node[j] = temp;
}
}
}
return node;
}
public HTreeNode createHFMTree(int[] ca,String[] sa){
//将字符数组和权值数组合并到一起--节点数组
HTreeNode[] node = new HTreeNode[sa.length];
for(int i=0;i<sa.length;i++) {
node[i] = new HTreeNode();
node[i].s = sa[i];
node[i].count =ca[i];
}
while(node.length>1){
HTreeNode[] da=sort(node);//排序
HTreeNode n1 = da[0];
HTreeNode n2 = da[1];//用这两个节点,构成一个树
//把这两个节点之和,再放入要排序的数组中
HTreeNode Node = new HTreeNode();
Node.left = n1;
Node.right = n2;
n1.parent = Node;n2.parent = Node;
Node.count = n1.count+n2.count;
//删除n1、n2
HTreeNode[] node2 = new HTreeNode[node.length-1];
for(int i=2;i<node.length;i++) {
node2[i-2] = node[i];
}
node2[node2.length-1] = Node;
node = node2;
}
HTreeNode root = node[0];
return root;//建好树的根节点;
}
创建好哈夫曼树后,对节点的左右两个方向左边编0右边编1,进行前序遍历,看看字符的哈夫曼编码
public void printTreeCode(HTreeNode root,String code){
if(root != null) {
if(root.left == null && root.right == null) {
String m=root.s+"权值:"+root.count+" HFM编码:"+code;
System.out.println(m);
}
printTreeCode(root.left,code+"0");
printTreeCode(root.right,code+"1");
}
}
看看结果
这就是哈夫曼编码的过程