1. 基础概念

  1. 1.基本介绍
  2. 赫夫曼编码是变长(不定长)编码的一种。
  3. 将【字符出现频率】作为权重,权重越高的节点路劲长度越短
  4. 编码规则,从根点触发,通路分支向左为0,向右为1
  5. 压缩率在20%-90%
  6. 2.编码步骤
  7. 1.将文本中的字符各个封转为节点,以出现次数为权重
  8. 2.将节点构造哈夫曼树,叶子节点为数据节点
  9. 3.根据路径左01规则给数据节点编码,得到数据-编码的映射(编码表)
  10. 4.根据编码表对原始文本的byte数组进行编码,得到二进制编码字符串
  11. 5.将二进制字符串解析成byte数组(最后的编码,将其长度/原始文本bytes长度=压缩率)
  12. 3.解码步骤
  13. 1.byte数据,二进制字符串(8位)
  14. 2.将编码表逆向<byte,String> -> <String,byte>
  15. 3.同逆向编码表得到byte数组
  16. 4.直接将byte数组转成String

代码实现

  1. import com.alibaba.fastjson.JSONObject;
  2. import lombok.Getter;
  3. import lombok.Setter;
  4. import org.junit.Test;
  5. import javax.persistence.criteria.CriteriaBuilder;
  6. import java.io.*;
  7. import java.util.*;
  8. @Setter
  9. @Getter
  10. class HeffmanNode implements Comparable{
  11. // 字符的ascall码
  12. private Byte value;
  13. // 字符在文本中出现的次数(权重)
  14. private int weight;
  15. // 左子节点
  16. private HeffmanNode left;
  17. private HeffmanNode right;
  18. public HeffmanNode(Byte value, int weight) {
  19. this.value = value;
  20. this.weight = weight;
  21. }
  22. @Override
  23. public int compareTo(Object o) {
  24. return this.weight - ((HeffmanNode)o).weight;
  25. }
  26. }
  27. public class HeffmanEncodeTest {
  28. /**
  29. * NodeList (初始森林)
  30. * @param sourceBytes
  31. * @return
  32. */
  33. private List<HeffmanNode> getNodeList(byte[] sourceBytes){
  34. Map<Byte, Integer> countMap = new HashMap<>();
  35. // 统计出现频率
  36. for(byte b : sourceBytes) {
  37. Integer count = countMap.get(b);
  38. if(count==null){
  39. countMap.put(b,1);
  40. }else {
  41. countMap.put(b, count+1);
  42. }
  43. }
  44. // 构建NodeList
  45. List<HeffmanNode> nodes = new ArrayList<>();
  46. countMap.forEach((key,value)-> {
  47. HeffmanNode node = new HeffmanNode(key, value);
  48. nodes.add(node);
  49. });
  50. return nodes;
  51. }
  52. /**
  53. * 构建赫夫曼树(根节点)
  54. * @param nodes
  55. * @return
  56. */
  57. private HeffmanNode buildHeffmantree(List<HeffmanNode> nodes) {
  58. while (nodes.size()>1) {
  59. Collections.sort(nodes);
  60. HeffmanNode min1 = nodes.get(0);
  61. HeffmanNode min2 = nodes.get(1);
  62. HeffmanNode newNode = new HeffmanNode(null, min1.getWeight() + min2.getWeight());
  63. newNode.setLeft(min1);
  64. newNode.setRight(min2);
  65. nodes.remove(min1);
  66. nodes.remove(min2);
  67. nodes.add(newNode);
  68. }
  69. return nodes.get(0);
  70. }
  71. /**
  72. * 根据赫夫曼树建立编码表
  73. * 编码顺序:从上往下
  74. * 编码规则:左 0 右 1
  75. * @param root
  76. * @return
  77. */
  78. private Map<Byte,String> getCodeTable(HeffmanNode root) {
  79. if(root==null){
  80. return null;
  81. }
  82. //编码表
  83. StringBuilder sb = new StringBuilder();
  84. Map<Byte,String> codeMap = new HashMap<>();
  85. // 处理左节点
  86. getCodeStr(root.getLeft(), "0",sb,codeMap);
  87. // 处理右边节点
  88. getCodeStr(root.getRight(), "1",sb,codeMap);
  89. return codeMap;
  90. }
  91. /**
  92. * 功能:
  93. * 细节:因为该方法有递归,所有编码表定义为成员变量
  94. * @param node: 当前待处理的节点
  95. * @param subPath: 当前需要添加的路径(左0右1)
  96. * @param sb : 前边非叶子节点走下来的路径
  97. */
  98. private void getCodeStr(HeffmanNode node,String subPath, StringBuilder sb,Map<Byte,String> codeMap) {
  99. // 先拼上前面的路径(这里一定要照sb2,否则会对右边的路径有影响)
  100. StringBuilder sb2 = new StringBuilder(sb);
  101. sb2.append(subPath);
  102. // 非叶子节点,继续往后走
  103. if(node.getValue() == null) {
  104. // 左0 ,右1
  105. if(node.getLeft()!=null) {
  106. getCodeStr(node.getLeft(),"0",sb2, codeMap);
  107. }
  108. if(node.getRight()!=null) {
  109. getCodeStr(node.getRight(),"1",sb2, codeMap);
  110. }
  111. }
  112. // 将叶子节点的编码放入编码表
  113. else {
  114. codeMap.put(node.getValue(),sb2.toString());
  115. }
  116. }
  117. /**
  118. * 根据编码表,通过ASCALL码Byte获取编码字符串
  119. */
  120. private StringBuilder getEncodeStringByCodeTable(byte[] sourceBytes, Map<Byte,String> codeMap) {
  121. StringBuilder sb = new StringBuilder();
  122. for(byte b : sourceBytes) {
  123. sb.append(codeMap.get(b));
  124. }
  125. return sb;
  126. }
  127. /**
  128. * 将编码字符串作为二进制补码生成byte[],
  129. * 一个byte字节,8bit
  130. */
  131. private byte[] getHeffmanEncodeBytesByEncodeString(StringBuilder encodeStringBuilder) {
  132. // 计算byte长度
  133. // 下面的计算过程可以简写: int len = (encodeStringBuilder.length() + 7)/ 8+1 ;
  134. int len = 0;
  135. if(encodeStringBuilder.length()%8==0){
  136. len = encodeStringBuilder.length()/8;
  137. }else{
  138. len = encodeStringBuilder.length()/8+1;
  139. }
  140. byte[] bytes = new byte[len];
  141. int j = 0;
  142. for(int i=0; i<encodeStringBuilder.length(); i+=8) {
  143. String bitStr = null;
  144. // 截取的最后端可能不满8位
  145. if(i+8 >= encodeStringBuilder.length()) {
  146. bitStr = encodeStringBuilder.substring(i);
  147. }else {
  148. bitStr = encodeStringBuilder.substring(i,i+8);
  149. }
  150. byte bit = (byte)Integer.parseInt(bitStr, 2);
  151. bytes[j] = bit;
  152. j++;
  153. }
  154. return bytes;
  155. }
  156. /**
  157. * 根据被编码数据动态创建编码表
  158. * @param source 被编码数据
  159. * @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}
  160. */
  161. public Map<Byte,String>buildEncodeTable(String source) {
  162. // 1.获得原始森林
  163. List<HeffmanNode> nodes = getNodeList(source.getBytes());
  164. // 2. 根据原始森林构建赫夫曼树
  165. HeffmanNode root = buildHeffmantree(nodes);
  166. // 3. 由于赫夫曼树构建编码表
  167. return getCodeTable(root);
  168. }
  169. /**
  170. * 编码
  171. * @param source: 被编码的原数据
  172. */
  173. public byte[] encode(String source,Map<Byte,String> encodeTable){
  174. // 4. 根据编码表,对元数据进行编码
  175. StringBuilder sb = getEncodeStringByCodeTable(source.getBytes(),encodeTable);
  176. // 5. 为了减小数据长度,将编码后的字符串"100111....",放入HeffmanEncodeByte[], 一个字节八位,
  177. byte[] targetBytes = getHeffmanEncodeBytesByEncodeString(sb);
  178. return targetBytes;
  179. }
  180. ///////////////////////////解码////////////////////////////////////
  181. /**
  182. * 将一个byte 转成一个二进制的字符串, 如果看不懂,可以参考我讲的Java基础 二进制的原码,反码,补码
  183. * @param b 传入的 byte
  184. * @param flag 标志是否需要补高位如果是true ,表示需要补高位,如果是false表示不补, 如果是最后一个字节,无需补高位
  185. * @return 是该b 对应的二进制的字符串,(注意是按补码返回)
  186. */
  187. private static String byteToBitString(boolean flag, byte b) {
  188. //使用变量保存 b
  189. int temp = b; //将 b 转成 int
  190. //如果是正数我们还存在补高位
  191. if(flag) {
  192. temp |= 256; //按位与 256 1 0000 0000 | 0000 0001 => 1 0000 0001
  193. }
  194. String str = Integer.toBinaryString(temp); //返回的是temp对应的二进制的补码
  195. if(flag) {
  196. return str.substring(str.length() - 8);
  197. } else {
  198. return str;
  199. }
  200. }
  201. //完成数据的解压
  202. //思路
  203. //1. 将huffmanCodeBytes [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
  204. // 重写先转成 赫夫曼编码对应的二进制的字符串 "1010100010111..."
  205. //2. 赫夫曼编码对应的二进制的字符串 "1010100010111..." =》 对照 赫夫曼编码 =》 "i like like like java do you like a java"
  206. //编写一个方法,完成对压缩数据的解码
  207. /**
  208. *
  209. * @param encodeTable 赫夫曼编码表 map
  210. * @param huffmanBytes 赫夫曼编码得到的字节数组
  211. * @return 就是原来的字符串对应的数组
  212. */
  213. private static byte[] decode(byte[] huffmanBytes, Map<Byte,String> encodeTable) {
  214. //1. 先得到 huffmanBytes 对应的 二进制的字符串 , 形式 1010100010111...
  215. StringBuilder stringBuilder = new StringBuilder();
  216. //将byte数组转成二进制的字符串
  217. for(int i = 0; i < huffmanBytes.length; i++) {
  218. byte b = huffmanBytes[i];
  219. //判断是不是最后一个字节
  220. boolean flag = (i == huffmanBytes.length - 1);
  221. stringBuilder.append(byteToBitString(!flag, b));
  222. }
  223. //2. 把赫夫曼编码表进行调换,因为反向查询 a->100 100->a
  224. Map<String, Byte> map = new HashMap<String,Byte>();
  225. for(Map.Entry<Byte, String> entry: encodeTable.entrySet()) {
  226. map.put(entry.getValue(), entry.getKey());
  227. }
  228. //创建要给集合,存放byte
  229. List<Byte> list = new ArrayList<>();
  230. //i 可以理解成就是索引,扫描 stringBuilder
  231. for(int i = 0; i < stringBuilder.length(); ) {
  232. int count = 1; // 小的计数器
  233. boolean flag = true;
  234. Byte b = null;
  235. while(flag) {
  236. //1010100010111...
  237. //递增的取出 key 1
  238. String key = stringBuilder.substring(i, i+count);//i 不动,让count移动,指定匹配到一个字符
  239. b = map.get(key);
  240. if(b == null) {//说明没有匹配到
  241. count++;
  242. }else {
  243. //匹配到
  244. flag = false;
  245. }
  246. }
  247. list.add(b);
  248. i += count;//i 直接移动到 count
  249. }
  250. //当for循环结束后,我们list中就存放了所有的字符 "i like like like java do you like a java"
  251. //把list 中的数据放入到byte[] 并返回
  252. byte b[] = new byte[list.size()];
  253. for(int i = 0;i < b.length; i++) {
  254. b[i] = list.get(i);
  255. }
  256. return b;
  257. }
  258. /**
  259. * 测试解压
  260. */
  261. @Test
  262. public void test() {
  263. // 被编码数据
  264. String source = "i like like like java do you like a java";
  265. // 动态创建编码表解码
  266. Map<Byte, String> encodeTable = buildEncodeTable(source);
  267. // 编码
  268. byte[] encodeBytes = encode(source, encodeTable);
  269. // 解码
  270. byte[] retByte = decode(encodeBytes,encodeTable);
  271. System.out.println("decodeStr:" + new String(retByte));
  272. }
  273. }