思路全部写到注释里面了
完整代码如下:
package com.wqc.tree.huffmancode;
import java.io.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author 高瞻远瞩
* @version 1.0
* @motto 算法并不可怕, 可怕的是你不敢面对它, 加油!别浮躁~冲击大厂!!!
* 哈夫曼编码的压缩数据:1,将发送的数据转换成哈夫曼树 2 将生成的哈夫曼树转换成哈夫曼编码表 规定向左为0 向右为1 是按照补码进行编码
* 比如10101000是补码形式 对应的原码(符号位不变 其他位取反 末尾+1)--》11011000 对应的十进制-->-88
* 3,把发送的数据根据哈夫曼表转换得到成二进制的数据 然后将二进制数据每8位作为一个byte位存放到一个字节数组 进行发送
* 根据收到的字节数组进行哈夫曼编码解压:1,将字节数组根据哈夫曼编码表得到二进制的字符串序列 2,将哈夫曼编码表的k和v交换
* 3,扫描得到的二进制字符串序列 对照交换后的哈夫曼编码表 根据扫描到的字符串作为key得到对应的value(ascii码) 存放到字节数组返回
* 注意事项:如果只是读取文件内容 进行编码和解码 在控制台输出的化 需要考虑最后一位byte位的情况 即需要190~192行的代码
* 如果是解压文件 则不能加上那段代码 会报内存不足的异常
*/
public class HuffmanCode {
private static HashMap<Byte, String> hm = new HashMap<>();//k-->字符 v-->哈夫曼编码 比如:32=01
private static StringBuilder sb = new StringBuilder();//用于拼接哈夫曼编码
private static int lastLength;//记录压缩最后一个字节的二进制长度
public static void main(String[] args) throws Exception {
//====压缩文件====
// String srcPath = "d:\\111.bmp";
// String destPath = "d:\\sun.zip";
// zipFile(srcPath,destPath);
// System.out.println("压缩成功~");
//
// ====解压文件====
// String srcPath = "d:\\sun.zip";
// String destPath = "d:\\a111.bmp";
// unZip(srcPath, destPath);
// System.out.println("解压成功~");
// String filePath = "d:\\VarParameter.java";
// FileInputStream fileInputStream = new FileInputStream(filePath);
byte[] bytes = new byte[(int)new File(filePath).length()];
// byte[] bytes = new byte[fileInputStream.available()];//也可以用这种方式定义字节数组的大小
// //available()返回从此输入流中可以读取(或跳过)的剩余字节数的估计值,而不会被下一次调用此输入流的方法阻塞。
// fileInputStream.read(bytes);//将文件里的内容读到这个字节数组中
String content = "我爱你一生一世";
byte[] bytes = content.getBytes();
byte[] zip = huffmanZip(bytes);//40个数据被压缩成了17个 压缩了17/40
System.out.println("最后压缩后的字节数组=" + Arrays.toString(zip) + " length=" + zip.length);
//将压缩后的字节数组通过网络传输发送出去 即可
//将字节数组解码 --》content
byte[] decode = decode(zip,hm);
System.out.println(new String(decode));
}
/**
* 文件解压
*
* @param srcPath 需要解压的文件路径
* @param destPath 把文件解压到的目标路径
*/
public static void unZip(String srcPath, String destPath) {
ObjectInputStream ois = null;//读取源文件
FileInputStream fis = null;
FileOutputStream fos = null;//写文件
try {
fis = new FileInputStream(srcPath);
ois = new ObjectInputStream(fis);
byte[] bytes = (byte[]) ois.readObject();//读取压缩后的字节数组
Map<Byte, String> huffmanCodes = (HashMap<Byte, String>) ois.readObject();
byte[] decode = decode(bytes, huffmanCodes);
fos = new FileOutputStream(destPath);//创建文件输出流对象
fos.write(decode);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fos != null) {
fos.close();
}
if (ois != null) {
ois.close();
}
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 文件压缩
*
* @param srcPath 需要压缩的文件路径
* @param destPath 压缩后的文件路径
*/
public static void zipFile(String srcPath, String destPath) {
//创建文件输入输出流
FileInputStream fis = null;
ObjectOutputStream oos = null;
try {
fis = new FileInputStream(srcPath);
byte[] bytes = new byte[fis.available()];
// byte[] bytes = new byte[(int) new File(srcPath).length()];
fis.read(bytes);//将文件写入字节数组中
//压缩
byte[] zipBytes = huffmanZip(bytes);//压缩后哈夫曼编码表就已经生成
//用对象输出流 将压缩文件以及哈夫曼编码表输出到指定路径
oos = new ObjectOutputStream(new FileOutputStream(destPath));
oos.writeObject(zipBytes);
Map<Byte, String> map = hm;
oos.writeObject(hm);//静态属性不能被序列化 但是HashMap集合又实现了Serializable接口
} catch (Exception e) {
e.printStackTrace();
} finally {//关流
try {
fis.close();
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 将一个字节输入流转换成字节数组返回
*
* @param is 字节输入流 显性参数可以是InputStream的子类
* @return 返回的是一个字节数组
*/
public static byte[] streamToByteArray(InputStream is) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//输出流
int len = 0;
byte[] bytes = new byte[1024];//缓冲数组
byte[] b;
while ((len = is.read(bytes, 0, len)) != -1) {//边读边写 一次读len长度
byteArrayOutputStream.write(len);
}
b = byteArrayOutputStream.toByteArray();
//关流
byteArrayOutputStream.close();
return b;
}
/**
* 将一个字节数据转换成二进制字符串
*
* @param b byte类型的数据
* @param flag flag表示是否需要补高位 真的话表示需要补
* 最后一个字节不足8位不需要补 但是如果它是负数的话 需要截取后8位 而不是直接返回
* @return 返回的是字节数据对应的二进制字符串 补码的形式
*/
public static String byteToString(byte b, boolean flag) {
int temp = b;
if (flag) {//最后一位28不需要进行补位
temp |= 256;// 00000001 | 100000000(256) 或的意义在于如果传进来的是1 不够8位 需要自行补位
//11100 | 100000000 --> 000011100 | 100000000 ---> 1000011100 所以最后一个字节不够8位的不需要进行这个补位操作
}
String s = Integer.toBinaryString(temp);//结果的形式是int类型是32位的 只需要取最后的8位
// Integer.toBinaryString(int i)//由二进制(基数2)中的参数表示的无符号整数值的字符串表示形式。
if (flag || temp < 0) {//如果传进来的最后一位字节是负数的话 补码形式也需要截取后8位
return s.substring(s.length() - 8);
} else {
return s;
//如果是传进来的是加密的字节数组的最后一位(比如) 转换后补码形式的二进制不够8位 就不需要补位 直接返回即可
}
}
/**
* 将一个字节数组转换成二进制字符串 解码:将二进制字符串和哈夫曼编码表进行匹配 得到存放字符的字节数组
*
* @param bytes 需要解码的字节数组
* @param huffmanCodes 哈夫曼编码表
* @return 返回解码过的字节数组
*/
public static byte[] decode(byte[] bytes, Map<Byte, String> huffmanCodes) {
StringBuffer builder = new StringBuffer();
// for (byte aByte : bytes) {
// builder.append(Integer.toBinaryString(Byte.toUnsignedInt(aByte)));
// //Byte.toUnsignedInt(byte b) 将一个字节类型的数据转换成对应的十进制无符号Int类型
// //Integer.toBinaryString(int i) 将一个整型数据转换成二进制字符串
// }
for (int i = 0; i < bytes.length; i++) {
boolean flag = (i == bytes.length - 1);//如果是最后一个不够8位的 返回真 传入方法为假
if (i != bytes.length - 1) {
builder.append(byteToString(bytes[i], !flag));
} else {//考虑到最后一位可能是存在少0的情况 比如byte【】 中存储1 ,对应的二进制编码可能是001 或者 0001,但是我们解压只返回了1
String s = byteToString(bytes[i], !flag);
int length = s.length();
// while (length + builder.length() != lastLength) {
// builder.append(0);//一直增加
// }
builder.append(s);
}
}
System.out.println("解码后的二进制=" + builder + " length=" + builder.length());
//将哈夫曼编码表的k和v进行反转 加入到另一个集合中 方便和二进制字符串进行匹配
HashMap<String, Byte> newMap = new HashMap<>();
for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
newMap.put(entry.getValue(), entry.getKey());
}
//遍历StringBuilder 进行匹配
ArrayList<Byte> list = new ArrayList<>();//暂存byte
for (int i = 0; i < builder.length(); ) {
int count = 1;//小的计数器 每次都多向后扫描一位 直到扫描到的字符串再map中有对应的value
Byte aByte = null;
while (true) {
String substring = "";
// if (i + count < builder.length()) {
substring = builder.substring(i, i + count);
// } else {
// String substring1 = builder.substring(i);
// aByte = newMap.get(substring1);
// break;
// }
aByte = newMap.get(substring);
if (aByte == null) {//获取不到 count就依次+1 直到匹配到
count++;
} else {
break;//不为null的话 说明匹配到
}
}
//当跳出while循环 说明扫描到哈夫曼编码对应的字符
if (aByte != null) {
list.add(aByte);
}
i += count;//更新i 从下一次位置开始截取进行匹配
}
byte[] b = new byte[list.size()];
int index = 0;
//将list集合中的数据存放到byte数组中返回
for (Byte aByte : list) {
b[index++] = aByte;
}
return b;
}
/**
* 将main方法里面的一系列方法封装成一个方法
*
* @param bytes 传入的数据得到的字节数组
* @return 返回的是一个哈夫曼编码的字节数组
*/
private static byte[] huffmanZip(byte[] bytes) {
//把字节数组转换成一个存放Node结点的List集合
List<Node> nodes = huffmanCode(bytes);
System.out.println("转换成的Node结点=" + nodes + " length=" + nodes.size());
//将此集合中的Node结点构建成一个哈夫曼树
Node root = huffmanTree(nodes);
System.out.println("===前序遍历===");
preOrder(root);
System.out.println();
//根据哈夫曼树创建哈夫曼编码表 存放在HashMap中
Map<Byte, String> huffmanCodes = getCodes(root);
//当执行完上面那句话 哈夫曼编码表就已经形成
System.out.println("哈夫曼表=" + hm);
//把字节数组根据哈夫曼编码表进行压缩
return zip(bytes, huffmanCodes);
}
/**
* 将一个字节数组根据哈夫曼编码表压缩成二进制数据 然后再由每8位二进制组成一个字节依次存放在字节数组中
*
* @param bytes 需要压缩数据对应的字节数组
* @param huffmanCodes 哈夫曼编码表
* @return 返回的是一个压缩的二进制数据每8位组成的字节数组
*/
public static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
//1,转换成二进制数据用stringBuilder进行拼接
StringBuilder stringBuilder = new StringBuilder();
for (byte aByte : bytes) {
String s = huffmanCodes.get(aByte);
stringBuilder.append(s);
}
lastLength = stringBuilder.length();//这个length此时为整个二进制字符串的长度
System.out.println("二进制数据=" + stringBuilder + " length=" + stringBuilder.length());
//2,每8位作为一个字节存放在辅助字节数组中
// int len = 0;
// int length = stringBuilder.length();
// if (length % 8 == 0) {
// len = length / 8;
// } else {
// len = length / 8 + 1;
// }
//计算byte数组长度另一种写法 一句话搞定
int len = (stringBuilder.length() + 7) / 8;
byte[] tempBytes = new byte[len];//len = 17
int t = 0;//辅助字节数组的指针
while (t < len) {
if (t == len - 1) {
// String s = stringBuilder.substring(0, stringBuilder.length());
String s = stringBuilder.substring(0);//和上面那句话等价 从0号索引开始取 有多少位就取多少位
// lastLength = s.length();
System.out.println("最后截取的数据=" + s + " length=" + lastLength);
byte b = (byte) Integer.parseInt(s, 2);
// System.out.println(b);
tempBytes[t] = b;
break;
}
String substring = stringBuilder.substring(0, 8);
System.out.println("每次截取的数据=" + substring);//10101000
tempBytes[t++] = (byte) Integer.parseInt(substring, 2);
stringBuilder.delete(0, 8);
}
return tempBytes;
}
//得到哈夫曼编码表的方法重载
public static Map<Byte, String> getCodes(Node root) {
if (root == null) {
System.out.println("空树");
return null;
}
if (root.left != null) {//向左子树递归
getCodes(root.left, sb, "0");
}
if (root.right != null) {//向右子树递归
getCodes(root.right, sb, "1");
}
return hm;
}
/**
* 将一个哈夫曼树从根结点到每一个叶子结点的路径转换成哈夫曼编码存放到map集合中 得到 哈夫曼编码表
*
* @param node 传入的结点
* @param stringBuilder 用于拼接哈夫曼编码
* @param code 路径 规定左0 右1
*/
public static void getCodes(Node node, StringBuilder stringBuilder, String code) {
StringBuilder sb2 = new StringBuilder(stringBuilder);
//每递归一次就创建一个新的StringBuilder对象 并且这个对象和递归传进来的stringBuilder字符序列相等
sb2.append(code);//拼接
if (node != null) {
if (node.data == null) {//说明传入的是非叶子结点 递归
//向左递归
getCodes(node.left, sb2, "0");
//向右递归
getCodes(node.right, sb2, "1");
} else {//说明是叶子结点 说明到了叶子结点
hm.put(node.data, sb2.toString());
}
}
}
/**
* 功能:统计一个字节数组里的每个字符的个数 创建成结点存放在List中 返回
* Node结点的形式如Node[date=97,weight=5]
*
* @param bytes 要压缩的字节数组
* @return 返回一个存放Node结点的List集合
*/
public static List<Node> huffmanCode(byte[] bytes) {
HashMap<Byte, Integer> hm = new HashMap<>();//存放结点的数据和权重
//充分利用hashMap存放数据不能重复的原理 如果key重复就会替换value
for (byte b : bytes) {
Integer weight = hm.get(b);
//第一次取出都为null 当第二次取出的时候是1 再存放时就+1
if (weight == null) {
hm.put(b, 1);
} else {
hm.put(b, weight + 1);
}
}
ArrayList<Node> nodes = new ArrayList<>();
for (Map.Entry<Byte, Integer> entry : hm.entrySet()) {
nodes.add(new Node(entry.getKey(), entry.getValue()));
}
return nodes;
}
/**
* 将一个集合中Node结点以权重为标准转换成一个哈夫曼树
*
* @param list
* @return 返回哈夫曼树的根节点
*/
public static Node huffmanTree(List<Node> list) {
while (list.size() > 1) {
//排序
Collections.sort(list, new Comparator<Node>() {
@Override
public int compare(Node o1, Node o2) {
return o1.weight - o2.weight;
}
});
//取出前两个结点 生成父节点 构建关系 并将前两个结点从集合中删除 父节点加入到集合中
Node leftNode = list.get(0);
Node rightNode = list.get(1);
Node parent = new Node(null, leftNode.weight + rightNode.weight);
parent.left = leftNode;
parent.right = rightNode;
list.remove(leftNode);
list.remove(rightNode);
list.add(parent);
}
return list.get(0);
}
public static void preOrder(Node node) {
if (node != null) {
node.preOrder();
} else {
System.out.println("哈夫曼树为空");
}
}
}
class Node {
Byte data;//存放ascii码值 对应一个字符 不得不说0对应的ascii码就是null
//但是存放ascii码的还是需要定义为包装类 因为生成的父节点没有对应的字符 所以要传入一个null值
int weight;//权重 字符出现的次数
Node left;
Node right;
public Node(int weight) {
this.weight = weight;
}
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 +
'}';
}
}