Huffman编码算法之Java实现

来源:互联网 发布:linux安装中文输入法 编辑:程序博客网 时间:2024/05/16 03:53

目录(?)[+]

Huffman编码介绍

Huffman编码处理的是字符以及字符对应的二进制的编码配对问题,分为编码和解码,目的是压缩字符对应的二进制数据长度。我们知道字符存贮和传输的时候都是二进制的(计算机只认识0/1),那么就有字符与二进制之间的mapping关系。字符属于字符集(Charset), 字符需要通过编码(encode)为二进制进行存贮和传输,显示的时候需要解码(decode)回字符,字符集与编码方法是一对多关系(Unicode可以用UTF-8,UTF-16等编码)。理解了字符集,编码以及解码,满天飞的乱码问题也就游刃而解了。以英文字母小写a为例, ASCII编码中,十进制为97,二进制为01100001。ASCII的每一个字符都用8个Bit(1Byte)编码,假如有1000个字符要传输,那么就要传输8000个Bit。问题来了,英文中字母e的使用频率为12.702%,而z为0.074%,前者是后者的100多倍,但是确使用相同位数的二进制。可以做得更好,方法就是可变长度编码,指导原则就是频率高的用较短的位数编码,频率低的用较长位数编码。Huffman编码算法就是处理这样的问题。

Huffman编码Java实现

Huffman编码算法主要用到的数据结构是完全二叉树(full binary tree)和优先级队列。后者用的是java.util.PriorityQueue,前者自己实现(都为内部类),代码如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. static class Tree {  
  2.         private Node root;  
  3.   
  4.         public Node getRoot() {  
  5.             return root;  
  6.         }  
  7.   
  8.         public void setRoot(Node root) {  
  9.             this.root = root;  
  10.         }  
  11.     }  
  12.   
  13.     static class Node implements Comparable<Node> {  
  14.         private String chars = "";  
  15.         private int frequence = 0;  
  16.         private Node parent;  
  17.         private Node leftNode;  
  18.         private Node rightNode;  
  19.   
  20.         @Override  
  21.         public int compareTo(Node n) {  
  22.             return frequence - n.frequence;  
  23.         }  
  24.   
  25.         public boolean isLeaf() {  
  26.             return chars.length() == 1;  
  27.         }  
  28.   
  29.         public boolean isRoot() {  
  30.             return parent == null;  
  31.         }  
  32.   
  33.         public boolean isLeftChild() {  
  34.             return parent != null && this == parent.leftNode;  
  35.         }  
  36.   
  37.         public int getFrequence() {  
  38.             return frequence;  
  39.         }  
  40.   
  41.         public void setFrequence(int frequence) {  
  42.             this.frequence = frequence;  
  43.         }  
  44.   
  45.         public String getChars() {  
  46.             return chars;  
  47.         }  
  48.   
  49.         public void setChars(String chars) {  
  50.             this.chars = chars;  
  51.         }  
  52.   
  53.         public Node getParent() {  
  54.             return parent;  
  55.         }  
  56.   
  57.         public void setParent(Node parent) {  
  58.             this.parent = parent;  
  59.         }  
  60.   
  61.         public Node getLeftNode() {  
  62.             return leftNode;  
  63.         }  
  64.   
  65.         public void setLeftNode(Node leftNode) {  
  66.             this.leftNode = leftNode;  
  67.         }  
  68.   
  69.         public Node getRightNode() {  
  70.             return rightNode;  
  71.         }  
  72.   
  73.         public void setRightNode(Node rightNode) {  
  74.             this.rightNode = rightNode;  
  75.         }  
  76.     }  

统计数据

既然要按频率来安排编码表,那么首先当然得获得频率的统计信息。我实现了一个方法处理这样的问题。如果已经有统计信息,那么转为Map<Character,Integer>即可。如果你得到的信息是百分比,乘以100或1000,或10000。总是可以转为整数。比如12.702%乘以1000为12702,Huffman编码只关心大小问题。统计方法实现如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public static Map<Character, Integer> statistics(char[] charArray) {  
  2.         Map<Character, Integer> map = new HashMap<Character, Integer>();  
  3.         for (char c : charArray) {  
  4.             Character character = new Character(c);  
  5.             if (map.containsKey(character)) {  
  6.                 map.put(character, map.get(character) + 1);  
  7.             } else {  
  8.                 map.put(character, 1);  
  9.             }  
  10.         }  
  11.   
  12.         return map;  
  13.     }  

