数据压缩
压缩数据的原因主要有两点:节省保存信息所需的空间和节省传输信息所需的时间。
基础模型
数据压缩的基础模型由两个主要的部分组成,两者都是一个能够读写比特流的黑盒子:
- 压缩盒:能够将一个比特流B转化为压缩后的版本C(B);
- 展开盒:能够将C(B)转化为回B。
如果使用|B|表示比特流中的比特的数量的话,我们感兴趣的是将|C(B)|/|B|最小化,这个值被称为压缩率。
这种模型叫无损压缩模型——保证不丢失任何信息,即压缩和展开之后的比特流必须和原始的比特流完全相同。许多种类型的文件都会用到无损压缩,例如数值数据或者可执行的代码。对于某些类型的文件(例如图像、视频和音乐),有损的压缩方法也是可以接受的,此时解码器所产生的输出只是与原输入文件近似。有损压缩算法的评价标准不仅是压缩率,还包括主观的质量感受。
通用数据压缩
通用性的数据压缩算法,即一个能够缩小任意比特流的算法,是不可能存在的。不存在能够压缩任意比特流的算法。当遇到一种新的无损压缩算法时,我们可以肯定它是无法大幅度压缩随机比特流的。
霍夫曼压缩
霍夫曼压缩是一种能够大幅度压缩自然语言文件空间的数据压缩技术。它的主要思想是放弃文本文件的普通保存方式:不再使用7位或8位二进制数表示每一个字符,而是用较少的比特表示出现频率高的字符,用较多的比特表示出现频率低的字符。
变长前缀码
如果所有字符编码都不会成为其他字符编码的前缀,那么就不需要分隔符了,含有这种性质的编码规则叫做前缀码。所有前缀码的解码方式是唯一的(不需要任何分隔符),因此前缀码被广泛应用于实际生产之中。注意,像7位ASCII编码这样的定长编码也是前缀码。
前缀码的单词查找树
表示前缀码的一种简便方法就是使用单词查找树。事实上,任意含有M个空链接的单词查找树都为M个字符定义了一种前缀方法:我们将空链接替换为指向叶子结点(含有两个空链接的结点)的链接,每个叶子结点都含有一个需要编码的字符。这样,每个字符的编码就是从根结点到该结点的路径表示的比特字符串,其中左链接表示0,右链接表示1。是否存在能够压缩得更多得单词查找树呢?如何才能找到压缩率最高的前缀码?实际上,这些问题都有一个优雅的解。有一种算法能够为任意字符串构造一棵能够将比特流最小化的单词查找树。寻找最优前缀码的通用方法是D.Huffman在1952年发现的,因此被称为霍夫曼编码。
概述
使用前缀码进行数据压缩需要经过5个主要步骤。我们将待编码的比特流看作一个字节流并按照以下方式使用前缀码:
- 构造一棵编码单词查找树;
- 将该树以字节流的形式写入输出以供展开时使用;
- 使用该树将字节流编码为比特流。
在展开时需要:
- 读取单词查找树(保存在比特流的开头);
- 使用该树将比特流解码。
使用前缀码展开
有了定义前缀码的单词查找树,扩展被编码的比特流就简单了。根据比特流的输入从根结点开始向下移动(读取一个比特,如果为0则移动到左子结点,如果为1则移动到右子结点)。当遇到叶子结点后,输出该结点的字符并重新回到根结点。
使用前缀码压缩
在压缩时,我们使用单词查找树定义的编码来构造编译表。对于任意单词查找树,它都能产生一张将树中的字符和比特字符串(用由0和1组成的String字符串表示)相对应的编译表。编译表就是一张将每个字符和它的比特字符串相关联的符号表。在构造符号表时,函数递归
遍历整棵树并为每个结点维护了一条从根结点到它的路径所对应的二进制字符串(0表示左链接,1表示右链接)。每当到达一个叶子结点时,算法就将结点的编码设为该二进制字符串。编译表建立之后,压缩就很简单了,只需在其中查找输入字符所对应的编码即可。
单词查找树的构造
我们将需要被编码的字符放在叶子结点中并在每个结点中维护了一个名为freq的实例变量来表示以它为根结点的子树中的所有字符出现的频率。构造的第一步是创建一片由许多只有一个结点(即叶子结点)的树所组成的森林。每棵树都表示输入的一个字符,每个结点中的freq变量的值都表示了它在输入中的出现频率。接下来自底向上根据频率构造这棵编码的单词查找树。在构造时将它看作一棵结点中含有频率信息的二叉树;构造后,我们才将它看作一棵用于编码的单词查找树。构造过程如下:首先找到两个频率最小的结点,然后创建一个以二者为子结点的新结点(新结点的频率值为它的两个子结点的频率值之和)。这个操作会将森林中树的数量减一。然后不断重复这个过程,找到森林中的两棵频率最小的树并用相同的方式创建一个新的结点。用优先队列能够轻易实现这个过程。随着这个过程的继续,我们构造的单词查找树将越来越大,而森林中的树会越来越少(每一步都会删除两棵树,添加一棵新树)。最终,所有的结点会被合并为一棵单独的单词查找树。这棵树中的叶子结点为所有待编码的字符和它们在输入中出现的频率,每个非叶子结点中的频率值为它的两个子结点之和。频率较低的结点会被安排在树的底层,而高频率的结点则会被安排在根结点附近的地方。根结点的频率值等于输入中的字符数量。
最优性
加权外部路径长度:它是所有叶子结点的权重(频率)和深度之积的和。
对于任意前缀码,编码后的比特字符串的长度等于相应单词查找树的加权外部路径长度。
给定一个含有r个符号的集合和它们的频率,霍夫曼算法所构造的前缀码是最优的。
不同的选择会得到不同的霍夫曼编码,但用它们将信息编码所得到的比特字符串在所有前缀码中都是最优的。
霍夫曼压缩的实现
其中,优先队列为MinPQ。
package section5_5;
import section2_1.priorityqueue.MinPQ;
import java.util.ArrayList;
import java.util.List;
public class Huffman {
private static int R = 256; //ASCII字母表
private Node root; //霍夫曼编码单词查找树的根
private int N; //输入数据字符总数
private List<Boolean> compressed_data = new ArrayList<>(); //已压缩数据
private List<Character> decompressed_data = new ArrayList<>(); //已解压数据
//霍夫曼单词查找树中的结点
private static class Node implements Comparable<Node> {
private char ch;
private int freq;
private final Node left, right;
public Node(char ch, int freq, Node left, Node right) {
this.ch = ch;
this.freq = freq;
this.left = left;
this.right = right;
}
public boolean isLeaf() {
return left == null && right == null;
}
@Override
public int compareTo(Node that) {
return this.freq - that.freq;
}
}
//使用单词查找树构造编译表
private static String[] buildCode(Node root) {
String[] st = new String[R];
buildCode(st,root,"");
return st;
}
private static void buildCode(String[] st, Node x, String s) {
if (x.isLeaf()) {
st[x.ch] = s;
return;
}
buildCode(st,x.left,s + '0');
buildCode(st,x.right,s + '1');
}
//构造一棵霍夫曼编码单词查找树
private static Node buildTrie(int[] freq) {
MinPQ<Node> pq = new MinPQ<>(100);
for (char c = 0;c < R;c++) {
if (freq[c] > 0) {
pq.insert(new Node(c,freq[c],null,null));
}
}
while (pq.size() > 1) {
Node x = pq.delMin();
Node y = pq.delMin();
Node parent = new Node('\0',x.freq + y.freq,x,y);
pq.insert(parent);
}
return pq.delMin();
}
//压缩编码
public void compress() {
//读取输入
String s = "it was the best of times it was the worst of times";
char[] input = s.toCharArray();
//频率统计
int[] freq = new int[R];
for (int i = 0;i < input.length;i++) {
freq[input[i]]++;
}
//构造霍夫曼编码树
root = buildTrie(freq);
//(递归地)构造编译表
String[] st = new String[R];
buildCode(st,root,"");
System.out.println("Huffman code:");
for (int i = 0;i < st.length;i++) {
if (st[i] != null) {
System.out.println("\"" + (char) i + "\":" + st[i]);
}
}
//字符总数
N = input.length;
//使用霍夫曼编码处理输入
for (int i = 0;i < input.length;i++) {
String code = st[input[i]];
for (int j = 0;j < code.length();j++) {
if (code.charAt(j) == '1') {
compressed_data.add(true);
} else {
compressed_data.add(false);
}
}
}
}
//前缀码的展开(解码)
public void expand() {
int idx = 0;
for (int i = 0;i < N;i++) {
Node x = root;
while (!x.isLeaf()) {
if (compressed_data.get(idx)) {
x = x.right;
} else {
x = x.left;
}
idx++;
}
decompressed_data.add(x.ch);
}
}
//转储(dump)表示的是比特流的一种可供人类阅读的形式
public void binaryDump() {
int width = 64;
int cnt = 0;
System.out.println("compressed data:");
for (int i = 0;i < compressed_data.size();i++,cnt++) {
if (width == 0) continue;
if (cnt != 0 && cnt % width == 0) {
System.out.println();
}
if (compressed_data.get(i)) {
System.out.print("1");
}
else {
System.out.print("0");
}
}
System.out.println();
System.out.println(cnt + " bits");
}
public static void main(String[] args) {
Huffman huffman = new Huffman();
huffman.compress();
huffman.binaryDump();
huffman.expand();
System.out.println("decompressed data:");
for (int i = 0;i < huffman.decompressed_data.size();i++) {
System.out.print(huffman.decompressed_data.get(i));
}
}
}
输出: