数据压缩是指在不丢失有用信息的前提下,缩减数据量以减少存储空间,提高其传输、存储和处理效率,或按照一定的算法对数据进行重新组织,减少数据的冗余和存储的空间的一种技术方法。数据压缩包括有损压缩和无损压缩。
这篇文章将从0到1的告诉大家如何实现下图所示的字符串文本压缩与文件压缩。
完成数据压缩很简单,只需要知道下面几个知识点:
- 二进制
- 数组
- 前缀编码
- Huffman tree
- Huffman coding
下面进入正题:
什么是字节(Byte)数组?
在 scala 中每个 Byte 都是由八位二进制补码组成的,这样的 Byte 组成的数组就是字节数组。字节也是计算机中数据存储的基本单位。
一个实现压缩的小案例:
字符串 “abcd” 中每个字符对应的 Ascii 码是 a->97 b->98 c->99 d->100,这个字符串字符串转化为字节数组就是 Array[97, 98, 99, 100]
转为二进制补码的对应关系:
97->01100001 98->01100010 99->01100011 100->01100100
同理,字符串 “aaaabbbccd” 对应的字节数组就是 Array[97,97,97,97,98,98,98,99,99,100] 在计算机底层其实就是 01100001011000010110000101100001011000100110001001100010011000110110001101100100
所以这个字符串 “aaaabbbccd” 的大小为10个字节。
这里我们重新做一个编码,把上边字符串 “aaaabbbccd” 中每个字符出现的次数做个排序,也就是 a:4 b:3 c:2 d:1
我们按照排序先后,分别用二进制 0,1,10,11 来表示 a,b,c,d
那么,字符串 “aaaabbbccdd” 就变成了 0000111101011,这里存储大小才用了12个bit,不到2个字节的大小,这其实就是一个数据压缩。
问题来了,上边虽然实现了数据压缩,但是,我们无法还原,因为编码中 0000111101011 标黄的 0 我们不能确定它是 a 还是 c, 里面的 1 我们也不知道他们是单独的 1 还是 11 ,这种字符编码存在可能本身为其他字符编码前缀这种问题。
前缀编码
什么是前缀编码?
字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码,哈夫曼编码(Huffman coding) 就是这种 前缀 编码。
用这种编码实现的数据压缩,我们就可以解决数据压缩还原(逆向编码即可)的问题。
哈夫曼树(Huffman Tree)
在谈论哈夫曼编码前,我们先了解什么是哈夫曼树?
- 给定 n 个权值作为 n 个叶子结点,构造一棵二叉树,若该树的 带权路径长度 (wpl: weighted path length) 达到最小,称这样的 二叉树 为 最优二叉树 ,也称为 哈夫曼树(Huffman Tree)
- 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近
如图所示:
下面我们用代码来实现创建一个 Huffman Tree
- 创建一个节点类;这里 Node 需要继承 Comparable ,因为 Node 必须是可排序的。
- 构建 Huffman Tree;这里需要注意的地方是,scala 中 ListBuffer 是才是可变长 List,List 是不可变的。
哈夫曼编码(Huffman Coding)
什么是哈夫曼编码?
哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,哈夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码(有时也称为霍夫曼编码)。
哈夫曼编码表
给定字符串 "aaaabbbccd" ,根据给定字符串,我们以字符出现次数为权构建哈夫曼树;根据哈夫曼树,给各个字符规定编码,向左的路径为 0 ,向右的路径为 1 。 编码如图所示:
有了这个编码表,我们可以把给定字符串 "aaaabbbccd" 编码为 "0000101010111111110" 。比起原来的编码 "01100001011000010110000101100001011000100110001001100010011000110110001101100100" 减少了不少的空间,之前是 10 个字节,通过哈夫曼编码后只用了不到 3 个字节。
重点来了!!!
如何把字节数组变成哈夫曼编码后的字节数组?
- 获取字节数组,遍历字节数组,以每个字节信息为 Node 节点的数据信息(data),字节出现次数为权(weight),构建一个 ListBuffer
- 根据 ListBuffer 里面的元素(也就是Node节点)构建哈夫曼树
- 根据哈夫曼树生成哈夫曼编码表
- 根据哈夫曼编码表,把原字节数组编码成哈夫曼编码后的字节数组,完成数据压缩
我们现在用下边这个字符串来走一遍过程
“Only if you asked to see me, our meeting would be meaningful to me”
- 先把字符串变成字节数组
val content = "Only if you asked to see me, our meeting would be meaningful to me"
val bytes = content.getBytes()
for (b <- bytes) print(b + " ")
// 遍历bytes:
/*
79 110 108 121 32 105 102 32 121 111 117 32 97 115 107 101 100 32 116 111 32 115 101 101 32 109 101 44 32 111 117 114 32 109 101 101 116 105 110 103 32 119 111 117 108 100 32 98 101 32 109 101 97 110 105 110 103 102 117 108 32 116 111 32 109 101
*/
- 使用字节数组构建 ListBuffer,ListBuffer 中元素为 Node,Node 中 data 为字节信息,weight(权) 为每个字节出现的次数,也就类似做一个word count 的功能
val list = getNodes(contentBytes)
for (x <- list) print(x.data + "->" + x.weight + "; ")
// 这里遍历结果为:
/*
32->13; 97->2; 98->1; 100->2; 101->9; 102->2; 103->2; 105->3; 107->1; 44->1; 108->3; 109->4; 110->4; 79->1; 111->5; 114->1; 115->2; 116->3; 117->4; 119->1; 121->2;
*/
- 根据 ListBuffer 里面的元素(也就是Node节点)构建哈夫曼树,这里返回的是哈夫曼树的 root 节点
- 根据哈夫曼树生成哈夫曼编码表,编码表用一个可变Map来存储
val list = getNodes(contentBytes)
val tree = createHuffmanTree(list)
val map = getHuffmanCodeTab(tree)
for ((k, v) <- map) print(k + "->" + v + "; ")
// 这里遍历后得到
/*
32->00; 97->01110; 98->110000; 100->01111; 101->101; 102->10000; 103->10001; 105->11101; 107->110001; 44->110010; 108->11110; 109->0100; 110->0101; 79->110011; 111->1101; 114->111000; 115->10010; 116->11111; 117->0110; 119->111001; 121->10011;
*/
- 根据哈夫曼编码表,把原字节数组编码成哈夫曼编码后的字节数组,完成数据压缩
val huffmanBytes = enCode(bytes, map)
for (b <- huffmanBytes) print(b + " ")
// 这里遍历将得到
/*
-51 125 51 -80 39 -84 58 88 -41 -97 -46 86 -119 114 53 -72 18 -33 -11 98 115 -83 -25 -104 81 43 -105 -85 24 55 -113 -24 37 0
*/
到这里,我们已经完成了数据的压缩
原字节数组:
79 110 108 121 32 105 102 32 121 111 117 32 97 115 107 101 100 32 116 111 32 115 101 101 32 109 101 44 32 111 117 114 32 109 101 101 116 105 110 103 32 119 111 117 108 100 32 98 101 32 109 101 97 110 105 110 103 102 117 108 32 116 111 32 109 101
编码后的数组:
-51 125 51 -80 39 -84 58 88 -41 -97 -46 86 -119 114 53 -72 18 -33 -11 98 115 -83 -25 -104 81 43 -105 -85 24 55 -113 -24 37 0
最后,我们来看看如何解码?
解码只需要根据编码流程,逆向操作即可
实现文件压缩
哈夫曼编码是按字节来处理的,因此可以处理所有的文件(图片,文本文件,xml文件等);当然如果一个文件中内容重复数据不多,那么压缩效果就不会很明显,比如很复杂的色彩丰富的图片。
我们来测试一下
zipFile("F:\\src.txt", "F:\\scr.zip")
结束奉上源码: