1、基本介绍
2、应用实践(文件压缩)
package tree;
import java.io.*;
import java.util.*;
public class FileCompression {
public static Map<Byte, String> huffmanCodes; // 用于存放哈弗曼编码
public static byte[] huffmanCodesBytes; // 用于存放哈夫曼编码字节数组(压缩后的字节数组)
public static void main(String[] args) {
String srcFile = "f://1.txt";
String midFile = "f://2.txt";
// String destFile = "f://3.txt";
// 压缩文件
if (zipFile(srcFile, midFile)) {
System.out.println("压缩成功!");
} else {
System.out.println("压缩失败!");
}
// 解压文件
// if (unZipFile(desFile, destFile)) {
// System.out.println("解压成功!");
// } else {
// System.out.println("解压失败!");
// }
}
public static boolean zipFile(String srcFile, String desFile) {
FileInputStream fis = null;
FileOutputStream fos = null;
ObjectOutputStream oss = null; // 以对象的形式写出到文件中
try {
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(desFile);
oss = new ObjectOutputStream(fos);
byte[] bytes = new byte[fis.available()]; // 用于暂存读出来的数据
fis.read(bytes); // 读取数据放到字节数组
huffmanZip(bytes);
oss.writeObject(huffmanCodesBytes); // 将压缩后的字节数组写出到文件
oss.writeObject(huffmanCodes); // 将哈夫曼编码写出到文件,用于后面对文件的解压
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
oss.close();
fos.close();
fis.close();
return true;
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
public static boolean unZipFile(String zipFile, String destFile) {
FileInputStream fis = null;
ObjectInputStream ois = null; // 以对象的形式写出到文件中
FileOutputStream fos = null;
try {
fis = new FileInputStream(zipFile);
ois = new ObjectInputStream(fis);
fos = new FileOutputStream(destFile);
byte[] huffmanByte = (byte[])ois.readObject(); // 将哈弗曼编码字节数组(压缩数组)读取出来
Map<Byte, String> huffmanCode = (Map<Byte, String>) ois.readObject();// 将哈弗曼编码独取出来
byte[] b = huffmanUnZip(huffmanByte, huffmanCode); // 返回解压后的字节数组
fos.write(b); // 将字节数组写出到文件
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
fos.close();
ois.close();
fis.close();
return true;
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
// 哈夫曼编码压缩
public static void huffmanZip(byte[] srcBytes) {
// 2、将字节数组 转为 类型为Node的List集合
List<ZNode> nodes = getNodes(srcBytes);
// 3、根据List集合构建哈夫曼树
HuffmanTrees HuffmanTrees = createHuffmanTrees(nodes);
// 4、根据哈夫曼树,生成对应的哈夫曼编码
huffmanCodes = getCodes(HuffmanTrees.root, "", stringBuilder);// 根节点没有路径,设置为空
// 5、根据哈夫曼编码表,将原字节数组压缩,并返回压缩后的以补码形式表示的哈夫曼编码字节数组
huffmanCodesBytes = zip(srcBytes, huffmanCodes); // 此时有17个字符
}
// 哈夫曼编码解压
public static byte[] huffmanUnZip(byte[] huffmanByte, Map<Byte, String> huffmanCode) {
return unzip(huffmanByte, huffmanCode);
}
//---------------------------------------------------------------------------------------------------
// 将被压缩字节数组的每一位字符转为0、1字符串
public static String toBinaryString(byte b, boolean flag) {
/*
个人理解:
1、遍历数组每一位时,对负数需要截取后8位,同时还需要考虑对字节数组中的正数进行高位补齐
2、但当遍历到字节数组最后一位时:
2、1.如果是正数,则无需补齐
2、2.如果是负数,则需要截取后八位
*/
int temp = b; // 将其转为int类型,用于获取对应的二进制字符串
String str = null; // 存放二进制字符串
if (flag) { // 为true:表示该字符为字节数组的最后一位,如果是正数,表示该字符无需高位补齐。如果是负数,则需要进行截取
if (temp < 0) {
str = Integer.toBinaryString(temp);
return str.substring(str.length() - 8); // 截取后8位
} else {
return Integer.toBinaryString(temp);
}
} else {
temp |= 256; // 为false,表该字符不是字节数组的最后一位
str = Integer.toBinaryString(temp);
return str.substring(str.length() - 8); // 截取后8位
}
}
private static byte[] unzip(byte[] huffmanByte, Map<Byte, String> huffmanCode) {
// 1、将哈夫曼编码字节数组huffmanCodesBytes的每一个字符(-88, -65, -56, -65, -56)拼接成二进制字符串(10101000......)
StringBuilder stringBuilder = new StringBuilder(); // 用于拼接二进制字符串
for (int i = 0; i < huffmanByte.length; i++) {
boolean flag = (i == huffmanByte.length - 1); // 字节数组最后一位需特殊处理,要考虑其是否满八位的情况
stringBuilder.append(toBinaryString(huffmanByte[i], flag)); // 逐个读取字符,从而获取到对应的二进制字符串
}
// 2、按照哈夫曼编码,对二进制字符串进行解码
// 因为要查字符串多个二进制位 对应 的字符,而原哈夫曼编码集合形式是<Byte,String>,所以需要将哈夫曼编码表进行反转
Map<String, Byte> temp = new HashMap<>();
for (Map.Entry entry : huffmanCode.entrySet()) {
temp.put((String) entry.getValue(), (Byte)entry.getKey());
}
List<Byte> ch = new ArrayList<>();
// 3、遍历二进制字符串,从而获取到对应的字符
for (int i = 0; i < stringBuilder.length(); ) {
// 通过循环不断地截取对应的二进制个数,判断temp集合中是否有该哈夫曼编码对应的字符
int count = 1;
boolean flag = true;
Byte b = null;
while (flag) {
String key = stringBuilder.substring(i, i + count); // 一个一个二进制位匹配
b = temp.get(key); // 判断key这个哈夫曼编码在temp集合中是否存在
if (b == null) {
count++; // 如果不存在,则遍历到下一个二进制位
} else {
flag = false; // 如果存在,则已经获取到一个哈夫曼编码对应的字符,则退出循环
}
}
ch.add(b);
i += count; // i移动到count的位置,寻找下一个哈夫曼编码对应的字符
}
// 将List集合中的数据放到字节数组,并返回
byte[] bytes = new byte[ch.size()];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = ch.get(i);
}
return bytes;
}
/*
1、根据哈夫曼编码集合huffmanCodes的哈夫曼编码,将原始的字节数组strBytes每一个字符拼接成一个由0、1组成的字符串
2、然后将字符串按8位进行分割,再转成十进制,返回byte类型的数组
*/
private static byte[] zip(byte[] strBytes, Map<Byte, String> huffmanCodes) {
StringBuilder stringBuilder = new StringBuilder(); // 用于拼接字节数组每一个字符所对应的哈夫曼编码
int len = 0; // 统计哈夫曼编码字节数组的长度
int index = 0; // 作为哈夫曼编码字节数组的下标,用于存放1个字节对应的数据
String temp; // 暂存所截取的二进制字符串
// 1、将字节数组转换成由0、1组成的哈夫曼编码字符串
for (byte b : strBytes) {
// 获取字节数组的每一个字符,然后去哈夫曼编码集合中,获取该字符对应的哈夫曼编码(0、1字符串)
stringBuilder.append(huffmanCodes.get(b));
}
// 2、将哈夫曼编码字符串中的0、1按8位进行分割
if (stringBuilder.length() % 8 == 0) { // 判断该哈夫曼编码字符串可以组成多少个十进制数
len = stringBuilder.length() / 8;
} else {
len = stringBuilder.length() / 8 + 1;
}
byte[] huffmanCodeBytes = new byte[len]; // 存放哈夫曼编码字符串对应的以补码形式表示的字符
for (int i = 0; i < stringBuilder.length(); ) {
if (i + 8 > stringBuilder.length()) { // 考虑到字符串末尾时,不足八位的情况
temp = stringBuilder.substring(i); // 则从当前位开始截取,直到字符串末尾
huffmanCodeBytes[index] = (byte) Integer.parseInt(temp, 2); // 将其转为二进制的形式
break;
}
temp = stringBuilder.substring(i, i + 8);
huffmanCodeBytes[index] = (byte)Integer.parseInt(temp, 2); // 将其转为二进制的形式
i+=8; // 每8位存放一个字符
index++;
}
return huffmanCodeBytes; // 将对应的哈夫曼编码字节数组返回(全为补码)
}
// 下面两个东西用于生成对应的哈夫曼编码时使用:
public static Map<Byte, String> huffmanCode = new HashMap<>(); // 用于暂存存放哈夫曼编码
public static StringBuilder stringBuilder = new StringBuilder(); // 用于暂存并拼接路径上的0、1个数
private static Map<Byte, String> getCodes(ZNode node, String code, StringBuilder stringBuilder) {
// 用于拼接每个叶子节点上的0、1
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
// 将路径上的0、1拼接到StringBuilder
stringBuilder2.append(code);
// 处理不为null的节点
if (node != null) {
// 为null时,表明该节点是非叶子节点,则递归统计根节点到该节点路径上0、1的个数
if (node.data == null) {
getCodes(node.left, "0", stringBuilder2);
getCodes(node.right, "1", stringBuilder2);
} else {
// 不为null时,则表明该节点是叶子节点,这时将根节点到当前叶子结点上的哈夫曼编码添加到map集合中
huffmanCode.put(node.data, stringBuilder2.toString());
}
}
return huffmanCode;
}
private static HuffmanTrees createHuffmanTrees(List<ZNode> nodes) {
while (nodes.size() > 1) {
// 1、对List<Node>集合进行排序
Collections.sort(nodes);
// 2、取出两棵权值最小的二叉树
ZNode node1 = nodes.get(0);
ZNode node2 = nodes.get(1);
// 3、构建一个棵新的二叉树,并重新添加到List集合
ZNode root = new ZNode(null, node1.weight + node2.weight);
root.left = node1;
root.right = node2;
nodes.add(root);
// 4、将原来那两棵二叉树从List集合中移除
nodes.remove(node1);
nodes.remove(node2);
}
// 5、当List集合仅剩一个元素时,该元素即为哈夫曼树的根节点
return new HuffmanTrees(nodes.get(0));
}
private static List<ZNode> getNodes(byte[] strBytes) {
List<ZNode> nodes = new ArrayList<>(); // 用于存放Node节点的List集合
Map<Byte, Integer> map = new HashMap<>(); // 用于暂存每个字符及出现次数的映射关系
for (byte b : strBytes) {
Integer count = map.get(b); // 判断当前字符在map中是否有值
if (Objects.isNull(count)) { // 返回结果为null,表示当前字符还未放入map集合
map.put(b, 1); // 将当前字符放入map集合,key为字符本身,value为字符出现的次数
} else {
map.put(b, count + 1); // 不为null,表明map集合中已有该字符,则将其出现次数加1并再次放到map集合中
}
}
// 将map中每一个[key为字符,value为字符出现个数]的元素 转化为Node对象 并放入到List集合中
for (Map.Entry entry : map.entrySet()) {
nodes.add(new ZNode((Byte) entry.getKey(), (Integer) entry.getValue()));
}
return nodes;
}
}
class HuffmanTrees {
public ZNode root;
public HuffmanTrees(ZNode root) {
this.root = root;
}
// 前序遍历
public void preOrderTraverse() {
if (this.root == null) {
return;
} else {
System.out.print("前序遍历:");
this.root.preOrderTraverse();
}
}
}
class ZNode implements Comparable<ZNode> { // 对ZNode对象进行集合排序
public Byte data; // 字符对应的ASCII
public Integer weight; // 权值
public ZNode left;
public ZNode right;
public ZNode(Byte data, Integer weight) {
this.data = data;
this.weight = weight;
this.left = null;
this.right = null;
}
// 前序遍历
public void preOrderTraverse() {
System.out.print(this);
if (this.left != null) {
this.left.preOrderTraverse();
}
if (this.right != null) {
this.right.preOrderTraverse();
}
}
@Override
public int compareTo(ZNode o) {
// 升序排序
return this.weight - o.weight;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
}