ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

哈夫曼编码介绍与实现

2022-07-17 15:31:19  阅读:124  来源: 互联网

标签:编码 哈夫曼 int 介绍 new byte public out


1952年,David Huffman发表了一篇名为《一种构建最优编码的方法》( A Method for the Construction of Minimum-Redundancy Codes)的论文,提出了一种构建最优编码(最少冗余)的方法,这种方法后来被称为哈夫曼编码(Huffman coding)。


冗余,意味着多余或者啰嗦。最少的冗余意味着用最少的数据表达最多的信息。我们聊天或文字表达时都存在一定的信息冗余,有时为了强调甚至会“说三遍”(必要的冗余能保证信息的有效传达)。而在计算机信息传输和存储领域,冗余信息就是意味着更多的带宽和存储空间。

出于信息传递效率的目的,需要对冗余信息进行精简。这里讨论的冗余信息精简不是把没用的文字去掉,而是在不改变原有信息的基础上,在编码层次进行精简,即无损编码(无损压缩)。

以我们常见的纯文本文件为例,例如文件内容是abcaba,共计6个字节。如果让你设计一种压缩方法,你会如何做呢?

一种简单的方法是看字母或者字母组合的出现次数,比如ab是出现了2次,我们可以用1来代替,这样就变成了“1c1a”,长度就变成了4,相当于减少了33%。

还能再压缩吗?好像已经没有办法了。

这时能看到的额外信息无非是1出现了两次,c,a个一次,而且1、c、a已经是最小字节长度了。从字符层次看的确是没法再压缩了,我们需要更深一层来看这些字母。

我们以二进制格式查看这个文件,如下

ASCII编码中分别用十进制的0-127表示一些常用字符。其中abc分别对应97 98 99,转换成二进制就是 01100001 01100010 01100011。可以看到二进制下abc三个字母的前6个比特都是011000。

在这种编码中,最小单位是1字节(8bit),是固定长度的。好处是读取方便,但必然存在一定的空间冗余。如果从文件整体的编码来考虑,一个字符出现次数多,应该给它分配较小的长度编码,类似莫尔斯电码中对常用的E、T只分配了一个点一个横,而不是每个字符都分配固定的长度(当然莫尔斯电码中还有间隔的表示方法,以区分各个字符)。

现在假设我们采用了一种“更高效”的编码,我们先分析各个字符的出现次数,a 3次, b 2次, c 1次,所以我们用0表示a,1表示b,01表示c

a 0
b 1
c 01

按此规则转换后的编码是0101010,共计7比特,远小于之前的48比特。压缩比这么高吗?稍等,我们只压缩数据还不行,还需要考虑如何还原数据。你可能会说映射回去就可以了,但是我们仔细观察下:取第一位0表示a,第二位1表示b,第三位0,是a吗?转换完毕最后就变成 abababa了。原信息可是abcaba!

问题就出在第三位这个0,本来应该是拿01映射回去的,但是却被0匹配上了!还原时并不知道这个规则,这就是可变长度编码带来的问题,如果是固定的8bit就没这个问题了,当然长度又变长了…所以我们需要调整下编码,让它能必然能匹配上。

这次我们用以下编码
a 0
b 10
c 11

这样就转换为010110100,共9比特,虽然大于之前的7比特,但也远远小于原始的48比特。再看下这时的还原过程,仍然从第一位0读取,对应字母a,第二位1没查到,编码不存在吗?先不管,我们继续往后读第三位0,是a吗?不行,这时由于前面有一位未映射到的1,我们要把1和0组合成10再取映射关系,也就是b,然后再取第四位1又不存在,继续取下一位1,和上一步的1组合成11是c,就这样一直取完,只要发现不匹配的就临时存下来,再和下一位组合后再匹配,直到匹配到为止。在这种规则下我们就能顺利还原出数据了。

我们称这种编码规则为“前缀码”,即每次出现的编码不能是其他编码的前缀,例如 00 01 10 110这种是满足前缀码的,但是 01 001 010 110就不可以(01对应01的同时,也是010的前缀,会导致010一直匹配不到)。而这种规则对应到数据结构上就是之前提到过的二叉树:我们把节点左侧的连接线用0表示,节点右侧的连接线用1表示,如图示

 

从图中可以看到如果一个字符编码短,它的位置就应该更靠上方。

