概述
赫夫曼编码一般指哈夫曼编码,又称霍夫曼编码,是一种编码方式,哈夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码。
赫夫曼编码可以很有效地压缩数据:通常可以节省20%~90%的空间,具体压缩率依赖于数据的特征。
编码知识
编码是信息从一种形式或格式转换为另一种形式的过程。用预先规定的方法将文字、数字或其它对象编成数码,或将信息、数据转换成规定的电脉冲信号。
定长编码:每个字符使用相同长度的编码。
变长编码:不同字符使用不同长度的编码。本文讲述的赫夫曼编码为变长编码中的一种。
下面通过一个例子来简述定长编码与变长编码:
假定我们希望压缩一个10万个字符的数据文件,下表给出了文件中出现的字符和它们的出现频率。
如果为每个字符指定一个3位的码字,我们可以将文件编码为300 000位的长度。但使用上表所示的变长编码,我们可以仅用224 000位编码文件。
变长编码可以达到比定长编码好得多的压缩率。
赫夫曼编码的构造
赫夫曼设计了一个贪心算法来构造这个变长编码(即赫夫曼编码)
在下述伪代码中,假定C是一个n个字符的集合,而其中每个字符c属于C都是一个对象,其属性c.freq给出了字符的出现频率。该算法自底向上地构造出对应最优编码的二叉树T。它从 |C| 个叶节点开始,执行 |C|-1 个“合并”操作创建出最终的二叉树。算法中使用了一个以属性freq为关键字的最小优先队列Q,以识别两个最低频率的对象并将其合并。当合并两个对象时,得到的新对象的频率设置为原来两个对象的频率之和。
//伪代码
HUFFMAN(C)
n = |C|
Q = C
for i = 1 to n-1
allocate a new node z
z.left = x = EXTRACT-MIN(Q)
z.right = y = EXTRACT-MIN(Q)
z.freq = x.freq + y.freq
INSERT(Q,z)
return EXRACT-MIN(Q) //return the root of tree
下图给出了其过程。字符的赫夫曼码用从赫夫曼树根节点到该字符叶节点的简单路径表示,“0”意味着“转向左孩子”,“1”意味着“转向右孩子”。
Java代码实现
Node类:
public class Node implements Comparable<Node>{
Byte data; //可以为空
int weight;
Node left;
Node right;
public Node(Byte data,int weight) {
this.data = data;
this.weight = weight;
}
@Override
public int compareTo(Node o) {
return o.weight - this.weight; //倒序(从大到小)
}
@Override
public String toString() {
//打印内容
return "Node [data=" + data + ", weight=" + weight + "]";
}
public int getDate() {
return this.weight;
}
}
main:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class TestHuffmanCode {
public static void main(String[] args) {
// TODO Auto-generated method stub
String msg = "can you can a can as a can canner can a can.";
byte[] bytes = msg.getBytes();
//进行赫夫曼编码压缩
byte[] bs = huffmanZip(bytes);
//比较原来和使用赫夫曼编码后的编码效率
System.out.println("效率比较:");
System.out.println("进行编码压缩前: " + bytes.length);
System.out.println("赫夫曼编码压缩后: " + bs.length);
System.out.println();
//使用指定的赫夫曼编码表进行解码
byte[] newBytes = decode(huffCodes,bs);
//比较解码前后的字符编码
System.out.println("原字符串与解码后字符串的ASCII码:");
System.out.println("原字符串: " + Arrays.toString(bytes));
System.out.println("解码后字符串: " + Arrays.toString(newBytes));
System.out.println();
//转译成解码后字符串
System.out.println("原字符串与解码后字符串:");
System.out.println("原字符串: " + new String(bytes));
System.out.println("解码后字符串: " + new String(newBytes));
}
//使用指定的赫夫曼编码表进行解码
private static byte[] decode(Map<Byte, String> huffCodes, byte[] bs) {
// TODO Auto-generated method stub
StringBuilder sb = new StringBuilder();
for(int i = 0;i < bs.length;i++) {
byte b = bs[i];
// String s = Integer.toBinaryString(b);
// System.out.println(s);
//是否是最后一个
boolean flag = (i == bs.length-1);
sb.append(byteToBitStr(!flag,b));
}
//将解码后的二进制码打印,对应编码前的二进制数组
// System.out.println(sb.toString());
//把字符串按照指定的赫夫曼编码进行解码
//把赫夫曼编码的键值对调换,便于解码
Map<String,Byte> map = new HashMap<>();
for(Map.Entry<Byte, String> entry:huffCodes.entrySet()) {
map.put(entry.getValue(), entry.getKey());
}
//创建一个集合,临时存储byte元素(不知道字符串能截取出多少byte元素)
List<Byte> list = new ArrayList<>();
//处理字符串
for(int i = 0;i < sb.length();) {
int count = 1;
boolean flag = true;
//截取出一个byte
Byte b = null;
while(flag) {
String key = sb.substring(i, i+count);
b = map.get(key); //与map的key对比
if(b == null) {
//若不存在,则多取一位字节
count++;
}else {
//若取到了,退出循环
flag = false;
}
}
// System.out.println(b);
list.add(b);
i += count;
}
//把集合转为数组传出
byte[] bts = new byte[list.size()];
for(int i = 0;i <bts.length;i++) {
bts[i] = list.get(i);
}
return bts;
}
//补齐为全部8位的元素
private static String byteToBitStr(boolean flag,byte b) {
//负数不足8位自动补齐,正数添0,flag确保最后一位数位正常(如果是正数,则不补位)
int temp = b;
if(flag) {
temp |= 256; //位运算
}
String str = Integer.toBinaryString(temp);
if(flag) {
return str.substring(str.length()-8); //返回后8位
}else {
return str;
}
}
//进行赫夫曼编码压缩的方法
private static byte[] huffmanZip(byte[] bytes) {
// TODO Auto-generated method stub
//先统计每一个byte出现的次数,并放入一个集合中
List<Node> nodes = getNodes(bytes);
//创建一个赫夫曼树
Node Tree = createHuffmanTree(nodes);
//创建一个赫夫曼编码表(键值对)
Map<Byte,String> huffCodes = getCodes(Tree);
// System.out.println(huffCodes);
//编码
byte[] c = zip(bytes,huffCodes); //ASCII码值转化为字符形式
return c;
}
//进行赫夫曼编码
private static byte[] zip(byte[] bytes, Map<Byte, String> huffCodes) {
// TODO Auto-generated method stub
StringBuilder sb = new StringBuilder();
//把需要压缩的bytes数组处理成一个二进制字符串
for(byte b:bytes) {
sb.append(huffCodes.get(b));
}
//输出经赫夫曼编码后的二进制数组
// System.out.println(sb.toString());
//将二进制字符串写入8位的字节数组(压缩)
//定义长度
int len;
if(sb.length()%8 == 0) {
len = sb.length()/8;
}else {
len = sb.length()/8 + 1;
}
//用于存储压缩后的byte
byte[] by = new byte[len];
//记录新的byte数组下标
int index = 0;
for(int i = 0;i < sb.length();i+=8) {
String StrByte;
if(i+8 > sb.length()) {
//如果剩余字节不足8位,则全部截取出
StrByte = sb.substring(i);
}else {
//截取8个字节
StrByte = sb.substring(i, i+8);
}
// System.out.println(StrByte);
//将StrByte转化为十进制数(减少内存的消耗)
byte byt = (byte) Integer.parseInt(StrByte, 2);
// System.out.println(StrByte + ":" + byt);
by[index] = byt;
index++;
}
return by;
}
//用于临时存储路径
static StringBuilder sb = new StringBuilder();
//存储赫夫曼编码表
static Map<Byte,String> huffCodes = new HashMap<>();
//根据赫夫曼树获取赫夫曼编码
private static Map<Byte, String> getCodes(Node tree) {
// TODO Auto-generated method stub
if(tree == null) {
return null;
}
getCodes(tree.left,"0",sb);
getCodes(tree.right,"1",sb);
return huffCodes;
}
private static void getCodes(Node node, String code, StringBuilder sb) {
// TODO Auto-generated method stub
StringBuilder sbb = new StringBuilder(sb); //
sbb.append(code);
if(node.data == null) {
getCodes(node.left,"0",sbb);
getCodes(node.right,"1",sbb);
}else {
huffCodes.put(node.data,sbb.toString());
}
}
//创建赫夫曼树
private static Node createHuffmanTree(List<Node> nodes) {
// TODO Auto-generated method stub
while(nodes.size()>1) {
//排序
Collections.sort(nodes);
Node left = nodes.get(nodes.size()-1);
Node right = nodes.get(nodes.size()-2);
Node parent = new Node(null,left.getDate() + right.getDate());
parent.left = left;
parent.right = right;
nodes.remove(left);
nodes.remove(right);
nodes.add(parent);
}
return nodes.get(0);
}
//把bytes数组转为Node集合
private static List<Node> getNodes(byte[] bytes) {
// TODO Auto-generated method stub
List<Node> nodes = new ArrayList<>();
Map<Byte,Integer> counts = new HashMap<>(); //bytes元素对象,出现的次数
//统计每一个byte出现的次数
for(byte b:bytes) {
Integer count = counts.get(b);
if(count == null) {
counts.put(b, 1); //如果此前出现的次数为0,则置为1
}else {
counts.put(b, count+1); //如果此前出现的次数不为0,则+1
}
}
// System.out.println(counts);
//把每一对键值对转为一个Node对象
for(Map.Entry<Byte, Integer> entry:counts.entrySet()) {
//遍历counts
nodes.add(new Node(entry.getKey(),entry.getValue()));
}
return nodes;
}
}