哈夫曼树(赫夫曼树):给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(WPL)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树。

哈夫曼树 - 图1

路径:在一棵树中,从一个结点到另一个结点所经过的所有结点,被我们称为两个结点之间的路径。
路径长度:在一棵树中,从一个结点到另一个结点所经过的“边”的数量,被我们称为两个结点之间的路径长度。
结点的带权路径长度:树的每一个结点,都可以拥有自己的“权重”(Weight),权重在不同的算法当中可以起到不同的作用。结点的带权路径长度,是指树的根结点到该结点的路径长度和该结点权重的乘积。
树的带权路径长度:在一棵树中,所有叶子结点的带权路径长度之和,被称为树的带权路径长度,也被简称为WPL。

哈夫曼树构建步骤:
1、构建森林:把每一个叶子结点,都当做树一颗独立的树(只有根结点的树),这样就形成了一个森林,将全部叶子结点排序;
2、取出根结点权值最小的两颗二叉树;
3、组成一棵新的二叉树,该二叉树的根结点的权值是这两颗二叉树根结点权值之和;
4、再将这颗新的二叉树的根结点权值跟未处理的结点权值再次排序,不断重复1、2、3、4的步骤,直到数列中所有结点都被处理,就得到一棵哈夫曼树;

  1. /**
  2. * 《哈夫曼树》
  3. */
  4. public class HuffmanTree {
  5. /**
  6. * 根结点
  7. */
  8. private TreeNode rootNode;
  9. /**
  10. * 结点类
  11. */
  12. private class TreeNode implements Comparable<TreeNode>{
  13. private Integer value;
  14. private TreeNode left;
  15. private TreeNode right;
  16. public TreeNode(Integer value) {
  17. this.value = value;
  18. }
  19. public TreeNode(Integer value, TreeNode left, TreeNode right) {
  20. this.value = value;
  21. this.left = left;
  22. this.right = right;
  23. }
  24. /**
  25. * 前序遍历
  26. */
  27. public void beforeOrder() {
  28. if (this == null) {
  29. return;
  30. }
  31. // 1、输出结点
  32. System.out.printf("%s ", this.value);
  33. // 2、前序遍历左子树
  34. if (this.left != null) {
  35. this.left.beforeOrder();
  36. }
  37. // 3、前序遍历右子树
  38. if (this.right != null) {
  39. this.right.beforeOrder();
  40. }
  41. }
  42. @Override
  43. public int compareTo(TreeNode o) {
  44. return this.value - o.value;
  45. }
  46. }
  47. /**
  48. * 前序遍历
  49. */
  50. public void beforeOrderPrint() {
  51. if (rootNode == null) {
  52. System.out.println("前序遍历为空");
  53. return;
  54. }
  55. rootNode.beforeOrder();
  56. }
  57. /**
  58. * 构建哈夫曼树
  59. */
  60. public void buildHuffman(int arr[]) {
  61. // 创建存放结点的集合
  62. List<TreeNode> nodeList = new ArrayList<>();
  63. for (int v: arr) {
  64. nodeList.add(new TreeNode(v));
  65. }
  66. while (nodeList.size() > 1) {
  67. // 1、将集合按从小到大排序
  68. Collections.sort(nodeList);
  69. // 2、取出根结点权值最小的两颗二叉树
  70. TreeNode leftNode = nodeList.get(0);
  71. TreeNode rightNode = nodeList.get(1);
  72. // 3、组成一棵新的二叉树,该二叉树的根结点的权值是这两颗二叉树根结点权值之和
  73. TreeNode parentNode = new TreeNode((leftNode.value + rightNode.value), leftNode, rightNode);
  74. // 4、添加新的二叉树根结点
  75. nodeList.add(parentNode);
  76. // 5、移除刚刚处理过的两个二叉树根结点
  77. nodeList.remove(leftNode);
  78. nodeList.remove(rightNode);
  79. }
  80. rootNode = nodeList.get(0);
  81. }
  82. public static void main(String[] args) {
  83. int[] arr = new int[]{13, 7, 8, 3, 29, 6, 1};
  84. HuffmanTree binaryTree = new HuffmanTree();
  85. // 构建哈夫曼树
  86. binaryTree.buildHuffman(arr);
  87. System.out.print("前序遍历哈夫曼树:");
  88. binaryTree.beforeOrderPrint();
  89. }
  90. }


