huffman coding

继上一节树和二叉树的介绍,本文详细介绍一种特殊的二叉树,叫huffman tree(赫夫曼树)。如果读者是初学者的话,请思考几个问题。

为什么需要这样一种二叉树?这种数据结构有什么优点?如何实现?应用实例?

huffman coding

大家都知道huffman树,却很少提到huffman编码,但huffman编码是介绍huffman树绕不过的一道坎。

首先,说说编码的事。就像谍战片中常出现的编解码问题,没份电报通过编码加密之后发出去,收到电报的一份用约定好的解码规则将电报内容还原,这不仅是一种信息的传递方式,还有传递安全的重要意义。抛开安全不说,回到最初的编码问题。那么人们对编码最初的要求有哪些?

  • 译码结果唯一,无歧义
  • 编码后的电文尽可能短

就ABCD编码而言,你可能会想到编码成0、00、1、01,虽然编码是短了,但译码结果却不唯一了,比如“0000”就可以译为“AAAA”、“BB”、“AAB”、“BAA”、“ABA”多种电文,显然这种编码是不合格的。能保证任意字符的编码都不是其他字符编码的前缀的编码,就叫作前缀编码。

借助二叉树编码,如何确保译码结果唯一?

如果非叶子节点保存字符信息的话,那么就会出现歧义的现象。相反地,只要保证只在叶子节点上存储字符信息,就可以避免这种现象。

定义叶子的编码对应规则:从根节点出发,选择左子树代表编码0,右子树编码1,那么从根节点到叶子的过程中进行的一系列选择唯一对应了叶子的编码。还是以ABCD编码为例说明,图如下:

借助二叉树编码,如何使得电文最短?

在满足条件一的情况下,考虑电文最短问题。不同字符出现的概率不同,电文最短问题,就是在保证为前缀编码的前提下,使得出现频率越高的字符对应的编码越短。假设字符出现的概率为wi,其对应的叶子节点到根的路径长度为li,则求min∑wil ,也叫作带权路径长度。

huffman树就是这样一种数据结构,可以满足以上要求。即概率越大的字符叶子离节点越近,且为前缀编码。为何满足从其构造过程可见一斑。构造过程如下:

  1. 构造两个队列A和B,分别存放叶子节点和子树。
  2. 选择A和B中概率最小的两个节点m和n
  3. 构造一个新的二叉树o,新子树的概率为m和n的概率之和,m和n为其左右子树
  4. 将o加入B中,判断是否满足A为空且B中只有一颗子树,若不满足返回步骤2,满足进入步骤5
  5. 返回B中唯一的一颗子树作为huffman树

由其构造过程可知,huffman树是严格二叉树,也即其节点只有两个孩子或者没有孩子。又忍不住问两个问题:

为什么huffman树得是严格二叉树?

假如存在只有一个子节点的节点,那么该节点的子节点取代其之后,∑wili比之前更小,这样的树不满足电文最短的要求。

huffman树唯一吗?

可能不唯一,举个例子。

图I 和 图II 为相同字符集的不同huffman树结构,可以证明,它们的带权路径长度∑wili相同。

huffman coding 实现

