游程编码
定义:
用一个 符号值/串 代替具有相同值的连续符号(连续符号构成了一段连续的“游程”。游程编码因此而得名),使符号长度少于原始数据的长度。
应用场景:
游程编码经典应用场景就是比特流中的冗余情况。例如有一串比特流:0000000000000001111111000000011111111111,该比特流中有15个0,然后是7个1,然后是7个0,然后是11个1。因为0和1总是交替出现的,我们只要表示出游程长度即可。上面的比特流可用游程编码压缩为:1111011101111011(15=1111,7=0111,7=0111,11=1011)。
游程编码被广泛使用于保存图像和扫描文档。不适用于比特流不含较长游程的情况(比如典型的英文文档)。
Q:
- 应该用多少比特记录游程长度?
- 某个游程长度超过了能够记录的最大长度怎么办?
- 当游程长度所需的比特数小于记录长度的比特数怎么办?
A:
- 游程长度应该在0-255之间,使用8位编码;
- 在需要的情况下使用长度为0的游程来保证所有游程的长度小于256;
- 较小的游程也会编码,虽然这样可能使输出变得更长。
代码实现:
压缩操作:读取一个比特,如果它和上个比特值不同,保存(写入)当前计数器的值并将计数器清零;如果它和上个比特值相同,分两种情况:计数器还未到最大值,则直接增加计数器的值即可;如果计数器已经为最大值,则写入计数器的值并再写入一个0,然后计数器归0.
解压操作:读取一个游程的长度,将当前比特按照长度复制并输出,转换比特值并继续,直到结束。
霍夫曼编码
霍夫曼压缩的思想:
使用较少的比特表示出现频繁的字符而使用较多的比特表示使用较少的字符。这样表示字符串所使用的总比特数就会减少。这里有一个问题:应该保证所有字符编码都不会成为其他字符编码的前缀,这个问题可以使用霍夫曼树解决。
构造霍夫曼树:
首先定义霍夫曼树的结点类:
private static class Node implements Comparable {
private final char ch;
private final int freq;
private final Node left, right;
Node(char ch, int freq, Node left, Node right) {
this.ch = ch;
this.freq = freq;
this.left = left;
this.right = right;
}
private boolean isLeaf() { return (left == null) && (right == null); }
public int compareTo(Node that) { return this.freq - that.freq; }
}
然后构建霍夫曼树:
霍夫曼树是一个二轮算法,它需要扫描目标字符串两次才能压缩它。第一次扫描统计每个字符出现的频率,第二次扫描根据生成的编译表压缩。
构造过程:为每个字符创建一个独立的结点(可以看成只有一个结点的树)。首先找到两个频率最小的结点,然后创建一个以这两个结点为子结点的新节点(新节点的频率值为两个子结点频率值之和);这个操作会使森林中树的数量减一。不断重复这个过程直到只剩下一棵树为止。这样,从树的根结点到叶结点的路径就是叶结点中字符对应的霍夫曼编码。
private static Node buildTrie(int[] freq) {
MinPQ pq = new MinPQ();
for (char i = 0; i < R; i++)
if (freq[i] > 0)
pq.insert(new Node(i, freq[i], null, null));
while (pq.size() > 1) {
Node left = pq.delMin();
Node right = pq.delMin();
Node parent = new Node('\0', left.freq + right.freq, left, right);
pq.insert(parent);
}
return pq.delMin();
}
解码操作:
根据霍夫曼树将比特流解码:根据比特流的输入从根节点向下移动(遇到0走左子结点,遇到1走右子结点),遇到叶结点后,输出该叶结点的字符并回到根结点。
public static void expand() {
Node root = readTrie();
int length = BinaryStdIn.readInt();
for (int i = 0; i < length; i++) {
Node x = root;
while (!x.isLeaf()) {
boolean bit = BinaryStdIn.readBoolean();
if (bit) x = x.right;
else x = x.left;
}
BinaryStdOut.write(x.ch);
}
BinaryStdOut.close();
}
压缩操作:
压缩操作是根据构造的编译表实现的。根据霍夫曼树建立一张字符和路径对应的二进制字符串相联系的表,然后扫描目标字符串,每读入一个字符,查表得到相应的二进制字符串并输出即可。
构建编译表:
private static void buildCode(String[] st, Node x, String s) {
if (!x.isLeaf()) {
buildCode(st, x.left, s + '0');
buildCode(st, x.right, s + '1');
}
else {
st[x.ch] = s;
}
}
使用编译表进行压缩:
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) == '0')
BinaryStdOut.write(false);
else (code.charAt(j) == '1')
BinaryStdOut.write(true);
}
}
- 对于任意前缀码,编码后的比特字符串长度等于霍夫曼树的加权外部路径长度。
- 给定一个含有r个符号的集合和它们的频率,霍夫曼算法所构造的前缀码是最优的。