学习一样事物,自然要先明其义,再通其用。哈夫曼树,顾名思义是一种树,不过它是一类带权路径最短的树。何谓权值呢?权值就是定义的路径上面的值。可以这样理解为结点间的距离;它通常指字符对应的二进制编码出现的概率。至于哈夫曼树中的权值可以理解为:权值大表明出现概率大!一个结点的权值实际上就是这个结点子树在整个树中所占的比例.
举一个网上给的例子:
abcd四个叶子节点的权值为7,5,2,4. 这个7,5,2,4是根据实际情况得到的,比如说从一段文本中统计出abcd四个字母出现的次数分别为7,5,2,4. 说a结点的权值为7,意思是说a结点在系统中占有7这个份量.实际上也可以化为百分比来表示,但反而麻烦,实际上是一样的.
由此哈夫曼树叶称作最优树或二叉树,不过这里要注意完全二叉树不一定就是最优二叉树呦!!那么哈夫曼树怎样构造呢?它的构造方式如下:
假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:
(1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
(2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
(3)从森林中删除选取的两棵树,并将新树加入森林;
(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。
嗯,现在哈夫曼树的定义以及构造方式已经描述完了,就差通过代码把这个过程实现。通过两部曲,我们把哈夫曼树构造理解清楚!
在此之前,你会不会有疑问:为什么这样构造就能确保带权路径最小呢???我们在高中接触过排序不等式公式,如下:
0<a1<a2<a3......<an 0<b1<b2<b3......<bn
然后有:an×bn+an-1×bn-1+......+a1×b1>=乱序和>=a1×bn+a2×bn-1+......+an×b1
构建哈夫曼树的方法使权值越大的节点离根节点越近,而权值越小的节点离根节点越远,如此便能保证带权路径最小了!
第一曲:手工构造!!
手工构造,其实非常简单,就是自己创建几个节点,然后建立节点之间的关系,形成一棵树,左节点上的权值为0,右节点未1,父节点也为1,最后把叶子节点的编码打印出来。这个过程实际上就是帮助我们更好地理解哈夫曼树的构造过程,为自动建树巩固基础!
第二曲:自动建树!!
通过给定一个字符串,然后统计相同字符出现的次数,再根据这些次数自动创建哈夫曼树,再输出叶子节点的编码。
代码如下:
public class HFMTreeCode { class Node{ private Node left;//左节点 private Node right;//右节点 private Node parent=null;//父节点 private int data;//数据 } private Node root;//根节点 //哈夫曼树的手工建造 public Node TreeCreat(){ // 19 // 8 11 // 4 4 5 6 // 2 2 Node n1=new Node(); n1.data=2; Node n2=new Node(); n2.data=2; Node n3=new Node(); n3.data=n1.data+n2.data; n3.left=n1; n3.right=n2; Node n4=new Node(); n4.data=4; Node n5=new Node(); n5.data=5; Node n6 =new Node(); n6.data=6; Node n7=new Node(); n7.data=n3.data+n4.data; n7.left=n3; n7.right=n4; Node n8=new Node(); n8.data=n5.data+n6.data; n8.left=n5; n8.right=n6; Node n9=new Node(); n9.data=n7.data+n8.data; n9.left=n7; n9.right=n8; root=n9; return root; } //输出哈夫曼树的编码 public void Treeprint(int []data) { Node []nodes=new Node[data.length];//创建一个节点类型的数组 //遍历数组,将data数组里的数据传给nodes数组里data数据 for(int i=0;i<nodes.length;i++) { nodes[i]= new Node(); nodes[i].data=data[i]; } while(nodes.length>1) { //排序 BubbleSort(nodes); //找到最小的两个值,然后生成新结点 Node node1=new Node(); Node node2=new Node(); node1=nodes[0]; node2=nodes[1]; Node newNode=new Node(); newNode.left=node1; newNode.right=node2; newNode.data=node1.data+node2.data;//新结点数据之值为nodes数组两个含最小data数值的节点它们data值之和 //创建一个新Node类型数组,存放原来nodes数组中除两个含最小data值的node外其他node和新结点 Node[]node1s=new Node[nodes.length-1]; for(int i=2;i<nodes.length;i++) { node1s[i-2]=nodes[i]; } node1s[node1s.length-1]=newNode; nodes=node1s;//交换两个数组地址 } root=nodes[0]; TreePrint(root,""); } //打印当前节点的编码,用递归的思想 public void TreePrint(Node node ,String code){ if(node!=null){ if(node.left==null&&node.right==null) {//只打印出需要的节点编码 System.out.println(node.data+"编码是"+code); } TreePrint(node.left,code+"0");//递归调用打印左节点编码,每次都+0 TreePrint(node.right,code+"1");//递归调用打印右节点编码,每次都+1 } } /* * 冒泡法 */ public void BubbleSort(Node [] array) { //循环遍历数组 for(int i=0;i<array.length;i++) { for(int j=i+1;j<array.length;j++) { if(array[i].data>array[j].data) { Node temp = new Node(); temp=array[i]; array[i]=array[j]; array[j]=temp; } } } } /* * 交换数组元素的方法 */ public void swap(Node [] array,int x,int y) { Node temp=null;//设置一个中间变量,先初始化为0 //交换过程 temp=array[x]; array[x]=array[y]; array[y]=temp; } /** * @param args */ /* * 统计一个字符串中不同字符的次数,并返回一个存入次数的整型数组 */ public int[]arrayPrint(String str) { String str1="";//创建一个存储不含重复字符的字符串 for(int i=0;i<str.length();i++) { char c=str.charAt(i); if(str1.indexOf(c)==-1) {//如果str1中不含c str1+=c;//则将c加到str1上 } } int []array=new int[str1.length()]; //遍历数组,把字符出现的次数加到整型数组array[]中 for(int i=0;i<str1.length();i++) { char ch1=str1.charAt(i); array[i]=getCount(str,ch1); } for(int i=0;i<array.length;i++) { System.out.println(array[i]); } return array; } /* * 统计一个字符在字符串中出现的次数 */ public int getCount(String str,char ch) { int count=0; for(int i=0;i<str.length();i++) { if(str.charAt(i)==ch) count++; } return count; } public static void main(String[] args) { // TODO Auto-generated method stub //int[]array={1,4,5,8,2}; String str="dfsdfdsfsdfsdfsdasdas"; HFMTreeCode htc=new HFMTreeCode(); htc.Treeprint(htc.arrayPrint(str)); } }说明下,这里不需要另外创建一个结点类,而是直接在这个类内部中创建,因为结点类一般不会被其他类所使用,它的创建单纯是为了这个类的实现。
接下来,在完成了这些后,我们就可以开始朝哈夫曼压缩方面努力了!!!