赫夫曼编码详解
说明
- 赫夫曼编码是基于赫夫曼树的一种文件压缩算法,常用于文件的编码,压缩效率较高
- 本文主要基于赫夫曼编码说明,对于赫夫曼树见前一篇
- 大致思路如下:
- 因为任何类型的文件在计算机底层存储的时候都是以字符的形式存储的,因此文件的压缩主要是对字符的优化处理
- 先创建节点类,应当包含当前字符,权值,左右子节点属性,权值为当前字符在文件中出现的总的次数,节点类要实现Comparable接口,方便使用Collections接口的排序方法
- 通过算法设计获取到文件中不同的字符及其出现的权值后,然后将这个字符及其权值封装为一个Node节点,文件中总共有多少个字符,那么就会产生多少个节点,然后将这些节点再封装到集合中
- 通过对集合中这些节点按照权值进行排序,生成一颗赫夫曼树,注意由于可能存在多个字符的权值是一样的,因此生成的赫夫曼树可能存在差异,但是总的WPL值是一样的,因此压缩效率也是一样的
- 生成赫夫曼树后,需要对每个字符进行编码,因为每个字符都处于赫夫曼树的叶子节点,因此假定每个节点的向左路径为 0,向右路径为 1,那么每个叶子节点就能唯一的对应于一种编码,即每个字符唯一对应一种编码,而且是前缀编码,不定长编码
- 将每个字符及其对应的唯一编码存储于具有映射关系的HashMap中,那么就可以通过字符获取到其所对应的唯一编码
- 经过上述步骤,已然生成某种规则的赫夫曼编码表
- 然后再扫描该文件中所有字符,将所有字符用编码表中的唯一编码进行替换,替换完之后的文件应当为一大推二进制文件
- 然后将这些二进制文件进行转字符的处理,即每8个为一个字符,即将其转换为10进制,减少空间
- 到此已经完成了文件的编码,即压缩
- 具体设计及算法实现见下
源码及分析
package algorithm.tree.huffmancode;
import java.util.*;
/**
* @author AIMX_INFO
* @version 1.0
*/
public class HuffmanCode {
public static void main(String[] args) {
//测试
String content = "i like like like java do you like a java";
//将字符串转字节数组
byte[] bytes = content.getBytes();
//压缩
byte[] huffmanCodeByte = huffmanZip(bytes);
//查看效果
System.out.println(Arrays.toString(huffmanCodeByte));
}
//将压缩的所有方法封装
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;
}
}
//节点类
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;
}
}