数据结构019_数据压缩(赫夫曼编码)
一、基本介绍
- 赫夫曼编码也翻译为哈夫曼编码(HuffmanCoding),是一种编码方式,也是一种程序算法。
- 赫夫曼编码是赫夫曼树在电讯通信中的经典应用之一。
- 赫夫曼编码也广泛用于文件压缩。其压缩率通常在20%~90%之间。
- 赫夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法称为最佳编码。
二、原理剖析
通信领域中信息处理的方式
1)定长编码
2)变长编码:按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小。
比如:could you find a way to let me down slowly,比如空格出现9次,编码为0,其他依次类推。0、1、10、11、100、101........
3)赫夫曼编码
按照字符出现的次数构建一棵赫夫曼树,次数作为权值。根据赫夫曼树规定编码,向左的路径规定为0,向右为1。赫夫曼编码满足前缀编码。
字符的编码都不能是其他字符编码的前缀。符合此要求的编码叫做前缀编码,即不能匹配到重复的编码。(就是比如1是10,11的前缀,这样就可能存在二义性 )
注意:这个赫夫曼树根据排序方法不同,这样对应的赫夫曼编码也不完全一样。但WPL是一样的,最后生成的赫夫曼编码长度也是一样的。
三、实例
对字符串"i like like like java do you like a java"用赫夫曼编码进行数据压缩处理。(我还是要用老师这个辣鸡字符串,些出来也好对一下运行结果)
步骤:
1.创建字符串对应赫夫曼树
2.赫夫曼树生成赫夫曼编码
3.数据压缩-赫夫曼编码字节数据
4.方法封装-赫夫曼字节数组封装
【完整代码过阵子再拿出来码一遍!】
步骤一生成赫夫曼树代码(想看完整代码看步骤二):
package com.njcx.huffman; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.xml.ws.EndpointReference; import org.w3c.dom.traversal.NodeIterator; public class HuffmanCode { // 1.创建字符串对应的赫夫曼树 // 思路:(1)Node{data(存放数据),weight(权值),left,right} // (2)得到字符串对应的bete[]数组 // (3)编写一个方法将准备构建赫夫曼树的Node节点放到list中, // 形式[Node[data=97,weight=5],node[]ata=32,weight=9...] // 体现d:1,y:1,u:1,...... // (4)可以通过list创建对应的赫夫曼树 public static void main(String[] args) { String content = "i like like like java do you like a java"; byte[] contentBytes = content.getBytes(); System.out.println(contentBytes.length);// 40 List<Node> nodes = getNodes(contentBytes); System.out.println("nodes=" + nodes); // 测试一把创建的二叉树 System.out.println("赫夫曼树:"); Node huffmanTree = createHuffmanTree(nodes); huffmanTree.preOrder(); } public static void preOrder(Node root) { if (root != null) root.preOrder(); else System.out.println("空树"); } /** * * @param bytes * 接收一个字符数组 * @return 返回的就是List,形式的Node[data='97',weight=5] */ private static List<Node> getNodes(byte[] bytes) { // 创建一个arrayList ArrayList<Node> nodes = new ArrayList<Node>(); // 存储每个byte出现的次数,用map存储,比较巧妙 // 遍历bytes,统计每个byte出现的的次数,map[key,value] Map<Byte, Integer> counts = new HashMap<>(); for (byte b : bytes) { Integer count = counts.get(b);// map.get(key)返回这个key对应的value if (count == null) { // map里还没有这个字符数据 counts.put(b, 1); } else { counts.put(b, count + 1);// 我工作中遇到过这种,那时候我使用的是list } } // 把每个键值对转成一个Node对象,并加入到nodes集合 // 遍历map【这里我比较陌生,在后面一篇博客里会单独做一下这个的笔记】 for (Map.Entry<Byte, Integer> entry : counts.entrySet()) { nodes.add(new Node(entry.getKey(), entry.getValue())); } return nodes; } // 通过list创建对应的赫夫曼树 private static Node createHuffmanTree(List<Node> nodes) { while (nodes.size() > 1) { // 排序 从小到大 Collections.sort(nodes); // 取出两棵最小的二叉树 Node leftNode = nodes.get(0); Node rightNode = nodes.get(1); // 创建一个新的二叉树,没有data只有权值 Node parent = new Node(null, leftNode.weight + rightNode.weight); parent.left = leftNode; parent.right = rightNode; // 移除 nodes.remove(leftNode); nodes.remove(rightNode); // 加入 nodes.add(parent); } return nodes.get(0); // 最后只有一个节点,哈夫曼树的根结点 } } // Node类,带数据和权值 class Node implements Comparable<Node> { Byte data;// 存放数据本身,比如'a'的data97 int weight;// 权值,表示字符出现的次数 Node left; Node right; // 这里data是Byte,可以是null,如果是byte,就不可以是null了 public Node(Byte data, int weight) { this.data = data; this.weight = weight; } @Override public int compareTo(Node o) { // 从小到大排序 这里又忘了,当前值小于o的值返回的是负数 return this.weight - o.weight; } @Override public String toString() { return "Node [data=" + data + ", weight=" + weight + "]"; } // 前序遍历 public void preOrder() { System.out.println(this); if (this.left != null) this.left.preOrder(); if (this.right != null) this.right.preOrder(); } }
运行结果:
40 nodes=[Node [data=32, weight=9], Node [data=97, weight=5], Node [data=100, weight=1], Node [data=101, weight=4], Node [data=117, weight=1], Node [data=118, weight=2], Node [data=105, weight=5], Node [data=121, weight=1], Node [data=106, weight=2], Node [data=107, weight=4], Node [data=108, weight=4], Node [data=111, weight=2]] 赫夫曼树: Node [data=null, weight=40] Node [data=null, weight=17] Node [data=null, weight=8] Node [data=108, weight=4] Node [data=null, weight=4] Node [data=106, weight=2] Node [data=111, weight=2] Node [data=32, weight=9] Node [data=null, weight=23] Node [data=null, weight=10] Node [data=97, weight=5] Node [data=105, weight=5] Node [data=null, weight=13] Node [data=null, weight=5] Node [data=null, weight=2] Node [data=100, weight=1] Node [data=117, weight=1] Node [data=null, weight=3] Node [data=121, weight=1] Node [data=118, weight=2] Node [data=null, weight=8] Node [data=101, weight=4] Node [data=107, weight=4]
步骤二:(生成赫夫曼编码、使用赫夫曼编码生成赫夫曼编码数据[包含赫夫曼编码字节数组,赫夫曼编码字节数组封装])
这里我就有点懵了,每一步都知道,但是代码太多了,那种全局概念非常模糊,就好像走迷宫的人只知道当前的路。
package com.njcx.huffman; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.xml.ws.EndpointReference; import org.w3c.dom.traversal.NodeIterator; public class HuffmanCode { // 1.创建字符串对应的赫夫曼树 // 思路:(1)Node{data(存放数据),weight(权值),left,right} // (2)得到字符串对应的bete[]数组 // (3)编写一个方法将准备构建赫夫曼树的Node节点放到list中, // 形式[Node[data=97,weight=5],node[]ata=32,weight=9...] // 体现d:1,y:1,u:1,...... // (4)可以通过list创建对应的赫夫曼树 public static void main(String[] args) { String content = "i like like like java do you like a java"; byte[] contentBytes = content.getBytes(); System.out.println(contentBytes.length);// 40 /* * 下面的代码都是分布过程,把这些封装到下面的方法中这里就不要了 * * List<Node> nodes = getNodes(contentBytes); * System.out.println("nodes=" + nodes); // 测试一把创建的二叉树 * System.out.println("赫夫曼树:"); Node huffmanTreeRoot = * createHuffmanTree(nodes); huffmanTreeRoot.preOrder(); * * // // 测试一把是否生成了对应的赫夫曼编码 // getCodes(huffmanTreeRoot, "", sb); // * System.out.println("生成的赫夫曼编码表" + huffmanCodes); // // * 运行结果:生成的赫夫曼编码表{32=01, 97=100, 100=11000, 117=11001, 101=1110, // // * 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, // * 111=0011} * * // 使用重载过的方法测试是否生成了对应的赫夫曼编码 Map<Byte, String> huffmancodes = * getCodes(huffmanTreeRoot); System.out.println("生成的赫夫曼编码表" + * huffmancodes); * * // 测试 byte[] huffmanCodesByte = zip(contentBytes, huffmancodes); * System.out.println("huffmanCodesByte=" + * Arrays.toString(huffmanCodesByte));// 17 // 发送huffmanCodeBytes 数组 * */ byte[] huffmanCodesBytes = huffmanZip(contentBytes); System.out.println("压缩后的结果是:" + Arrays.toString(huffmanCodesBytes)); System.out.println("长度=" + huffmanCodesBytes.length); } /** * 使用一个方法将前面的方法封装,便于调用. * * @param bytes * 原始的字符串对应的字节数组 * @return 返回的是经过和和赫夫曼编码处理后的字节数组(压缩后的数组) */ private static byte[] huffmanZip(byte[] bytes) { // 第一步 List<Node> nodes = getNodes(bytes); // 第二步,根据nodes创建赫夫曼树 Node huffmanTreeRoot = createHuffmanTree(nodes); // 第三步,根据赫夫曼树生成对应的赫夫曼编码 Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot); // 第四步,根据生成的赫夫曼编码得到压缩后的赫夫曼编码字节数组 byte[] huffmanCodeBytes = zip(bytes, huffmanCodes); return huffmanCodeBytes; } /** * 接收一个字符数组返回一个list * * @param bytes * 接收一个字符数组 * @return 返回的就是List,形式的Node[data='97',weight=5] */ private static List<Node> getNodes(byte[] bytes) { // 创建一个arrayList ArrayList<Node> nodes = new ArrayList<Node>(); // 存储每个byte出现的次数,用map存储,比较巧妙 // 遍历bytes,统计每个byte出现的的次数,map[key,value] Map<Byte, Integer> counts = new HashMap<>(); for (byte b : bytes) { Integer count = counts.get(b);// map.get(key)返回这个key对应的value if (count == null) { // map里还没有这个字符数据 counts.put(b, 1); } else { counts.put(b, count + 1);// 我工作中遇到过这种,那时候我使用的是list } } // 把每个键值对转成一个Node对象,并加入到nodes集合 // 遍历map【这里我比较陌生,在后面一篇博客里会单独做一下这个的笔记】 for (Map.Entry<Byte, Integer> entry : counts.entrySet()) { nodes.add(new Node(entry.getKey(), entry.getValue())); } return nodes; } /** * 通过list创建对应的赫夫曼树 * * @param nodes * @return */ private static Node createHuffmanTree(List<Node> nodes) { while (nodes.size() > 1) { // 排序 从小到大 Collections.sort(nodes); // 取出两棵最小的二叉树 Node leftNode = nodes.get(0); Node rightNode = nodes.get(1); // 创建一个新的二叉树,没有data只有权值 Node parent = new Node(null, leftNode.weight + rightNode.weight); parent.left = leftNode; parent.right = rightNode; // 移除 nodes.remove(leftNode); nodes.remove(rightNode); // 加入 nodes.add(parent); } return nodes.get(0); // 最后只有一个节点,哈夫曼树的根结点 } // 生成赫夫曼对应的赫夫曼编码 // 思路:1.将赫夫曼编码表存放在Map<Byte,String> // 形式大概是 32->01,97->100,100->11000, static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>(); // 2. 在生成赫夫曼编码表时需要不停地拼接路径,定义一个StringBuilder存储某个叶子节点的路径 static StringBuilder sb = new StringBuilder(); /** * 功能:将传入的node节点的所有叶子节点的赫夫曼编码得到,并放入到huffmanCodes集合中 * * @param node * 传入的节点 * @param code * 路径,左子节点是0,右子节点是1 * @param sb * 用于拼接路径 */ private static void getCodes(Node node, String code, StringBuilder sb) { StringBuilder sb2 = new StringBuilder(sb); // 将code加入到sb2 sb2.append(code); if (node != null) {// 如果node==null不处理 // 判断当前node是叶子节点还是非叶子节点 if (node.data == null) {// 非叶子节点 // 递归处理 // 向左递归 getCodes(node.left, "0", sb2); // 向右递归 getCodes(node.right, "1", sb2); } else { // 叶子节点 // 表示找到了某个叶叶子节点的路径 huffmanCodes.put(node.data, sb2.toString()); } } } /** * 功能:为了调用方便,重载getCodes方法 * * @param root * @return */ private static Map<Byte, String> getCodes(Node root) { if (root == null) return null; getCodes(root.left, "0", sb); getCodes(root.right, "1", sb); return huffmanCodes; } /** * 将字符串对应的byte[]数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码压缩后的byte数组 * * @param bytes * 原始的字符串对应的byte[] * @param huffmanCodes * 生成的赫夫曼编码map * @return 返回赫夫曼编码处理后的一个byte[] 举例:String content * =101010001011111111001000101111111100100010111111110010010100110111 * +0001110000011011101000111100101000101111111100110001001010011011100 * "i like like like java do you like a java" =>byte[] contentBytes * = content.getByte[] 返回的是字符串对应的byte[],字符串是: =>对应的byte[] * huffmanCodeBytes,即8位对应一个byte,放入huffmanCodeBytes[] * huffmanCodeBytes[0]=10101000(补码)=>byte[推导10101000(补码)=>10100111( * 反码)=>11011000(原码)=>-88] */ private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) { // 先利用赫夫曼编码表huffmanCodes将bytes转成赫夫曼编码对应的字符串 StringBuilder sb = new StringBuilder(); // 遍历bytes数组 for (byte b : bytes) { sb.append(huffmanCodes.get(b)); } // System.out.println("测试stringbuilder=" + sb.toString()); // 将stringbuilder对应的字符串转成byte[] // 统计返回的byte[] huffmanCodeBytes长度 // int len; // if(sb.length()%8==0){ // len = sb.length()/8; // }else{ // len=sb.length()/8+1; // } // 上面的六行可以用一句话写 // int len = sb.length() % 8 == 0 ? sb.length() / 8 : sb.length() / 8 + // 1; // 也不是这个意思,太投机了,是真的用另一种方式的一句话: int len = (sb.length() + 7) / 8;// 【*】 // 创建一个存储压缩后的byte[] byte[] huffmanCodeBytes = new byte[len]; int index = 0;// 纪录是第几个byte // 步长为8,因为时每8位对应一个byte,所以步长是8 for (int i = 0; i < sb.length(); i += 8) { String strByte; if (i + 8 > sb.length()) { // 不够八位 strByte = sb.substring(i); } else { strByte = sb.substring(i, i + 8); } // 将strByte转成一个byte,放入到huffmanCodeBytes中 huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);// 【*】 index++; } return huffmanCodeBytes; } public static void preOrder(Node root) { if (root != null) root.preOrder(); else System.out.println("空树"); } } // Node类,带数据和权值 class Node implements Comparable<Node> { Byte data;// 存放数据本身,比如'a'的data97 int weight;// 权值,表示字符出现的次数 Node left; Node right; // 这里data是Byte,可以是null,如果是byte,就不可以是null了 public Node(Byte data, int weight) { this.data = data; this.weight = weight; } @Override public int compareTo(Node o) { // 从小到大排序 这里又忘了,当前值小于o的值返回的是负数 return this.weight - o.weight; } @Override public String toString() { return "Node [data=" + data + ", weight=" + weight + "]"; } // 前序遍历 public void preOrder() { System.out.println(this); if (this.left != null) this.left.preOrder(); if (this.right != null) this.right.preOrder(); } }
运行结果:
40 压缩后的结果是:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28] 长度=17