根据这个规则,我们可以设计一种编码方式:

1 统计文本种每个字符的出现次数,并按照出现次数从小到大进行排列

2 取出现次数最小的两个字符构建二叉树,然后把两个字符次数加和后的次数和剩余字符排序,再取次数最小的两个字符再加和,再合并排序,直到生成一棵包含所有字符的二叉树

3 按新的编码规则生成新的文件。

这其实就是哈夫曼编码的核心流程。例如“aababcde”构建过程如图示,表格第一列为字符和出现次数,后续列为每轮合并后再按次数排序的结果

 

 

 主要代码如下

  1 package org.example.huffmancoding;
  2 
  3 import java.util.HashMap;
  4 import java.util.Map;
  5 
  6 public class HuffmanCodingDemo {
  7     //保存每个字符的出现次数
  8     private final Map<String, Integer> countMap = new HashMap<>();
  9     //保存编码后的对应关系
 10     private final Map<String, String> codeMap = new HashMap<>();
 11     //最终编码的补齐长度,解码时需要用到
 12     private int paddingLength = 0;
 13 
 14     public static void main(String[] args) {
 15         String text = "aababcde";
 16         HuffmanCodingDemo demo = new HuffmanCodingDemo();
 17         byte[] encode = demo.encode(text);
 18         for (byte aByte : encode) {
 19             System.out.println(aByte + "->" + byteToBinary(aByte));
 20         }
 21         System.out.println(demo.decode(encode));
 22     }
 23 
 24     public byte[] encode(String text) {
 25         buildCountMap(text);
 26         System.out.println(countMap);
 27         buildCodeMap();
 28         System.out.println(codeMap);
 29         return buildCode(text);
 30     }
 31 
 32     public String decode(byte[] bytes) {
 33         //反转codeMap
 34         Map<String, String> codeNewMap = new HashMap<>();
 35         for (Map.Entry<String, String> entry : codeMap.entrySet()) {
 36             codeNewMap.put(entry.getValue(), entry.getKey());
 37         }
 38         System.out.println(codeNewMap);
 39 
 40         StringBuilder sb = new StringBuilder();
 41         byte[] buffer = new byte[128];
 42         int bufferWriteLength = 0;
 43         int totalBytes = bytes.length;
 44         for (byte aByte : bytes) {
 45             totalBytes--;
 46             byte[] binaryBytes = byteToBinary(aByte).getBytes();
 47             int binaryByteWriteLength = 8;
 48             for (byte binaryByte : binaryBytes) {
 49                 //处理最后一个字节时如果存在补齐 直接跳出
 50                 if (totalBytes == 0 && binaryByteWriteLength == paddingLength) {
 51                     System.out.println("break paddingLength " + paddingLength);
 52                     break;
 53                 }
 54                 buffer[bufferWriteLength++] = binaryByte;
 55                 String key = new String(buffer, 0, bufferWriteLength);
 56                 if (codeNewMap.containsKey(key)) {
 57                     sb.append(codeNewMap.get(key));
 58                     bufferWriteLength = 0;
 59                 }
 60                 binaryByteWriteLength--;
 61             }
 62         }
 63         return sb.toString();
 64     }
 65 
 66     public static String byteToBinary(int num) {
 67         char[] binaryDecode = new char[8];
 68         for (int i = 0; i < 8; i++) {
 69             int and = (num & (1 << i)) >> i;
 70             binaryDecode[7 - i] = (char) (and + 48);
 71         }
 72         return new String(binaryDecode);
 73     }
 74 
 75     private byte[] buildCode(String text) {
 76         //!!!这里为了演示,限制了编码后最长为2048。可以考虑到固定长度后写入文件,或者动态扩容。
 77         byte[] encodeBuffer = new byte[2048];
 78         int encodeBufferLength = 0;
 79 
 80         byte[] buffer = new byte[32];
 81         int bufferWriteLength = 0;
 82         byte[] bytes = text.getBytes();
 83         //每8个长度的01组合转换为1byte
 84         int binaryBitNum = 8;
 85         byte[] binaryBytes = new byte[binaryBitNum];
 86         int binaryWriteLength = 0;
 87         for (byte aByte : bytes) {
 88             buffer[bufferWriteLength++] = aByte;
 89             String key = new String(buffer, 0, bufferWriteLength);
 90             if (codeMap.containsKey(key)) {
 91                 bufferWriteLength = 0;
 92                 String code = codeMap.get(key);
 93                 System.out.println(key + "->" + code);
 94                 for (int i = 0; i < code.length(); i++) {
 95                     binaryBytes[binaryWriteLength++] = (byte) code.charAt(i);
 96                     if (binaryWriteLength == binaryBitNum) {
 97                         encodeBuffer[encodeBufferLength++] = bytesToBinary(binaryBytes);
 98                         binaryWriteLength = 0;
 99                     }
100                 }
101             }
102         }
103 
104         if (binaryWriteLength != 0) {
105             //有未处理完的需要补齐到8
106             paddingLength = binaryBitNum - binaryWriteLength;
107             System.out.printf("binaryWriteLength %s padding %s\n", binaryWriteLength, paddingLength);
108             for (int i = 0; i < paddingLength; i++) {
109                 //48表示ascii字符0,需要从右侧补齐
110                 binaryBytes[binaryWriteLength++] = 48;
111             }
112             encodeBuffer[encodeBufferLength++] = bytesToBinary(binaryBytes);
113         }
114         byte[] tmp = new byte[encodeBufferLength];
115         System.arraycopy(encodeBuffer, 0, tmp, 0, encodeBufferLength);
116         return tmp;
117     }
118 
119     private byte bytesToBinary(byte[] bytes) {
120         //1二进制下为110001 0为110000 取最后一个bit进行位移即可
121         int out = 0;
122         for (int i = 0; i < bytes.length; i++) {
123             out += (bytes[i] & 1) << (7 - i);
124         }
125         return (byte) out;
126     }
127 
128     private void buildCountMap(String text) {
129         //按单个字节统计出现次数
130         byte[] bytes = text.getBytes();
131         for (byte aByte : bytes) {
132             String str = String.valueOf((char) aByte);
133             countMap.putIfAbsent(str, 0);
134             countMap.put(str, countMap.get(str) + 1);
135         }
136     }
137 
138     private void buildCodeMap() {
139         BinaryHeap binaryHeap = buildBinaryHeap();
140         Node finalNode;
141         while (true) {
142             Node left = binaryHeap.getMin();
143             binaryHeap.removeMin();
144             if (binaryHeap.getCount() == 0) {
145                 finalNode = left;
146                 break;
147             }
148             Node right = binaryHeap.getMin();
149             binaryHeap.removeMin();
150 
151             Node mergeNode = new Node();
152             mergeNode.left = left;
153             mergeNode.right = right;
154             mergeNode.value = left.value + right.value;
155             binaryHeap.insert(mergeNode);
156         }
157         System.out.println(finalNode);
158         //遍历出每个叶子节点的编码,用StringBuilder来保存左右分支
159         StringBuilder sb = new StringBuilder();
160         preOrder(finalNode, sb);
161     }
162 
163     //前序遍历树 先处理根节点,然后左侧树,再右侧树
164     private void preOrder(Node node, StringBuilder sb) {
165         if (node == null) {
166             sb.delete(sb.length() - 1, sb.length());
167             return;
168         }
169         if (node.code != null) {
170             codeMap.put(node.code, sb.toString());
171         }
172         sb.append(0);
173         preOrder(node.left, sb);
174         sb.append(1);
175         preOrder(node.right, sb);
176         if (sb.length() != 0) {
177             sb.delete(sb.length() - 1, sb.length());
178         }
179     }
180 
181     private BinaryHeap buildBinaryHeap() {
182         //构建成小顶堆 每次取最小的次数
183         BinaryHeap binaryHeap = new BinaryHeap(countMap.size());
184         for (Map.Entry<String, Integer> entry : countMap.entrySet()) {
185             Node node = new Node();
186             node.value = entry.getValue();
187             node.code = entry.getKey();
188             binaryHeap.insert(node);
189         }
190         return binaryHeap;
191     }
192 }

 输出如下