哈夫曼编码

  1. 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,哈夫曼编码是可变字长编码(VLC)的一种。Huffman1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码(有时也称为霍夫曼编码)。<br /> 哈夫曼编码具有广泛的应用,利用哈夫曼树构造的用于通信的二进制编码称为哈夫曼编码。<br />例如: 有一段电文"CAST囗TAT囗A囗SA" 其中,"囗"表示一个空格)。统计电文中字母的频度 f('C')=1,f('S')=2,f('T')=3,f('囗')=3,f('A')=4;用频度{ 1 2 3 3 4 } 为权值生成哈夫曼树,并在每个叶子上注明对应的字符。树中从根到每个叶子都有一条路径,对路径上的各分枝约定指向左子树根的分枝表示"0"码,指向右子树的分枝表示"1"码,取每条路径上的"0""1"的序列作为和各个叶子对应的字符的编码,这就是哈夫曼编码。对应下图的哈夫曼树,上述字符编码为:<br /> ![](https://cdn.nlark.com/yuque/0/2021/jpeg/12644954/1613642908868-e6808e78-fc4b-43a0-9a97-dfbcf30d754d.jpeg#align=left&display=inline&height=66&margin=%5Bobject%20Object%5D&originHeight=66&originWidth=371&size=0&status=done&style=none&width=371)![](https://cdn.nlark.com/yuque/0/2021/jpeg/12644954/1613642908976-f51c1cba-3376-47b5-8ffa-8e2e412ff786.jpeg#align=left&display=inline&height=213&margin=%5Bobject%20Object%5D&originHeight=213&originWidth=306&size=0&status=done&style=none&width=306)
  1. /**
  2. * 《哈夫曼编码》
  3. */
  4. public class HuffmanCode {
  5. /**
  6. * 根结点
  7. */
  8. private TreeNode rootNode;
  9. /**
  10. * 哈夫曼编码表存放集合
  11. */
  12. private Map<Byte, String> huffmanCodeMap = new HashMap<>();
  13. /**
  14. * 暂存可能不足一个字节的字符编码
  15. */
  16. private String lastByte = "";
  17. /**
  18. * 结点类
  19. */
  20. private class TreeNode implements Comparable<TreeNode>{
  21. // 数据
  22. private Byte data;
  23. // 权重
  24. private Integer weight;
  25. // 左结点
  26. private TreeNode left;
  27. // 右结点
  28. private TreeNode right;
  29. public TreeNode(Byte data, Integer weight, TreeNode left, TreeNode right) {
  30. this.data = data;
  31. this.weight = weight;
  32. this.left = left;
  33. this.right = right;
  34. }
  35. @Override
  36. public String toString() {
  37. return "TreeNode{" + "data=" + data + ", weight=" + weight + '}';
  38. }
  39. @Override
  40. public int compareTo(TreeNode o) {
  41. return this.weight - o.weight;
  42. }
  43. }
  44. /**
  45. * 前序遍历
  46. */
  47. public void beforeOrderPrint() {
  48. if (rootNode == null) {
  49. System.out.println("前序遍历为空");
  50. return;
  51. }
  52. beforeOrderPrint(rootNode);
  53. }
  54. private void beforeOrderPrint(TreeNode node) {
  55. if (node == null) {
  56. return;
  57. }
  58. // 1、输出结点
  59. System.out.println(node);
  60. // 2、前序遍历左子树
  61. beforeOrderPrint(node.left);
  62. // 3、前序遍历右子树
  63. beforeOrderPrint(node.right);
  64. }
  65. /**
  66. * 生成哈夫曼编码表
  67. */
  68. private void createHuffmanCode(){
  69. if (rootNode == null) {
  70. System.out.println("根结点为空!");
  71. return;
  72. }
  73. getNodeCode(rootNode, null);
  74. }
  75. /**
  76. * 获取结点对应的编码
  77. * @param node 结点
  78. * @param code 编码
  79. */
  80. private void getNodeCode(TreeNode node, String code) {
  81. if (code == null) {
  82. code = "";
  83. }
  84. // 如果当前结点是非子结点时,继续往下
  85. if (node.data == null) {
  86. if (node.left != null) {
  87. getNodeCode(node.left, code + "0");
  88. }
  89. if (node.right != null) {
  90. getNodeCode(node.right, code + "1");
  91. }
  92. } else {
  93. huffmanCodeMap.put(node.data, code);
  94. }
  95. }
  96. /**
  97. * 构建哈夫曼树
  98. */
  99. private void buildHuffmanTree(byte[] bytes) {
  100. // 统计编码的频度
  101. Map<Byte, Integer> collectMap = getCollectMap(bytes);
  102. if (collectMap == null || collectMap.isEmpty()) {
  103. return;
  104. }
  105. // 创建存放结点的集合
  106. List<TreeNode> nodeList = new ArrayList<>();
  107. collectMap.forEach((k, v) -> {
  108. nodeList.add(new TreeNode(k, v, null, null));
  109. });
  110. while (nodeList.size() > 1) {
  111. // 1、将集合按从小到大排序
  112. Collections.sort(nodeList);
  113. // 2、取出根结点权值最小的两颗二叉树
  114. TreeNode leftNode = nodeList.get(0);
  115. TreeNode rightNode = nodeList.get(1);
  116. // 3、组成一棵新的二叉树,该二叉树的根结点的权值是这两颗二叉树根结点权值之和
  117. TreeNode parentNode = new TreeNode(null, (leftNode.weight + rightNode.weight), leftNode, rightNode);
  118. // 4、添加新的二叉树根结点
  119. nodeList.add(parentNode);
  120. // 5、移除刚刚处理过的两个二叉树根结点
  121. nodeList.remove(leftNode);
  122. nodeList.remove(rightNode);
  123. }
  124. rootNode = nodeList.get(0);
  125. }
  126. /**
  127. * 统计编码的频度
  128. * @return
  129. */
  130. private Map<Byte, Integer> getCollectMap(byte[] bytes) {
  131. if (bytes == null || bytes.length == 0) {
  132. return Collections.EMPTY_MAP;
  133. }
  134. Map<Byte, Integer> collectMap = new HashMap<>();
  135. for (Byte b : bytes) {
  136. Integer count = collectMap.get(b);
  137. if (count == null) {
  138. collectMap.put(b, 1);
  139. } else {
  140. collectMap.put(b, count + 1);
  141. }
  142. }
  143. return collectMap;
  144. }
  145. /**
  146. * 获取哈夫曼编码
  147. * @return
  148. */
  149. private String getHuffmanCode(byte[] bytes) {
  150. if (bytes == null || bytes.length == 0) {
  151. return null;
  152. }
  153. // 1、构建哈夫曼树
  154. buildHuffmanTree(bytes);
  155. // 2、生成哈夫曼编码表
  156. createHuffmanCode();
  157. // 3、获取待编码字符对应的哈夫曼编码
  158. StringBuilder sb = new StringBuilder();
  159. for (byte b : bytes) {
  160. sb.append(huffmanCodeMap.get(b));
  161. }
  162. return sb.toString();
  163. }
  164. /**
  165. * byte转成二进制字符串(按补码返回)
  166. * @return
  167. */
  168. private String byteToBitString(byte b) {
  169. int temp = b;
  170. // 如果是正数需要补高位,按位与256(1 0000 0000)
  171. temp |= 256;
  172. String str = Integer.toBinaryString(temp);
  173. return str.substring(str.length() - 8);
  174. }
  175. /**
  176. * 哈夫曼编码压缩
  177. * 1、获取待编码字符对应的哈夫曼编码(01字符串)
  178. * 2、遍历哈夫曼编码,每8位(一个字节)转换成byte类型,压缩成byte数组
  179. * @return
  180. */
  181. public byte[] huffmanZip(byte[] bytes) {
  182. // 1、获取待编码字符对应的哈夫曼编码
  183. String code = getHuffmanCode(bytes);
  184. // System.out.println("哈夫曼编码:" + code);
  185. // 2、将哈夫曼编码压缩成byte数组
  186. // 压缩数组的长度
  187. int len = code.length() / 8;
  188. if (code.length() % 8 != 0) {
  189. // 编码长度不能被8整除,保存多余的不足8位的部分
  190. lastByte = code.substring(len * 8);
  191. }
  192. System.out.println("lastByte: " + lastByte);
  193. // 存储压缩的byte数组
  194. byte[] huffmanCodeBytes = new byte[len];
  195. for (int i = 0; i < len; i++) {
  196. huffmanCodeBytes[i] = (byte)Integer.parseInt(code.substring(i * 8, i * 8 + 8), 2);
  197. }
  198. return huffmanCodeBytes;
  199. }
  200. /**
  201. * 哈夫曼编码解压
  202. * 1、逐个遍历压缩后的byte数组,将每个byte元素转换成8位二进制字符串,得到哈夫曼编码字符串
  203. * 2、根据哈夫曼编码表,将哈夫曼编码字符串转化成对应的字节数组
  204. * @return
  205. */
  206. public byte[] huffmanUnZip(byte[] bytes, Map<Byte, String> codeTable) {
  207. // 1、先得到压缩后的字节数组对应的二进制字符串
  208. StringBuffer sb = new StringBuffer();
  209. for(int i = 0; i < bytes.length; i++) {
  210. sb.append(byteToBitString(bytes[i]));
  211. }
  212. // 拼接最后不足一个字节的字符编码
  213. sb.append(lastByte);
  214. // 2、根据哈夫曼编码表,将二进制字符串转化成对应的字节
  215. // 将哈夫曼编码表key和value反转
  216. Map<String, Byte> codeMap = new HashMap<>();
  217. codeTable.forEach((k, v) -> {
  218. codeMap.put(v, k);
  219. });
  220. List<Byte> byteList = new ArrayList<>();
  221. for (int i = 0; i < sb.length(); ) {
  222. int count = 1;
  223. while (true) {
  224. String str = sb.substring(i, i+count);
  225. if (codeMap.containsKey(str)) {
  226. byteList.add(codeMap.get(str));
  227. i += count;
  228. break;
  229. }
  230. count++;
  231. }
  232. }
  233. // 转成字节数组
  234. byte[] byteArr = new byte[byteList.size()];
  235. for (int i = 0; i < byteArr.length; i++) {
  236. byteArr[i] = byteList.get(i);
  237. }
  238. return byteArr;
  239. }
  240. /**
  241. * 哈夫曼编码压缩文件
  242. */
  243. public void zipFile(String srcFile, String dstFile) {
  244. InputStream is = null;
  245. ObjectOutputStream os = null;
  246. try {
  247. is = new FileInputStream(srcFile);
  248. os = new ObjectOutputStream(new FileOutputStream(dstFile));
  249. // 创建一个源文件一样大小的byte数组
  250. byte[] b = new byte[is.available()];
  251. // 1、读取来源文件
  252. is.read(b);
  253. // 2、压缩源文件
  254. byte[] huffmanBytes = huffmanZip(b);
  255. // 3、写入压缩后的文件到目标文件
  256. os.writeObject(huffmanBytes);
  257. // 同时也要写入哈夫曼编码表,便于解压
  258. os.writeObject(huffmanCodeMap);
  259. } catch (Exception e) {
  260. e.printStackTrace();
  261. } finally {
  262. try {
  263. if (os != null) {
  264. os.close();
  265. }
  266. if (is != null) {
  267. is.close();
  268. }
  269. } catch (IOException e) {
  270. e.printStackTrace();
  271. }
  272. }
  273. }
  274. /**
  275. * 哈夫曼编码解压文件
  276. */
  277. public void unZipFile(String srcFile, String dstFile) {
  278. ObjectInputStream is = null;
  279. OutputStream os = null;
  280. try {
  281. is = new ObjectInputStream(new FileInputStream(srcFile));
  282. os = new FileOutputStream(dstFile);
  283. // 1、读取压缩文件
  284. // 读取压缩byte数组
  285. byte[] huffmanBytes = (byte[])is.readObject();
  286. // 读取哈夫曼编码表
  287. Map<Byte, String> huffmanCodeTable = (Map<Byte, String>)is.readObject();
  288. // 2、解压源文件
  289. byte[] bytes = huffmanUnZip(huffmanBytes, huffmanCodeTable);
  290. // 3、写入目标文件
  291. os.write(bytes);
  292. } catch (Exception e) {
  293. e.printStackTrace();
  294. } finally {
  295. try {
  296. if (os != null) {
  297. os.close();
  298. }
  299. if (is != null) {
  300. is.close();
  301. }
  302. } catch (IOException e) {
  303. e.printStackTrace();
  304. }
  305. }
  306. }
  307. public static void main(String[] args) {
  308. String str = "CAST&TAT&A&SAT&AA&";
  309. System.out.println("原数据:" + str);
  310. HuffmanCode binaryCode = new HuffmanCode();
  311. byte[] bytes = binaryCode.huffmanZip(str.getBytes());
  312. System.out.println("压缩:" + Arrays.toString(bytes));
  313. System.out.println("解压:" + new String(binaryCode.huffmanUnZip(bytes, binaryCode.huffmanCodeMap)));
  314. System.out.println("测试文件压缩测试");
  315. binaryCode.zipFile("/Users/yupan/Desktop/123.txt", "/Users/yupan/Desktop/456.txt");
  316. System.out.println("文件压缩成功!");
  317. binaryCode.unZipFile("/Users/yupan/Desktop/456.txt", "/Users/yupan/Desktop/789.txt");
  318. System.out.println("文件解压成功!");
  319. }
  320. }