二叉树之哈夫曼树
节点之间的路径长度:在树中从一个结点到另一个结点所经历的分支,构成了这两个结点间的路径上的经过的分支数称为它的路径长度。
树的路径长度:从树的根节点到树中每一结点的路径长度之和。在结点数目相同的二叉树中,完全二叉树的路径长度最短。
结点的权:在一些应用中,赋予树中结点的一个有某种意义的实数。
结点的带权路径长度:结点到树根之间的路径长度与该结点上权的乘积。
树的带权路径长度(Weighted Path Length of Tree:WPL):定义为树中所有叶子结点的带权路径长度之和。
最优二叉树:从已给出的目标带权结点(单独的结点) 经过一种方式的组合形成一棵树.使树的权值最小.。最优二叉树是带权路径长度最短的二叉树。根据结点的个数,权值的不同,最优二叉树的形状也各不相同。它们的共同点是:带权值的结点都是叶子结点。权值越小的结点,其到根结点的路径越长,深度越大。
(a)WPL=7*2+5*2+2*2+4*2=36
(b)WPL=7*3+5*3+2*1+4*2=46
(c)WPL=7*1+5*2+2*3+4*3=35
可以验证其中(c)树的WPL最小,可以验证,它就是哈夫曼树。
注意:
① 叶子上的权值均相同时,完全二叉树一定是最优二叉树,否则完全二叉树不一定是最优二叉树。
② 最优二叉树中,权越大的叶子离根越近。
③ 最优二叉树的形态不唯一,WPL最小。
二、利用哈夫曼树来进行哈夫曼编码
哈夫曼编码通常用来实现数据压缩,编码时,我们通常希望出现次数最多的字符所占的内存位数最少。但是如果字符位太少又会导致他能够表示的实际字符数不够,因为每一位只有0和1两个状态,n位有2的n次方个状态。所以采用定长编码时数据压缩率不是很高。使用哈夫曼编码可以解决这个问题。
下面是一个编码实例:
原句子为SUSIE SAYS IT IS EASY
句子中每个字符出现次数
相应的哈夫曼编码:
可以看出出现频率越高的字符所用的编码字符数越少。
创建哈夫曼树:
1.设计节点类Node:
Node中包含两个数据项:存储的字符和字符出现的频率(也就是权值)。
2、为每一个字符创建一个相应的Node对象,并把为其创建一个单独的树,树的根就是当前的节点。N个字符就创建N个相应的树。
3、把这些树都插入一个优先级队列中,按照权值排列。权值小的优先级高,排在队列的前方。每次移除队列的一项时,都是移除权值最小的那颗树。
4、从优先级队列中按顺序移除两个树,并将其作为一个新节点的两个子节点。新节点的权值(频率)是这两个子节点的权值和,至于新节点中存储的字符可以设为空。
5、把第四步形成的新树重新插入优先级队列中(自动插入到了相应位置)。
6、重复第四步和第五步,直到队列中只剩一棵树,这棵树就是要创建的哈夫曼树。
图解:
最后将图h中的两个树合并为1个就得到哈夫曼树。
根据哈夫曼树得到哈夫曼编码表:
所有字符在哈夫曼树中都被表示为树中的叶结点。遍历所有叶结点即可得到相应字符的哈夫曼编码,从而形成编码表。编码规则:从根节点到叶结点,记录向左和向右走的顺序,向左走用0表示,向右走用1表示。到达叶结点时的01字符串就是对应的哈夫曼编码。
例如字符U的编码是01111,字符Y的编码是1110。
代码实现:
Node节点代码,此处权值放到了Tree中。
class Node { public char cchar; //存储的字符 public Node leftChild; // 左子结点引用 public Node rightChild; // 右子节点引用 public Node() { } public Node(char c) { cchar = c; } public void displayNode() // 输出节点信息 { System.out.print('{'); System.out.print(cchar); System.out.print("} "); } }
class Tree implements Comparable { public Node root; // 根节点 public int weight; // 权重 // ------------------------------------------------------------- public Tree() // 无参构造函数 { root = null; } public String toString() {//转换为字符串 return root.cchar + ""; } // ------------------------------------------------------------- public void traverse(int traverseType) { // 三种遍历 switch (traverseType) { case 1: System.out.print("\nPreorder traversal: "); preOrder(root); break; case 2: System.out.print("\nInorder traversal: "); inOrder(root); break; case 3: System.out.print("\nPostorder traversal: "); postOrder(root); break; } System.out.println(); } // ------------------------------------------------------------- private void preOrder(Node localRoot) {//先序遍历 if (localRoot != null) { System.out.print(localRoot.cchar + " "); preOrder(localRoot.leftChild); preOrder(localRoot.rightChild); } } // ------------------------------------------------------------- private void inOrder(Node localRoot) {//中序遍历 if (localRoot != null) { System.out.print("("); inOrder(localRoot.leftChild); System.out.print(localRoot.cchar + " "); inOrder(localRoot.rightChild); System.out.print(")"); } } // ------------------------------------------------------------- private void postOrder(Node localRoot) {//后序遍历 if (localRoot != null) { postOrder(localRoot.leftChild); postOrder(localRoot.rightChild); System.out.print(localRoot.cchar + " "); } } // ------------------------------------------------------------- public void displayTree() { //利用栈输出树 Stack globalStack = new Stack(); globalStack.push(root); int nBlanks = 32; boolean isRowEmpty = false; System.out .println("......................................................"); while (isRowEmpty == false) { Stack localStack = new Stack(); isRowEmpty = true; for (int j = 0; j < nBlanks; j++) System.out.print(' '); while (globalStack.isEmpty() == false) { Node temp = (Node) globalStack.pop(); if (temp != null) { System.out.print(temp.cchar); localStack.push(temp.leftChild); localStack.push(temp.rightChild); if (temp.leftChild != null || temp.rightChild != null) isRowEmpty = false; } else { System.out.print("--"); localStack.push(null); localStack.push(null); } for (int j = 0; j < nBlanks * 2 - 2; j++) System.out.print(' '); } // end while globalStack not empty System.out.println(); nBlanks /= 2; while (localStack.isEmpty() == false) globalStack.push(localStack.pop()); } // end while isRowEmpty is false System.out .println("......................................................"); } // end displayTree() // ------------------------------------------------------------- @Override public int compareTo(Object o) { //重写比较函数 if (o == null) { return -1; } return weight - ((Tree) o).weight; } } // end class Tree // //////////////////////////////////////////////////////////////Huffman编码主体:
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.PriorityQueue; import java.util.Queue; import java.util.Set; import java.util.TreeMap; public class Huffman { public static Map<Character, String> map_char_code;//key为字符(Character是char类型的类封装),value为相应的哈夫曼编码 public static Map<String, Character> map_code_char;//key为哈夫曼编码,value为相应的字符 static { map_char_code = new HashMap<Character, String>(); // 编码用代码表 map_code_char = new HashMap<String, Character>(); // 解码用代码表 } // 编码分为四步 // 1.统计字符频率 // 2.生成Huffman树 // 3.生成编解码用代码表 // 4.编码字符串 //编码函数 public static String encode(String str) { char[] cchar = str.toCharArray();//将待编码的字符串转为字符数组。 // 1.统计字符频率 TreeMap<Character, Integer> map = new TreeMap<Character, Integer>(); //TreeMap是有序的,按照key来进行排序。Character类中实现了Comparable接口,该接口中只有一个 //public int compareTo(T o);方法,对象的大小关系由返回值来确定,返回负整数,零,正整数表示当前对象小于, //等于,大于指定对象。这里按照字典顺序排序。 for (int i = 0; i < cchar.length; i++) { if (map.containsKey(cchar[i])) {//map中包含该字符 map.put(cchar[i], map.get(cchar[i]).intValue() + 1);//统计数加1 } else { map.put(cchar[i], 1);//加入新字符,频率设为1 } } // 2.生成Huffman树 // 先由所有字符生成单节点树 // 然后根据优先级合成单节点树为一棵树 Queue<Tree> forest = new PriorityQueue<Tree>();//定义一个队列,存储数据类型为树 Set<Map.Entry<Character, Integer>> set = map.entrySet(); /* Set<Map.Entry<Character, Integer>> set = map.entrySet();返回映射所包含的映射关系的Set集合(一个关系就是一个键-值对),就是把(key-value)作为一个整体一对一对地存放到Set集合当中的。entrySet是 键-值对的集合,Set里面的类型是Map.Entry */ Iterator<Map.Entry<Character, Integer>> it = set.iterator();//转为迭代器 while (it.hasNext()) { // 遍历迭代器,生成单节点树 Map.Entry<Character, Integer> en = it.next(); Tree temp = new Tree(); temp.root = new Node(en.getKey());//创建相应节点为树的根 temp.weight = en.getValue();//设置权重 forest.add(temp);//加入队列 //因为Tree实现了Comparable接口,重写了CompareTo的方法,所以在加入队列时,会比较权重。最后形成基于权重的优先级队列。 } while (forest.size() > 1) { // 把单节点树合并为一棵树 Tree t1 = forest.remove();//移除第一个数据项 Tree t2 = forest.remove();//移除第二个数据项 Tree t3 = new Tree(); t3.root = new Node(); t3.weight = t1.weight + t2.weight;//新节点的权重是子节点之和 t3.root.leftChild = t1.root; t3.root.rightChild = t2.root; forest.add(t3); //重新加入队列 } Tree t = forest.remove(); // 获得队列中最后一棵树,也就是哈夫曼树 // 3.生成编码和解码用的map String code = ""; preOrder(t.root, code, map_char_code, map_code_char); // 4.编码字符串 StringBuffer output = new StringBuffer(); for (int i = 0; i < cchar.length; i++) {//获取每个字符的哈夫曼编码 output.append(map_char_code.get(cchar[i]));//append(String str),连接一个字符串到末尾。 } return output.toString();//转为字符串 } // 遍历Huffman树生成编码和解码代码表 private static void preOrder(Node localRoot, String code, Map<Character, String> map_char_code, Map<String, Character> map_code_char) { if (localRoot != null) { if (localRoot.cchar != '\0') {//'\0'代表空字符,在哈夫曼树,非叶节点没存储数据,相应的cchar为空。所以程序只操作了叶结点 map_char_code.put(localRoot.cchar, code);//加入编码表 map_code_char.put(code, localRoot.cchar);//加入解码表 } preOrder(localRoot.leftChild, code + "0", map_char_code, map_code_char);//向左走code末尾加上0 preOrder(localRoot.rightChild, code + "1", map_char_code, map_code_char);//向右走code末尾加上1 } } // 解码 // 根据解码代码表还原信息 public static String decode(String str) { StringBuffer result = new StringBuffer(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < str.length(); i++) { sb.append(str.charAt(i)); if (map_code_char.get(sb.toString()) != null) {//获取编码对应的字符 result.append(map_code_char.get(sb.toString())); sb = new StringBuffer(); } } return result.toString(); } public static void main(String[] args) { String code = encode("SUSIE SAYS IT IS EASY!"); System.out.println(code); String str = decode(code); System.out.println(str); } // ------------------------------------------------------------- public static String getString() throws IOException { InputStreamReader isr = new InputStreamReader(System.in); BufferedReader br = new BufferedReader(isr); String s = br.readLine(); return s; } } // =============================================================================