赫夫曼编码之文件压缩与解压详解
说明
- 使用赫夫曼编码对文件进行压缩与解压,与对文本文件的操作是一样的,只是加入了IO流的相关操作
- 赫夫曼编码对于重复率较高的二进制文件压缩效率较高,但是如果二进制文件重复率不高,则基本没有压缩效率
- 注意使用赫夫曼压缩后的文件,必须使用赫夫曼再进行解压,其他解压工具不提供技术支持
- 源码见下
源码及分析
//将文件进行压缩
/**
*
* @param srcFile 要压缩的文件路径
* @param dstFile 压缩后的文件路径
*/
public static void fileZip(String srcFile, String dstFile){
FileInputStream fis = null;
OutputStream os = null;
ObjectOutputStream oos = null;
try {
//文件输入流
fis = new FileInputStream(srcFile);
//创建和文件大小相同的字节数组
byte[] b = new byte[fis.available()];
//读取文件
fis.read(b);
//将读到的文件进行压缩
byte[] huffmanZipBytes = huffmanZip(b);
//文件输出流
os = new FileOutputStream(dstFile);
oos = new ObjectOutputStream(os);
//将压缩后的字节数组写入磁盘
oos.writeObject(huffmanZipBytes);
//将编码表写入磁盘
oos.writeObject(huffmanCodes);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
//释放资源
fis.close();
os.close();
oos.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
//将压缩的文件进行解压
/**
*
* @param zipFile 要解压的压缩文件
* @param dstFile 解压后的文件路径
*/
public static void unZipFile(String zipFile, String dstFile){
InputStream is = null;
ObjectInputStream ois = null;
OutputStream os = null;
try {
//文件输入流
is = new FileInputStream(zipFile);
//对象输入流
ois = new ObjectInputStream(is);
//读取字节数组
byte[] huffmanByte = (byte[]) ois.readObject();
//读取编码表
Map<Byte,String> huffmanCodes = (Map<Byte, String>)ois.readObject();
//解码
byte[] src = decode(huffmanCodes, huffmanByte);
//文件输出流
os = new FileOutputStream(dstFile);
//将解压后的文件存储到磁盘
os.write(src);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
//释放资源
os.close();
ois.close();
is.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
赫夫曼编码应用完整代码
package algorithm.tree.huffmancode;
import java.io.*;
import java.util.*;
/**
* @author AIMX_INFO
* @version 1.0
*/
@SuppressWarnings("ALL")
public class HuffmanCode {
public static void main(String[] args) {
//测试
//String content = "i like like like java do you like a java";
String content = "what is the most important is learning study";
System.out.println("编码前: " + content);
//将字符串转字节数组
byte[] bytes = content.getBytes();
//压缩
byte[] huffmanCodeByte = huffmanZip(bytes);
//查看效果
System.out.println("编码后: "+Arrays.toString(huffmanCodeByte));
byte[] source = decode(huffmanCodes, huffmanCodeByte);
System.out.println("解码后: "+new String(source));
//文件压缩测试
String srcFile = "D:\\src.png";
String dstFile = "D:\\dst.zip";
fileZip(srcFile,dstFile);
System.out.println("压缩成功");
//解压文件测试
String zipFile = "D:\\dst.zip";
String dstFile2 = "D:\\src2.png";
unZipFile(zipFile,dstFile2);
System.out.println("解压成功");
}
//将压缩的文件进行解压
/**
*
* @param zipFile 要解压的压缩文件
* @param dstFile 解压后的文件路径
*/
public static void unZipFile(String zipFile, String dstFile){
InputStream is = null;
ObjectInputStream ois = null;
OutputStream os = null;
try {
//文件输入流
is = new FileInputStream(zipFile);
//对象输入流
ois = new ObjectInputStream(is);
//读取字节数组
byte[] huffmanByte = (byte[]) ois.readObject();
//读取编码表
Map<Byte,String> huffmanCodes = (Map<Byte, String>)ois.readObject();
//解码
byte[] src = decode(huffmanCodes, huffmanByte);
//文件输出流
os = new FileOutputStream(dstFile);
//将解压后的文件存储到磁盘
os.write(src);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
//释放资源
os.close();
ois.close();
is.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
//将文件进行压缩
/**
*
* @param srcFile 要压缩的文件路径
* @param dstFile 压缩后的文件路径
*/
public static void fileZip(String srcFile, String dstFile){
FileInputStream fis = null;
OutputStream os = null;
ObjectOutputStream oos = null;
try {
//文件输入流
fis = new FileInputStream(srcFile);
//创建和文件大小相同的字节数组
byte[] b = new byte[fis.available()];
//读取文件
fis.read(b);
//将读到的文件进行压缩
byte[] huffmanZipBytes = huffmanZip(b);
//文件输出流
os = new FileOutputStream(dstFile);
oos = new ObjectOutputStream(os);
//将压缩后的字节数组写入磁盘
oos.writeObject(huffmanZipBytes);
//将编码表写入磁盘
oos.writeObject(huffmanCodes);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
//释放资源
fis.close();
os.close();
oos.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
//将压缩的所有方法封装
public static byte[] huffmanZip(byte[] bytes){
//获取节点
List<Node> node = getNode(bytes);
//生成赫夫曼树
Node huffmanTreeRoot = createHuffmanTree(node);
//生成赫夫曼编码
Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
//压缩
byte[] huffmanCodeByte = huffmanCodeByte(bytes, huffmanCodes);
return huffmanCodeByte;
}
//根据字节数组和赫夫曼编码表将字符串压缩
/**
*
* @param bytes 要根据赫夫曼编码表编码的字符数组
* @param huffmanCodes 赫夫曼编码表
* @return 返回压缩后的结果
*/
public static byte[] huffmanCodeByte(byte[] bytes, Map<Byte, String> huffmanCodes) {
//创建可变长字符串,存储编码后的字符串
StringBuilder stringBuilder = new StringBuilder();
//遍历字符数组,实现字符数组中元素的编码
for (byte b : bytes) {
stringBuilder.append(huffmanCodes.get(b));
}
//编码完成后将二进制转换为十进制完成压缩
//计算以8位为一组压缩需要的字符数组空间
int len;
if (stringBuilder.length() % 8 == 0) {
len = stringBuilder.length() / 8;
} else {
len = stringBuilder.length() / 8 + 1;
}
//创建huffmanCodeByte字节数组存放转换后的结果
byte[] huffmanCodeByte = new byte[len];
//辅助变量
int index = 0;
//遍历二进制编码,8位一组转换为十进制进行存储
for (int i = 0; i < stringBuilder.length(); i += 8) {
String bys;
//注意最后一个字符可能不满8位
if (i + 8 > stringBuilder.length()) {
bys = stringBuilder.substring(i);
} else {
bys = stringBuilder.substring(i, i + 8);
}
//添加
huffmanCodeByte[index] = (byte) Integer.parseInt(bys, 2);
index++;
}
return huffmanCodeByte;
}
//前序遍历的方法
public static void preOrder(Node root) {
if (root != null) {
root.preOrder();
} else {
System.out.println("赫夫曼树为空...");
}
}
//将生成的赫夫曼树进行编码,将结果存储到集合中
//可变字符串用于码值拼接
static StringBuilder stringBuilder = new StringBuilder();
//创建HashMap用于存储赫夫曼编码
static HashMap<Byte, String> huffmanCodes = new HashMap<>();
/**
* 将赫夫曼树的所有叶子节点生成编码存储在集合huffmanCodes中
*
* @param node 开始编码的节点
* @param code 码值 向左为0, 向右为1
* @param stringBuilder 码值的拼接
*/
public static void getCodes(Node node, String code, StringBuilder stringBuilder) {
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
//拼接
stringBuilder2.append(code);
//判断node是否为空
if (node != null) {
//再判断当前节点是叶子节点还是非叶子节点
if (node.data == null) {
//如果是非叶子节点则递归处理
//向左递归
getCodes(node.left, "0", stringBuilder2);
//向右递归
getCodes(node.right, "1", stringBuilder2);
} else {
//如果是叶子节点,则将编码添加到集合中
huffmanCodes.put(node.data, stringBuilder2.toString());
}
}
}
//编写生成赫夫曼编码的重载方法
public static Map<Byte, String> getCodes(Node root) {
if (root != null) {
//向左递归
getCodes(root.left, "0", stringBuilder);
//向右递归
getCodes(root.right, "1", stringBuilder);
} else {
System.out.println("赫夫曼树为空");
}
return huffmanCodes;
}
//创建赫夫曼树
public static Node createHuffmanTree(List<Node> list) {
while (list.size() > 1) {
Collections.sort(list);
Node leftNode = list.get(0);
Node rightNode = list.get(1);
Node parent = new Node(null, leftNode.weight + rightNode.weight);
parent.right = rightNode;
parent.left = leftNode;
list.add(parent);
list.remove(leftNode);
list.remove(rightNode);
}
return list.get(0);
}
//编写方法将字节数组中的字符及其权值封装为一个node存储到集合中
public static List<Node> getNode(byte[] bytes) {
//创建集合保存封装好的对象
List<Node> nodes = new ArrayList<>();
//创建HashMap保存字符及其出现的次数
HashMap<Byte, Integer> map = new HashMap<>();
for (byte b : bytes) {
Integer integer = map.get(b);
if (integer == null) {
map.put(b, 1);
} else {
map.put(b, integer + 1);
}
}
//遍历结束后字节数组中的字符及其对应次数已经存储在HashMap的键和值中
for (Map.Entry<Byte, Integer> entry : map.entrySet()) {
nodes.add(new Node(entry.getKey(), entry.getValue()));
}
return nodes;
}
/**
* @param b 要转换的byte数
* @param flag 该数是否需要高位补0
* @return 返回转换后的字符串
*/
public static String byteToBitString(byte b, boolean flag) {
//定义变量int保存byte b,因为Integer类的转二进制方法
int tmp = b;
//如果需要高位补零 说明: 正数需要高位补零,但是最后一个正数不需要
if (flag) {
//按位或
tmp = tmp | 256;
}
//将整数转为二进制字符串,返回对应的补码
String str = Integer.toBinaryString(tmp);
//转换为的二进制位长为计算机位长,32位或者64位,因此需要截取后八位
//如果不是最后一位,截取后八位
if (flag) {
return str.substring(str.length() - 8);
} else {
return str;
}
}
//将编码后的字符数组进行解码
/**
* @param huffmanCodes 赫夫曼编码表
* @param huffmanBytes 赫夫曼编码
* @return 返回解码后的字符串
*/
public static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
//定义StringBuilder拼接
StringBuilder stringBuilder = new StringBuilder();
//遍历赫夫曼编码中的 byte,将其转换为二进制字符串
for (int i = 0; i < huffmanBytes.length; i++) {
byte b = huffmanBytes[i];
//如果时是最后一个byte,则不需要高位补零
boolean flag = (i == huffmanBytes.length - 1);
String str = byteToBitString(b, !flag);
stringBuilder.append(str);
}
//根据赫夫曼编码表的映射关系,将编码转换为对应字符
//将赫夫曼编码表中的键和值反转
Map<String, Byte> map = new HashMap<>();
for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
map.put(entry.getValue(), entry.getKey());
}
//创建集合,存放Byte
ArrayList<Byte> list = new ArrayList<>();
//遍历二进制字符串,解码
for (int i = 0; i < stringBuilder.length();) {
//变量count保存有效的二进制位数
int count = 1;
//flag保存当前字符串是否匹配
boolean flag = true;
//b为当前匹配到的值
Byte b = null;
//循环拿出字符串的每一位进行比较
while (flag) {
String key = stringBuilder.substring(i, i + count);
b = map.get(key);
if (b == null){
count++;
}else {
flag = false;
}
}
list.add(b);
i += count;
}
//循环结束后已经将解码后的字符存储到集合中
byte[] bytes = new byte[list.size()];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = list.get(i);
}
return bytes;
}
}
//节点类
class Node implements Comparable<Node> {
//数据,字符对应的ASICII码
Byte data;
//权值
int weight;
Node left;
Node right;
public Node(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
//前序遍历
public void preOrder() {
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
@Override
public int compareTo(Node o) {
return this.weight - o.weight;
}
}