构建树

构建树是Huffman编码算法的核心步骤。思想是把所有的字符挂到一颗完全二叉树的叶子节点,任何一个非页子节点的左节点出现频率不大于右节点。算法为把统计信息转为Node存放到一个优先级队列里面,每一次从队列里面弹出两个最小频率的节点,构建一个新的父Node(非叶子节点), 字符内容刚弹出来的两个节点字符内容之和,频率也是它们的和,最开始的弹出来的作为左子节点,后面一个作为右子节点,并且把刚构建的父节点放到队列里面。重复以上的动作N-1次,N为不同字符的个数(每一次队列里面个数减1)。结束以上步骤,队列里面剩一个节点,弹出作为树的根节点。代码如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. private static Tree buildTree(Map<Character, Integer> statistics,  
  2.             List<Node> leafs) {  
  3.         Character[] keys = statistics.keySet().toArray(new Character[0]);  
  4.   
  5.         PriorityQueue<Node> priorityQueue = new PriorityQueue<Node>();  
  6.         for (Character character : keys) {  
  7.             Node node = new Node();  
  8.             node.chars = character.toString();  
  9.             node.frequence = statistics.get(character);  
  10.             priorityQueue.add(node);  
  11.             leafs.add(node);  
  12.         }  
  13.   
  14.         int size = priorityQueue.size();  
  15.         for (int i = 1; i <= size - 1; i++) {  
  16.             Node node1 = priorityQueue.poll();  
  17.             Node node2 = priorityQueue.poll();  
  18.   
  19.             Node sumNode = new Node();  
  20.             sumNode.chars = node1.chars + node2.chars;  
  21.             sumNode.frequence = node1.frequence + node2.frequence;  
  22.   
  23.             sumNode.leftNode = node1;  
  24.             sumNode.rightNode = node2;  
  25.   
  26.             node1.parent = sumNode;  
  27.             node2.parent = sumNode;  
  28.   
  29.             priorityQueue.add(sumNode);  
  30.         }  
  31.   
  32.         Tree tree = new Tree();  
  33.         tree.root = priorityQueue.poll();  
  34.         return tree;  
  35.     }  

编码

某个字符对应的编码为,从该字符所在的叶子节点向上搜索,如果该字符节点是父节点的左节点,编码字符之前加0,反之如果是右节点,加1,直到根节点。只要获取了字符和二进制码之间的mapping关系,编码就非常简单。代码如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public static String encode(String originalStr,  
  2.             Map<Character, Integer> statistics) {  
  3.         if (originalStr == null || originalStr.equals("")) {  
  4.             return "";  
  5.         }  
  6.   
  7.         char[] charArray = originalStr.toCharArray();  
  8.         List<Node> leafNodes = new ArrayList<Node>();  
  9.         buildTree(statistics, leafNodes);  
  10.         Map<Character, String> encodInfo = buildEncodingInfo(leafNodes);  
  11.   
  12.         StringBuffer buffer = new StringBuffer();  
  13.         for (char c : charArray) {  
  14.             Character character = new Character(c);  
  15.             buffer.append(encodInfo.get(character));  
  16.         }  
  17.   
  18.         return buffer.toString();  
  19.     }  