好吧,说到这里也该给出huffman coding的实现代码了,鄙人不才,代码是自己实现的,大部分接口是《数据结构从应用到实现(JAVA)版》中设计。给出代码之前,说明几点:

  1. 类中设置了七个变量,symbols[] probs[]为输入变量,存放字符集和对应的概率,要注意输入的字符集要按出现的概率大小升序排列。
  2. locations存放与输入字符索引一一对应的叶子节点的引用,编码的时候特别有用不需要遍历整个树匹配字符了。
  3. leaves和trees为存放叶子节点和子树的队列。leaves和trees使用的队列结构,小编电脑里只实现过循环队列,所以偷了个懒,哈哈。为什么是队列?leaves里的叶子节点自然是按照概率升序排列,只会减少不会增加,可以保证队头元素概率最小。那trees呢?因为新增的子树是由本轮两个队列概率最小的节点构成,其肯定比前轮的子树构成节点概率大,所以trees里保存的子树概率也是升序排列,也即队头子树概率最小。
  4. 输出变量是huffman和codes,分别是构建好的huffman树 和 与输入字符索引一一对应的字符编码,codes作用和locations相同,是locations的生成物,使用其编码比locations更方便。
  5. huffman 中叶子保存的是symbols[]里字符的索引,类型为float,非叶子节点保存的是其概率数值。
  6. 其实,可以不用这么麻烦,直接定制节点数据结构就能解决,但这么编写代码无疑耦合性较低。
  1 package com.structures.tree;
  2 
  3 import com.structures.linear.CircularQueue;
  4 
  5 import java.util.ArrayList;
  6 
  7 /**
  8  * Created by wx on 2017/11/4.
  9  * Implement huffman encode and decode function.
 10  */
 11 public class Huffman {
 12     // input variables
 13     private char[] symbols;
 14     private float[] probs;
 15 
 16     // process variables
 17     private ArrayList<BinaryTree<Float>> locations; // Save the pointer of binary node. Array is okay,too.
 18     private CircularQueue<BinaryTree<Float>> leaves;
 19     private CircularQueue<BinaryTree<Float>> subTrees;
 20 
 21     // output variables
 22     private BinaryTree<Float> huffmanTree;
 23     private String[] codes; // It is not necessary but useful when encode.
 24 
 25     // initializer of huffman tree
 26     public Huffman(char[] symbols, float[] probs){
 27         this.symbols = symbols;
 28         this.probs = probs;
 29         leaves = new CircularQueue<BinaryTree<Float>>(symbols.length);
 30         subTrees = new CircularQueue<BinaryTree<Float>>(symbols.length/2 + 1);
 31         locations = new ArrayList<BinaryTree<Float>>();
 32         codes = new String[symbols.length];
 33 
 34         if(symbols.length==0)
 35             throw new TreeViolationException("Error input parameters!");
 36         else if(symbols.length==1){ // If there are only one node, use it as root.
 37             buildLeaves();
 38             huffmanTree = leaves.dequeue();
 39             storeCodes();
 40             return;
 41         }
 42 
 43         buildLeaves();
 44         buildAll();
 45         huffmanTree = subTrees.dequeue();
 46         storeCodes();
 47     }
 48 
 49     // create binary tree for each input char with its index
 50     private void buildLeaves(){
 51         for(int i=0; i<symbols.length; i++){
 52             BinaryTree<Float> leaf = new BinaryTree<Float>();
 53             leaf.makeRoot((float)i);    // The binary tree saves the index of char
 54             leaves.enqueue(leaf);
 55             locations.add(leaf);
 56         }
 57     }
 58 
 59     // create a new binary tree with input trees
 60     private BinaryTree<Float> buildTree(BinaryTree<Float> first, BinaryTree<Float> second){
 61         BinaryTree<Float> newTree = new BinaryTree<Float>();
 62         newTree.makeRoot(getProb(first)+getProb(second));
 63         newTree.attachLeft(first);
 64         newTree.attachRight(second);
 65 
 66         return newTree;
 67     }
 68 
 69     // return the tree's prob
 70     private float getProb(BinaryTree<Float> tree){
 71         if(tree.left==null && tree.right==null)
 72             return probs[(int)((float)tree.getData())];
 73         else
 74             return tree.getData();
 75     }
 76 
 77     // get the trees with the lowest probability.
 78     // Caution! Only queue of leaves is not empty need this operation.
 79     private BinaryTree<Float> getMin(){
 80         BinaryTree<Float> first = leaves.first();
 81         BinaryTree<Float> second;
 82 
 83         if(subTrees.isEmpty()){
 84            return leaves.dequeue();
 85         }else{
 86             second = subTrees.first();
 87             if(getProb(first) > getProb(second))
 88                 return subTrees.dequeue();
 89             else
 90                 return leaves.dequeue();
 91         }
 92     }
 93 
 94     // create the whole huffman tree
 95     private void buildAll(){
 96         // Stage one
 97         while(!leaves.isEmpty()){
 98             BinaryTree<Float> first = getMin();
 99             BinaryTree<Float> second;
100             if(!leaves.isEmpty())
101                 second = getMin();
102             else
103                 second = subTrees.dequeue();
104             subTrees.enqueue(buildTree(first, second));
105         }
106         // Stage two
107         while (subTrees.size()>1){
108             subTrees.enqueue(buildTree(subTrees.dequeue(), subTrees.dequeue()));
109         }
110     }
111 
112     // return the height of this tree
113     private int treeHeight(BinaryTree<Float> tree){
114         if(tree==null)
115             return -1;
116         else
117             return Math.max(treeHeight(tree.left), treeHeight(tree.right)) + 1 ;
118     }
119 
120     // store each codes of total chars
121     private void storeCodes(){
122         char[] codeString = new char[treeHeight(huffmanTree)];
123 
124         // judge whether symbols.length equals 1
125         if(symbols.length==1) {
126             codes[0] = "0";
127             return;
128         }
129 
130         for(int i=0; i<symbols.length; i++){
131             BinaryTree<Float> node = locations.get(i);
132             int index = treeHeight(huffmanTree)-1;
133 
134             while(node.parent!=null){
135                 if(node.parent.left==node)
136                     codeString[index] = '0';
137 //                    codeString[index] = ((Integer) 0).toString().charAt(0);
138                 else
139                     codeString[index] = '1';
140                 index--;
141                 node = node.parent;
142             }
143             codes[i] = new String(codeString, index+1, codeString.length-1-index);
144 //            int codeLen =treeHeight(huffmanTree)-1-index;
145 //            char[] newArray = new char[codeLen];
146 //            System.arraycopy(codeString, 0, newArray, 0, codeLen);
147 //            codes[i] = newArray.toString();
148         }
149     }
150 
151     // encode the input String
152     public String encode(String originalString){
153         int len = originalString.length();
154         StringBuffer encodedString = new StringBuffer();
155 
156         for(int i=0; i<len; i++){   // encode char in order
157             char origin = originalString.charAt(i);
158 
159             for(int j=symbols.length-1; j >= 0; j--){
160                 if(symbols[j]==origin){
161                     encodedString.append(codes[j]);
162                     break;
163                 }
164             }
165         }
166         return encodedString.toString();
167     }
168 
169     // decode the input String
170     public String decode(String encodedCode){
171         StringBuffer originalString = new StringBuffer();
172         int pointer = 0;
173 
174         while(pointer<encodedCode.length()){    // Go through input code
175             BinaryTree<Float> node = huffmanTree;
176 
177             while(node.left!=null && node.right!=null) {    // Match the sub code
178                 char code = encodedCode.charAt(pointer);
179                 if (code == '0')
180                     node = node.left;
181                 else
182                     node = node.right;
183                 pointer++;
184             }
185             float fIndex = node.getData();
186             int index = (int)fIndex;
187             originalString.append(symbols[index]);
188         }
189 
190         return originalString.toString();
191     }
192 }

经测试,表现良好!

posted @ 2017-11-08 19:19  年华似水丶我如风  阅读(844)  评论(0编辑  收藏  举报