1. 基础概念
1.基本介绍 赫夫曼编码是变长(不定长)编码的一种。 将【字符出现频率】作为权重,权重越高的节点路劲长度越短 编码规则,从根点触发,通路分支向左为0,向右为1 压缩率在20%-90%2.编码步骤 1.将文本中的字符各个封转为节点,以出现次数为权重 2.将节点构造哈夫曼树,叶子节点为数据节点 3.根据路径左0右1规则给数据节点编码,得到数据-编码的映射(编码表) 4.根据编码表对原始文本的byte数组进行编码,得到二进制编码字符串 5.将二进制字符串解析成byte数组(最后的编码,将其长度/原始文本bytes长度=压缩率)3.解码步骤 1.将byte数据,二进制字符串(8位) 2.将编码表逆向<byte,String> -> <String,byte> 3.同逆向编码表得到byte数组 4.直接将byte数组转成String
代码实现
import com.alibaba.fastjson.JSONObject;import lombok.Getter;import lombok.Setter;import org.junit.Test;import javax.persistence.criteria.CriteriaBuilder;import java.io.*;import java.util.*;@Setter@Getterclass HeffmanNode implements Comparable{ // 字符的ascall码 private Byte value; // 字符在文本中出现的次数(权重) private int weight; // 左子节点 private HeffmanNode left; private HeffmanNode right; public HeffmanNode(Byte value, int weight) { this.value = value; this.weight = weight; } @Override public int compareTo(Object o) { return this.weight - ((HeffmanNode)o).weight; }}public class HeffmanEncodeTest { /** * NodeList (初始森林) * @param sourceBytes * @return */ private List<HeffmanNode> getNodeList(byte[] sourceBytes){ Map<Byte, Integer> countMap = new HashMap<>(); // 统计出现频率 for(byte b : sourceBytes) { Integer count = countMap.get(b); if(count==null){ countMap.put(b,1); }else { countMap.put(b, count+1); } } // 构建NodeList List<HeffmanNode> nodes = new ArrayList<>(); countMap.forEach((key,value)-> { HeffmanNode node = new HeffmanNode(key, value); nodes.add(node); }); return nodes; } /** * 构建赫夫曼树(根节点) * @param nodes * @return */ private HeffmanNode buildHeffmantree(List<HeffmanNode> nodes) { while (nodes.size()>1) { Collections.sort(nodes); HeffmanNode min1 = nodes.get(0); HeffmanNode min2 = nodes.get(1); HeffmanNode newNode = new HeffmanNode(null, min1.getWeight() + min2.getWeight()); newNode.setLeft(min1); newNode.setRight(min2); nodes.remove(min1); nodes.remove(min2); nodes.add(newNode); } return nodes.get(0); } /** * 根据赫夫曼树建立编码表 * 编码顺序:从上往下 * 编码规则:左 0 右 1 * @param root * @return */ private Map<Byte,String> getCodeTable(HeffmanNode root) { if(root==null){ return null; } //编码表 StringBuilder sb = new StringBuilder(); Map<Byte,String> codeMap = new HashMap<>(); // 处理左节点 getCodeStr(root.getLeft(), "0",sb,codeMap); // 处理右边节点 getCodeStr(root.getRight(), "1",sb,codeMap); return codeMap; } /** * 功能: * 细节:因为该方法有递归,所有编码表定义为成员变量 * @param node: 当前待处理的节点 * @param subPath: 当前需要添加的路径(左0右1) * @param sb : 前边非叶子节点走下来的路径 */ private void getCodeStr(HeffmanNode node,String subPath, StringBuilder sb,Map<Byte,String> codeMap) { // 先拼上前面的路径(这里一定要照sb2,否则会对右边的路径有影响) StringBuilder sb2 = new StringBuilder(sb); sb2.append(subPath); // 非叶子节点,继续往后走 if(node.getValue() == null) { // 左0 ,右1 if(node.getLeft()!=null) { getCodeStr(node.getLeft(),"0",sb2, codeMap); } if(node.getRight()!=null) { getCodeStr(node.getRight(),"1",sb2, codeMap); } } // 将叶子节点的编码放入编码表 else { codeMap.put(node.getValue(),sb2.toString()); } } /** * 根据编码表,通过ASCALL码Byte获取编码字符串 */ private StringBuilder getEncodeStringByCodeTable(byte[] sourceBytes, Map<Byte,String> codeMap) { StringBuilder sb = new StringBuilder(); for(byte b : sourceBytes) { sb.append(codeMap.get(b)); } return sb; } /** * 将编码字符串作为二进制补码生成byte[], * 一个byte字节,8bit */ private byte[] getHeffmanEncodeBytesByEncodeString(StringBuilder encodeStringBuilder) { // 计算byte长度 // 下面的计算过程可以简写: int len = (encodeStringBuilder.length() + 7)/ 8+1 ; int len = 0; if(encodeStringBuilder.length()%8==0){ len = encodeStringBuilder.length()/8; }else{ len = encodeStringBuilder.length()/8+1; } byte[] bytes = new byte[len]; int j = 0; for(int i=0; i<encodeStringBuilder.length(); i+=8) { String bitStr = null; // 截取的最后端可能不满8位 if(i+8 >= encodeStringBuilder.length()) { bitStr = encodeStringBuilder.substring(i); }else { bitStr = encodeStringBuilder.substring(i,i+8); } byte bit = (byte)Integer.parseInt(bitStr, 2); bytes[j] = bit; j++; } return bytes; } /** * 根据被编码数据动态创建编码表 * @param source 被编码数据 * @return: 生成的赫夫曼编码表{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011} */ public Map<Byte,String>buildEncodeTable(String source) { // 1.获得原始森林 List<HeffmanNode> nodes = getNodeList(source.getBytes()); // 2. 根据原始森林构建赫夫曼树 HeffmanNode root = buildHeffmantree(nodes); // 3. 由于赫夫曼树构建编码表 return getCodeTable(root); } /** * 编码 * @param source: 被编码的原数据 */ public byte[] encode(String source,Map<Byte,String> encodeTable){ // 4. 根据编码表,对元数据进行编码 StringBuilder sb = getEncodeStringByCodeTable(source.getBytes(),encodeTable); // 5. 为了减小数据长度,将编码后的字符串"100111....",放入HeffmanEncodeByte[], 一个字节八位, byte[] targetBytes = getHeffmanEncodeBytesByEncodeString(sb); return targetBytes; } ///////////////////////////解码//////////////////////////////////// /** * 将一个byte 转成一个二进制的字符串, 如果看不懂,可以参考我讲的Java基础 二进制的原码,反码,补码 * @param b 传入的 byte * @param flag 标志是否需要补高位如果是true ,表示需要补高位,如果是false表示不补, 如果是最后一个字节,无需补高位 * @return 是该b 对应的二进制的字符串,(注意是按补码返回) */ private static String byteToBitString(boolean flag, byte b) { //使用变量保存 b int temp = b; //将 b 转成 int //如果是正数我们还存在补高位 if(flag) { temp |= 256; //按位与 256 1 0000 0000 | 0000 0001 => 1 0000 0001 } String str = Integer.toBinaryString(temp); //返回的是temp对应的二进制的补码 if(flag) { return str.substring(str.length() - 8); } else { return str; } } //完成数据的解压 //思路 //1. 将huffmanCodeBytes [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28] // 重写先转成 赫夫曼编码对应的二进制的字符串 "1010100010111..." //2. 赫夫曼编码对应的二进制的字符串 "1010100010111..." =》 对照 赫夫曼编码 =》 "i like like like java do you like a java" //编写一个方法,完成对压缩数据的解码 /** * * @param encodeTable 赫夫曼编码表 map * @param huffmanBytes 赫夫曼编码得到的字节数组 * @return 就是原来的字符串对应的数组 */ private static byte[] decode(byte[] huffmanBytes, Map<Byte,String> encodeTable) { //1. 先得到 huffmanBytes 对应的 二进制的字符串 , 形式 1010100010111... StringBuilder stringBuilder = new StringBuilder(); //将byte数组转成二进制的字符串 for(int i = 0; i < huffmanBytes.length; i++) { byte b = huffmanBytes[i]; //判断是不是最后一个字节 boolean flag = (i == huffmanBytes.length - 1); stringBuilder.append(byteToBitString(!flag, b)); } //2. 把赫夫曼编码表进行调换,因为反向查询 a->100 100->a Map<String, Byte> map = new HashMap<String,Byte>(); for(Map.Entry<Byte, String> entry: encodeTable.entrySet()) { map.put(entry.getValue(), entry.getKey()); } //创建要给集合,存放byte List<Byte> list = new ArrayList<>(); //i 可以理解成就是索引,扫描 stringBuilder for(int i = 0; i < stringBuilder.length(); ) { int count = 1; // 小的计数器 boolean flag = true; Byte b = null; while(flag) { //1010100010111... //递增的取出 key 1 String key = stringBuilder.substring(i, i+count);//i 不动,让count移动,指定匹配到一个字符 b = map.get(key); if(b == null) {//说明没有匹配到 count++; }else { //匹配到 flag = false; } } list.add(b); i += count;//i 直接移动到 count } //当for循环结束后,我们list中就存放了所有的字符 "i like like like java do you like a java" //把list 中的数据放入到byte[] 并返回 byte b[] = new byte[list.size()]; for(int i = 0;i < b.length; i++) { b[i] = list.get(i); } return b; } /** * 测试解压 */ @Test public void test() { // 被编码数据 String source = "i like like like java do you like a java"; // 动态创建编码表解码 Map<Byte, String> encodeTable = buildEncodeTable(source); // 编码 byte[] encodeBytes = encode(source, encodeTable); // 解码 byte[] retByte = decode(encodeBytes,encodeTable); System.out.println("decodeStr:" + new String(retByte)); }}