[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. private static Map<Character, String> buildEncodingInfo(List<Node> leafNodes) {  
  2.         Map<Character, String> codewords = new HashMap<Character, String>();  
  3.         for (Node leafNode : leafNodes) {  
  4.             Character character = new Character(leafNode.getChars().charAt(0));  
  5.             String codeword = "";  
  6.             Node currentNode = leafNode;  
  7.   
  8.             do {  
  9.                 if (currentNode.isLeftChild()) {  
  10.                     codeword = "0" + codeword;  
  11.                 } else {  
  12.                     codeword = "1" + codeword;  
  13.                 }  
  14.   
  15.                 currentNode = currentNode.parent;  
  16.             } while (currentNode.parent != null);  
  17.   
  18.             codewords.put(character, codeword);  
  19.         }  
  20.   
  21.         return codewords;  
  22.     }  

解码

因为Huffman编码算法能够保证任何的二进制码都不会是另外一个码的前缀,解码非常简单,依次取出二进制的每一位,从树根向下搜索,1向右,0向左,到了叶子节点(命中),退回根节点继续重复以上动作。代码如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public static String decode(String binaryStr,  
  2.             Map<Character, Integer> statistics) {  
  3.         if (binaryStr == null || binaryStr.equals("")) {  
  4.             return "";  
  5.         }  
  6.   
  7.         char[] binaryCharArray = binaryStr.toCharArray();  
  8.         LinkedList<Character> binaryList = new LinkedList<Character>();  
  9.         int size = binaryCharArray.length;  
  10.         for (int i = 0; i < size; i++) {  
  11.             binaryList.addLast(new Character(binaryCharArray[i]));  
  12.         }  
  13.   
  14.         List<Node> leafNodes = new ArrayList<Node>();  
  15.         Tree tree = buildTree(statistics, leafNodes);  
  16.   
  17.         StringBuffer buffer = new StringBuffer();  
  18.   
  19.         while (binaryList.size() > 0) {  
  20.             Node node = tree.root;  
  21.   
  22.             do {  
  23.                 Character c = binaryList.removeFirst();  
  24.                 if (c.charValue() == '0') {  
  25.                     node = node.leftNode;  
  26.                 } else {  
  27.                     node = node.rightNode;  
  28.                 }  
  29.             } while (!node.isLeaf());  
  30.   
  31.             buffer.append(node.chars);  
  32.         }  
  33.   
  34.         return buffer.toString();  
  35.     }  

测试以及比较

以下测试Huffman编码的正确性(先编码,后解码,包括中文),以及Huffman编码与常见的字符编码的二进制字符串比较。代码如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public static void main(String[] args) {  
  2.         String oriStr = "Huffman codes compress data very effectively: savings of 20% to 90% are typical, "  
  3.                 + "depending on the characteristics of the data being compressed. 中华崛起";  
  4.         Map<Character, Integer> statistics = statistics(oriStr.toCharArray());  
  5.         String encodedBinariStr = encode(oriStr, statistics);  
  6.         String decodedStr = decode(encodedBinariStr, statistics);  
  7.   
  8.         System.out.println("Original sstring: " + oriStr);  
  9.         System.out.println("Huffman encoed binary string: " + encodedBinariStr);  
  10.         System.out.println("decoded string from binariy string: " + decodedStr);  
  11.   
  12.         System.out.println("binary string of UTF-8: "  
  13.                 + getStringOfByte(oriStr, Charset.forName("UTF-8")));  
  14.         System.out.println("binary string of UTF-16: "  
  15.                 + getStringOfByte(oriStr, Charset.forName("UTF-16")));  
  16.         System.out.println("binary string of US-ASCII: "  
  17.                 + getStringOfByte(oriStr, Charset.forName("US-ASCII")));  
  18.         System.out.println("binary string of GB2312: "  
  19.                 + getStringOfByte(oriStr, Charset.forName("GB2312")));  
  20.     }  
  21.   
  22.     public static String getStringOfByte(String str, Charset charset) {  
  23.         if (str == null || str.equals("")) {  
  24.             return "";  
  25.         }  
  26.   
  27.         byte[] byteArray = str.getBytes(charset);  
  28.         int size = byteArray.length;  
  29.         StringBuffer buffer = new StringBuffer();  
  30.         for (int i = 0; i < size; i++) {  
  31.             byte temp = byteArray[i];  
  32.             buffer.append(getStringOfByte(temp));  
  33.         }  
  34.   
  35.         return buffer.toString();  
  36.     }  
  37.   
  38.     public static String getStringOfByte(byte b) {  
  39.         StringBuffer buffer = new StringBuffer();  
  40.         for (int i = 7; i >= 0; i--) {  
  41.             byte temp = (byte) ((b >> i) & 0x1);  
  42.             buffer.append(String.valueOf(temp));  
  43.         }  
  44.   
  45.         return buffer.toString();  
  46.     } 
0 0