{a=3, b=2, c=1, d=1, e=1}  #统计的各字符出现次数
{L={a},R={L={b},R={L={d},R={L={c},R={e},},},},} #最终构建的二叉树,只输出了关键信息
{a=0, b=10, c=1110, d=110, e=1111}  #字符对应的新编码
a->0 #逐个转化为新编码
a->0
b->10
a->0
b->10
c->1110
d->110
e->1111
binaryWriteLength 2 padding 6  #计算最终需要补齐的位长度
37->00100101 #最终编码为三个字节
-37->11011011
-64->11000000 #最后一个字节包含了补齐的6位,即右侧的6个0
{0=a, 110=d, 1111=e, 1110=c, 10=b} #解码映射关系
break paddingLength 6 #跳过最后补齐的6位
aababcde #原始字符串

 依赖的Node.java和BinaryHeap.java如下

 1 package org.example.huffmancoding;
 2 
 3 public class Node {
 4     public Node left;
 5     public Node right;
 6     public String code;
 7     public int value;
 8 
 9     //简化输出 只保留L R code
10     @Override
11     public String toString() {
12         String out = "{";
13         if (left != null) {
14             out += "L=" + left + ",";
15         }
16         if (right != null) {
17             out += "R=" + right + ",";
18         }
19         if (code != null) {
20             out += "" + code + "";
21         }
22         return out + "}";
23             /*
24             return "Node{" +
25                     "left=" + left +
26                     ", right=" + right +
27                     ", code=" + code +
28                     ", value=" + value +
29                     '}';*/
30     }
31 }
View Code
 1 package org.example.huffmancoding;
 2 
 3 public class BinaryHeap {
 4     private int count = 0;
 5     private final int size;
 6     private final Node[] data;
 7 
 8     public BinaryHeap(int size) {
 9         this.size = size;
10         data = new Node[size + 1];
11     }
12 
13     public int getCount() {
14         return count;
15     }
16 
17     public Node getMin() {
18         return data[1];
19     }
20 
21     public void removeMin() {
22         if (count == 0) {
23             System.out.println("heap is empty!");
24             return;
25         }
26         data[1] = data[count];
27         count--;
28 
29         int leftChildIndex;
30         int rightChildIndex;
31         int currentIndex = 1;
32         int smallestIndex;
33         while (true) {
34             leftChildIndex = currentIndex * 2;
35             rightChildIndex = leftChildIndex + 1;
36             smallestIndex = currentIndex;
37 
38             if (leftChildIndex <= count && data[leftChildIndex].value < data[smallestIndex].value) {
39                 smallestIndex = leftChildIndex;
40             }
41 
42             if (rightChildIndex <= count && data[rightChildIndex].value < data[smallestIndex].value) {
43                 smallestIndex = rightChildIndex;
44             }
45 
46             if (currentIndex != smallestIndex) {
47                 swap(currentIndex, smallestIndex);
48                 currentIndex = smallestIndex;
49             } else {
50                 break;
51             }
52         }
53     }
54 
55     public void insert(Node num) {
56         if (count >= size) {
57             System.out.println("heap is full!");
58             return;
59         }
60         count++;
61         data[count] = num;
62 
63         int currentIndex = count;
64         int parentIndex;
65         while (true) {
66             parentIndex = currentIndex / 2;
67             if (parentIndex == 0) {
68                 break;
69             }
70             if (data[parentIndex].value > num.value) {
71                 swap(currentIndex, parentIndex);
72                 currentIndex = parentIndex;
73             } else {
74                 break;
75             }
76         }
77     }
78 
79     @Override
80     public String toString() {
81         StringBuilder str = new StringBuilder("[");
82         for (int i = 1; i <= count; i++) {
83             str.append(data[i]);
84             if (i != count) {
85                 str.append(", ");
86             }
87         }
88         return str.append("]").toString();
89     }
90 
91     private void swap(int pos1, int pos2) {
92         Node tmp = data[pos1];
93         data[pos1] = data[pos2];
94         data[pos2] = tmp;
95     }
96 }
View Code

 

参考资料

《一种构建最优编码的方法》论文
http://compression.ru/download/articles/huff/huffman_1952_minimum-redundancy-codes.pdf

哈夫曼编码的一些背景知识

https://www.maa.org/press/periodicals/convergence/discovery-of-huffman-codes

http://www.huffmancoding.com/my-uncle/scientific-american

https://web.stanford.edu/class/archive/cs/cs106b/cs106b.1212/assignments/7-huffman/huffman_background.html

 

idea中文件以二进制形式查看的插件

https://plugins.jetbrains.com/plugin/9339-bined--binary-hexadecimal-editor

标签:编码,哈夫曼,int,介绍,new,byte,public,out
来源: https://www.cnblogs.com/binary220615/p/16486816.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有