哈夫曼编码介绍与实现
作者:互联网
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
idea中文件以二进制形式查看的插件
https://plugins.jetbrains.com/plugin/9339-bined--binary-hexadecimal-editor
标签:编码,哈夫曼,int,介绍,new,byte,public,out 来源: https://www.cnblogs.com/binary220615/p/16486816.html