基于哈夫曼编码的单文件压缩/解压项目
项目语言:Java
项目地址: https://github.com/Mazai-Liu/Compress
哈夫曼编码
哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,哈夫曼编码是可变字长编码的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码(有时也称为霍夫曼编码)。
通过构造哈弗曼树(也称最优二叉树),可以得到哈弗曼编码。
哈夫曼编码实现可以应用于数据压缩,主要有以下两点原因:
- 哈夫曼编码是前缀编码,不会有二义性
- 哈夫曼树即最优二叉树,是带权路径长度最短的树,故字符编码的总长最短
项目实现思路及流程(完整代码见github)
源文件每个字节存储所需8位bit,通过构造哈夫曼树得到带权路径最小的字节编码。此编码长度不一定是8位bit,且出现频率越高的字节,编码长度越短。
把每个字节按编码表写入压缩后的文件,解压时再根据相同原理解码。(解码不会有二义性)
1 构造字节出现的频率表
public static int[] frequency = new int[256];
...
// OFFSET作为偏移量,用以处理中文
// frequency为频率表
BufferedInputStream in = null;
try {
in = new BufferedInputStream(new FileInputStream(filePath));
byte[] buf = new byte[1024 * 10];
int left;
while((left = in.read(buf)) > 0){
// 统计字符出现频率
for(int i = 0;i < left;i++)
frequency[buf[i] + OFFSET]++;
}
2 根据频率表,构造哈弗曼树
public class TNode implements Comparable<TNode>{
public byte value;
public int fre;
public TNode left;
public TNode right;
public boolean isLeaf;
public TNode(byte value, int fre){
...
}
public TNode(int fre){
...
}
public int compareTo(TNode tNode) {
return this.fre - tNode.fre;
}
}
public void makeHuffman(){
// make original queue
Queue<TNode> queue = new PriorityQueue();
for(int i = 0;i < 256;i++){
if(frequency[i] > 0){
queue.add(new TNode((byte)(i - OFFSET), frequency[i]));
}
}
while(queue.size() > 1){
TNode left = queue.poll();
TNode right = queue.poll();
TNode newNode = new TNode(left.fre + right.fre);
newNode.left = left;
newNode.right = right;
queue.add(newNode);
}
//root 为哈弗曼树的根节点
root = queue.poll();
}
3 根据哈弗曼树,构造编码表(字节 --> 编码)
public void makeCodeTable(){
dfs(root, "");
}
void dfs(TNode root, String code){
if(root.isLeaf){
encodeTable.put(root.value, code);
return;
}
dfs(root.left, code + "0");
dfs(root.right, code + "1");
}
4 根据编码表,把源文件按字节编码,按一定格式生成压缩文件
压缩文件的格式为:魔数 + 源文件大小 + 频率表大小 + 频率表 + 压缩后数据大小(bit) + 压缩后的数据
// magic total lengthOfFreTable freTable lengthOfCompressedData CompressedData
// 1B 4B 4B xB 4B xB
public void doCompression(String filePath){
BufferedOutputStream out = null;
try {
out = new BufferedOutputStream(new FileOutputStream(file));
...
out.write(MAGIC); // 魔数
writeInt(out, ORIGIN_TOTAL); // 源文件大小
writeInt(out, 256); // 频率表大小
writeFre(out); // 频率表
// 组合01串,再把01串按位写入压缩文件中
writeCompressedData(out, in); // 写入压缩后数据
...
out.flush();
}catch(){}
}
/**
* 不能直接 write(int x)。虽然x是一个4字节的int,但只会只写入x的最后一字节。
* 所以需要封装一个方法,真正写入4字节。通过位运算不断取v的8位填充到字节数组中。
*/
public static void writeInt(BufferedOutputStream out, int v) throws IOException {
byte[] buf = new byte[4];
buf[0] = (byte) ((v >> 24) & 0xFF);
buf[1] = (byte) ((v >> 16) & 0xFF);
buf[2] = (byte) ((v >> 8) & 0xFF);
buf[3] = (byte) (v & 0xFF);
out.write(buf);
}
5 按相同原理解码
public void readHead(String filePath){
BufferedInputStream in = null;
try {
in = new BufferedInputStream(new FileInputStream(filePath));
// 魔数
byte[] magic = new byte[1];
in.read(magic);
if(magic[0] != 'k'){
System.out.println("file format error!");
System.exit(-1);
}
// 总大小
checkTotalByte = readInt(in);
// 频率表长
tableLength = readInt(in);
// 读取频率表
readFre(in);
// 构建哈弗曼树
makeHuffman();
// 根据哈弗曼树解压
process(in);
} catch () {}
}
// 完整代码见github