赫夫曼编码
基本介绍
- 赫夫曼编码也翻译为哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,属于一种程序算法。
- 赫夫曼编码是赫夫曼树在电讯通信中的经典应用之一,
- 赫夫曼编码广泛地应用于数据文件压缩,其压缩率通常在 20%~90% 之间。
- 赫夫曼编码是可变字长编码(VLC)的一种,Huffman 于 1952 年提出的一种编码。
注意:这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是 wpl 是一样的,都是最小的。
数据压缩
思路分析
功能:根据赫夫曼编码压缩数据的原理,需要创建"i like like like java do you like a java"对应的赫夫曼树
思路:
- Node {data(存放数据),weight(权值),left,right}
- 得到"i like like like java do you like a java"对应的 byte[] 数组
- 编写一个方法将准备构建赫夫曼树的 Node 节点放到 List,形式:[Node[data = 97,weight = 5],…],体现 d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 [空格]:9
- 可以通过 List 创建对应的赫夫曼树
- 将字符和权值加入到 List 集合
- 将得到的赫夫曼树的所有叶子结点的赫夫曼编码得到,并放入到 huffmanCodes 集合,得到赫夫曼编码表(规定:左子节点的路径为 0,右子节点的路径为 1)
- 将字符串对应的数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码压缩后的数组
代码实现
public class HuffmanCode {
public static void main(String[] args) {
String content = "i like like like java do you like a java";
byte[] contentBytes = content.getBytes();
byte[] huffmanCodeBytes = huffmanZip(contentBytes);
System.out.println(Arrays.toString(huffmanCodeBytes));
}
/**
* 封装方法,便于调用
*
* @param bytes 原始的字符串对应的字节数组
* @return 是经过赫夫曼编码压缩过后的字节数组
*/
private static byte[] huffmanZip(byte[] bytes) {
// 将字符和权值加入到 List 集合
List<Node> nodes = getNodes(bytes);
// 通过 List 创建对应的赫夫曼树
Node huffmanTreeRoot = createHuffmanTree(nodes);
// 生成赫夫曼编码
Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
// 根据生成的赫夫曼编码,将字符数组进行压缩,得到压缩后的字符数组
return zip(bytes, huffmanCodes);
}
/**
* 编写方法,将字符串对应的 byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码压缩后的 byte[]
*
* @param bytes 原始字符串对应的 byte[]
* @param huffmanCodes 生成的赫夫曼编码 map
* @return 返回赫夫曼编码处理后的 byte[]
*/
private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
// 1. 利用 StringBuilder 将 bytes 转成赫夫曼编码
StringBuilder stringBuilder = new StringBuilder();
// 遍历 bytes
for (byte b : bytes) {
stringBuilder.append(huffmanCodes.get(b));
}
// 将拼接的字符串转为 byte[]
// 统计返回 byte[] huffmanCodeBytes 长度
int len;
if (stringBuilder.length() % 8 == 0) {
len = stringBuilder.length() / 8;
} else {
len = stringBuilder.length() / 8 + 1;
}
// 创建存储压缩后的 byte 数组
byte[] huffmanCodeBytes = new byte[len];
int index = 0; // 记录是第几个 byte
for (int i = 0; i < stringBuilder.length(); i += 8) { // 因为是每 8 位对应一个 byte,所以步长为 8
String stByte;
if (i + 8 > stringBuilder.length()) { // 不够 8 位
stByte = stringBuilder.substring(i);
} else {
stByte = stringBuilder.substring(i, i + 8);
}
// 将 strByte 构成一个 byte,放入到 huffmanCodeBytes
huffmanCodeBytes[index] = (byte) Integer.parseInt(stByte, 2);
index++;
}
return huffmanCodeBytes;
}
// 生成赫夫曼树对应的赫夫曼编码
// 思路
// 1. 将赫夫曼编码表存放在 Map<Byte,String> 形式
static Map<Byte, String> huffmanCodes = new HashMap<>();
// 2. 在生成赫夫曼编码表时,需要去拼接路径,,定义一个 StringBuilder 存储某个叶子结点的路径
static StringBuilder stringBuilder = new StringBuilder();
// 重载 getCodes
private static Map<Byte, String> getCodes(Node root) {
if (root == null) {
return null;
} else {
// 处理 root 的左子树
getCodes(root.left, "0", stringBuilder);
// 处理 root 的右子树
getCodes(root.right, "1", stringBuilder);
return huffmanCodes;
}
}
/**
* 功能:将传入的 node 的节点的所有的叶子结点的赫夫曼编码得到,并放入到 huffmanCodes 集合
*
* @param node 传入节点
* @param code 路径,左子节点 0,右子节点 1
* @param stringBuilder 拼接路径
*/
private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
// 将 code 加入到 stringBuilder2
stringBuilder2.append(code);
if (node != null) { // 如果 node == null,不处理
// 判断当前 node 是叶子结点还是非叶子结点
if (node.data == null) { // 非叶子结点
// 递归
// 向左
getCodes(node.left, "0", stringBuilder2);
// 向右
getCodes(node.right, "1", stringBuilder2);
} else { // 叶子结点
// 表示找到某个叶子结点的最后
huffmanCodes.put(node.data, stringBuilder2.toString());
}
}
}
// 将字符和权值加入到 List 集合
private static List<Node> getNodes(byte[] bytes) {
// 1.创建一个 ArrayList
ArrayList<Node> nodes = new ArrayList<>();
// 遍历 bytes,统计每个字符出现的次数
HashMap<Byte, Integer> count = new HashMap<>();
for (byte b : bytes) {
count.put(b, count.containsKey(b) ? count.get(b) + 1 : 1);
}
// 把每个键值对转为 Node 对象 并加入 nodes 集合
for (Map.Entry<Byte, Integer> entry : count.entrySet()) {
nodes.add(new Node(entry.getKey(), entry.getValue()));
}
return nodes;
}
// 通过 List 创建对应的赫夫曼树
private static Node createHuffmanTree(List<Node> nodes) {
while (nodes.size() > 1) {
// 排序
Collections.sort(nodes);
// 取出第一棵最小二叉树
Node leftNode = nodes.remove(0);
// 取出第二棵最小二叉树
Node rightNode = nodes.remove(0);
// 创建一棵新的二叉树,他的根节点没有 data 只有权值
Node parent = new Node(null, leftNode.weight + rightNode.weight);
parent.left = leftNode;
parent.right = rightNode;
// 将新的二叉树加入到 Node
nodes.add(parent);
}
// nodes 最后的节点就是根节点
return nodes.get(0);
}
}
// 创建 Node,存放数据和权值
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 this.weight - o.weight;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
}
数据解压
思路分析
- 将压缩后的数组转成二进制字符串
- 通过赫夫曼编码表,将二进制字符串重新转换成原来的字符串
代码实现
// 解压
// 思路:
// 1. 将 huffmanCodeBytes 转成赫夫曼编码对应的二进制字符串
// 2. 赫夫曼编码对应的二进制字符串对照赫夫曼编码转成原来的字符串
/**
* 编写一个方法完成解码
*
* @param huffmanCodes 赫夫曼编码表
* @param huffmanBytes 赫夫曼编码得到的字节数组
* @return 返回原来的字符串对应的数组
*/
private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
// 1. 先得到 HuffmanBytes 对应的二进制的字符串
StringBuilder stringBuilder = new StringBuilder();
// 2. 将 byte 数组换成二进制字符串
for (int i = 0; i < huffmanBytes.length; i++) {
// 判断是不是最后一个字节
boolean flag = (i == huffmanBytes.length - 1);
stringBuilder.append(byteBitString(!flag, huffmanBytes[i]));
}
// 把字符串指定的赫夫曼编码进行解码
// 把赫夫曼编码表进行调换
Map<String, Byte> map = new HashMap<>();
for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
map.put(entry.getValue(), entry.getKey());
}
// 创建集合,存放 byte
List<Byte> list = new ArrayList<>();
// i 可以理解成索引,扫描 stringBuilder
for (int i = 0; i < stringBuilder.length(); ) {
int count = 1; // 计数器,用于扫描
boolean flag = true;
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; // i 直接移动到 count
}
// 当 for 循环结束后,我们 list 中就存放了所有的字符
// 把 list 放入 byte 数组
byte[] b = new byte[list.size()];
for (int i = 0; i < b.length; i++) {
b[i] = list.get(i);
}
return b;
}
/**
* 将一个 byte 转换成二进制的字符串
*
* @param b 需要转换的 byte
* @param flag 标识是否需要补高位
* @return 返回二进制字符串
*/
private static String byteBitString(boolean flag, byte b) {
// 使用变量保存 b
int temp = b; // 将 b 转为 int
// 如果是整数我们需要补高位
if (flag) {
temp |= 256;
}
String str = Integer.toBinaryString(temp); // 返回的是 temp 对应的二进制补码
if (flag) {
return str.substring(str.length() - 8);
} else {
return str;
}
}
文件压缩
/**
* 编写方法,将一个文件进行压缩
*
* @param srcFile 传入的希望压缩的文件全路径
* @param dstFile 压缩后将压缩文件放到哪个目录
*/
public static void zipFile(String srcFile, String dstFile) {
// 创建输出流
OutputStream os = null;
ObjectOutputStream oos = null;
// 创建输入流
FileInputStream is = null;
try {
// 创建输入流
is = new FileInputStream(srcFile);
// 创建一个和源文件大小一样的 byte[]
byte[] b = new byte[is.available()];
// 读取文件
is.read(b);
// 获取赫夫曼编码表
// 对文件进行压缩
byte[] huffmanZip = huffmanZip(b);
// 创建文件输出流
os = new FileOutputStream(dstFile);
// 创建一个和文件输出流关联的 ObjectOutputStream
oos = new ObjectOutputStream(os);
// 把赫夫曼编码后的数组写入压缩文件
oos.writeObject(huffmanZip);
// 这里以对象流的方式写入赫夫曼编码,是为了以后恢复源文件时使用
oos.writeObject(huffmanCodes);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
is.close();
os.close();
oos.close();
} catch (Exception 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 数组
byte[] huffmanBytes = (byte[]) ois.readObject();
// 读取赫夫曼编码表
Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();
// 解码
byte[] bytes = decode(huffmanCodes, huffmanBytes);
// 将 bytes 写入目标文件
os = new FileOutputStream(dstFile);
// 写数据到文件
os.write(bytes);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
os.close();
ois.close();
is.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}