赫夫曼编码
1.基本介绍
(1)赫夫曼编码也翻译为哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,属于一种程序算法。
(2)赫夫曼编码是赫夫曼树在电讯通信中的经典应用之一。
(3)赫夫曼编码广泛的应用与数据文件压缩,其压缩率通常在20%~90%之间。
(4)赫夫曼编码是可变长编码(VLC)的一种,Huffman于1952年提出一种编码方式,称之为最佳编码。
2.原理剖析
2.1.定长编码
通讯领域中信息的处理方式1:定长编码
- i like like like java do you like a java //共40个字符(包括空格)
- 105 32 108 105 107 101 32 108 105 107 101 32 108 105 107 101 32 106 97 118 97 32 100 111 32 121 111 117 32 108 105 107 101 32 97 32 106 97 118 97 //对应的ASCII值(包括括号)
- 1101001 100000 1101100 1101001 1101011 1100101 100000 1101100 1101001 1101011 1100101 100000 1101100 1101001 1101011 1100101 100000 1101010 1100001 1110110 1100001 100000 1100100 1101111 100000 1111001 1101111 1110101 100000 1101100 1101001 1101011 1100101 100000 1100001 100000 1101010 1100001 1110110 1100001 //对应的二进制(包括空格)
- 按照二进制来传递信息,总的长度是320
2.2.变长编码
通讯领域中信息的处理方式2:变长编码
-
i like like like java do you like a java //共40个字符(包括空格)
-
d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 空格:9 //各个字符对应出现的次数
-
0=空格,1=a,10=i,11=e,100=k,101=l,110=o,111=v,1000=j,1001=u,1010=y,1011=d 说明:按照各个字符出现的次数进行编码,原则是出现次数越多,则编码越小,比如空格出现了9次,编码为0,其他依次类推
-
按照上边给的各个字符规定的编码方式,则在传输“i like like like java do you like a java”数据时,对应的编码方式就是:1001011010011010110100110101101001101000111110101111001010110100101011010011010100011111
-
此时按照新的编码方式来传递信息,总的长度是91
注意:此时信息接收方在收到二进制数据后,将其进行原文转换的时候,会出问题。不确定该二进制数据的分割,例如:第一次是取1,还是取10,还是取101,还是取1010,后边依次是该问题。会转换出多种原文,产生错误,具有多义性。
在此提出两个全新的概念:前缀编码、无前缀编码
前缀编码:对字符集进行编码时,要求字符集中任一字符的编码都不是其它字符的编码的前缀
无前缀编码:对字符集进行编码时,某一字符的编码可能是其他字符的编码的前缀
2.3.赫夫曼编码
通讯领域中信息的处理方式3:赫夫曼编码
-
i like like like java do you like a java //共40个字符(包括空格)
-
d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 空格:9 //各个字符对应出现的次数
-
按照上边各个字符出现的次数构建一棵赫夫曼树,次数作为权值,如图

-
按照上边的赫夫曼编码,在传输“i like like like java do you like a java”数据时,对应的编码方式就是:10101001101111011110100110111101111010011011110111101101110000111001100110100001100111100010010010011011110111101110011011100001110
-
此时按照新的编码方式来传递信息,总的长度是133
说明
(1)原来长度是320,压缩了 (320-133)/320=58.4375%
(2)此编码满足前缀编码,即字符的编码都不是其他字符编码的前缀。不会造成转换原文的多义性
(3)这种压缩方式为无损压缩
注意:此时要保证转换原文没有多义性,在构建二叉树时,节点的权值一样时,要严格保证权值一样的节点的顺序性。否则,每个字符所在的叶子节点的路径即发生了变化,路径发生变化,每个字符的编码值也就不一样了,转换原文自然就出错了。
3.编码实战
3.1.压缩/解压缩-字符串
3.1.1.压缩字符串
- 将原始字符串对应的字节数组转为List,并统计好每个字符出现的次数
- 根据上一步创建好的List构建一棵赫夫曼树,字符出现的次数作为其权值
- 通过构建好的赫夫曼树得到每个叶子节点对应的赫夫曼编码字符集Map<byte, String>
- 将原始字符串按照赫夫曼编码的方式,获取其对应的全新的字节数组
3.1.2.解压字符串
- 将按照赫夫曼编码压缩过之后的字节数组转为二进制字符串
- 将赫夫曼编码表中的key和value互换
- 将赫夫曼编码对应的二进制字符串,按照赫夫曼编码,转为原始字节数组
3.2.压缩/解压缩-文件
3.2.1.压缩文件
- 获取当前文件的输入流
- 利用当前文件输入流将当前文件中的内容读成一个byte[]
- 获取目标压缩文件的输出流
- 使用ObjectOutputStream写出压缩文件(originFileHuffmanZipBytes + huffmanCodeMap)
注意:解压时需要赫夫曼编码表
3.2.2.解压文件
-
获取当前压缩文件的输入流
-
使用ObjectInputStream将压缩文件进行读取(originFileHuffmanZipBytes + huffmanCodeMap)
-
结合Huffman编码表解压经过Huffman编码生成的byte[](originFileHuffmanZipBytes)
-
获取原始文件的输出流,将原始文件写出去
3.3.实战的编码
package com.zk.datastruct.huffman.code;
import java.io.*;
import java.util.*;
/**
* @Description:
* @ClassName: HuffmanCodeing
* @Author: ZK
* @Date: 2021/1/31 23:04
* @Version: 1.0
*/
public class HuffmanCoding {
public static void main(String[] args) {
String str = "i like like like java do you like a java";
// 测试字符串压缩
byte[] huffmanZipBytes = getHuffmanZipBytes(str.getBytes());
byte[] originBytes = decode(huffmanZipBytes, huffmanCodeMap);
// 测试文件压缩
huffmanZipFile("/Users/zk/Desktop/test.png", "/Users/zk/Desktop/test.zip");
unHuffmanZipFile("/Users/zk/Desktop/test.zip", "/Users/zk/Desktop/test2.png");
}
private static Map<Byte, String> huffmanCodeMap = new HashMap<>();
/**
* 将原始字节数组转换为 Node 的集合,并统计好每个字符出现的次数
* @param strBytes 原始字符串
* @return Node 的集合,包含数据和权重
*/
private static List<Node> getNodes(byte[] strBytes){
List<Node> nodes = new ArrayList<>();
Map<Byte, Integer> map = new HashMap<>();
for (byte b : strBytes) {
byte key = b;
Integer value = map.get(key);
if (value != null) {
map.put(key, value+1);
} else {
map.put(key, 1);
}
}
for (Map.Entry<Byte, Integer> entry : map.entrySet()) {
Byte data = entry.getKey();
Integer weight = entry.getValue();
Node node = new Node(data, weight);
nodes.add(node);
}
return nodes;
}
/**
* 创建赫夫曼树
* @param nodes
* @return 赫夫曼树的根节点
*/
private static Node createHuffmanTree(List<Node> nodes){
while(nodes.size() != 1){
// 1.从小到大排序
Collections.sort(nodes);
// 2.每次取出前两个最小的,并移除掉
Node left = nodes.remove(0);
Node right = nodes.remove(0);
// 3.将取出来的两个节点构成一个新的节点,并放入到nodes中
Node parent = new Node(null, left.weight + right.weight);
parent.left = left;
parent.right = right;
nodes.add(parent);
}
return nodes.get(0);
}
/**
* 前序遍历
* @param root
*/
private static void preOrder(Node root){
if (root == null) {
System.out.println("当前这棵树为空,无法遍历");
}
root.preOrder();
}
/**
* 获取一个赫夫曼树的赫夫曼编码
* @param root 当前赫夫曼树的根节点
* @return map key:字符 value:赫夫曼编码
*/
private static Map<Byte, String> getHuffmanCode(Node root){
Map<Byte, String> map = new HashMap<>();
StringBuilder sb = new StringBuilder();
getCodes(root, "", sb, map);
huffmanCodeMap = map;
return map;
}
/**
* 生成赫夫曼编码的递归程序
* @param node 当前节点
* @param code 向左"0",向右"1"
* @param sb 记录路径的StringBuilder
* @param map 存放结果
*/
private static void getCodes(Node node, String code, StringBuilder sb, Map<Byte, String> map){
// 遍历时,递归一层使用一个新的对象,回溯时自然采用本层的sb对象
// 相当于前序遍历,处理当前节点
StringBuilder sb2 = new StringBuilder(sb);
// 记录路径
sb2.append(code);
// 如果当前节点是一个根节点,保存到结果map中
if (node.data != null) {
map.put(node.data, sb2.toString());
}
// 向左走
if (node.left != null) {
getCodes(node.left, "0", sb2, map);
}
// 向右走
if (node.right != null) {
getCodes(node.right, "1", sb2, map);
}
}
/**
* 将原始字符串对应的字节数组按照赫夫曼编码的格式进行压缩
* @param strBytes 原始字符串对应的字符数据
* @param huffmanCodeMap 赫夫曼编码格式
* @return 压缩后的字符数组
*/
private static byte[] zip(byte[] strBytes, Map<Byte, String> huffmanCodeMap){
StringBuilder sb = new StringBuilder();
for (byte strByte : strBytes) {
// 按照赫夫曼编码格式重新对原始数据进行编码
sb.append(huffmanCodeMap.get(strByte));
}
int len = (sb.length() + 7) / 8;
byte[] res = new byte[len];
for (int i = 0; i < sb.length(); i+=8) {
int begin = i;
int end = i+8 > sb.length() ? sb.length() : i+8;
String resStr = sb.substring(begin, end);
res[i/8] = (byte)Integer.parseInt(resStr, 2);
}
return res;
}
/**
* 将一个字符串按照赫夫曼编码的格式获取其对应的字节数组
* @param strBytes 需要编码的字节数组
* @return 按照赫夫曼编码的格式得到的字节数组
*/
private static byte[] getHuffmanZipBytes(byte[] strBytes){
// 1.将原始字符串转为List<Node>
List<Node> nodes = getNodes(strBytes);
// 2.创建赫夫曼树
Node root = createHuffmanTree(nodes);
// 3.得到赫夫曼编码的字符集map
Map<Byte, String> huffmanCodeMap = getHuffmanCode(root);
// 4.压缩
byte[] bytes = zip(strBytes, huffmanCodeMap);
return bytes;
}
//=====================================================================================
// 解压
/**
* 将赫夫曼编码得到的字节数组,转为原始字符串对应的字节数组
* @param huffmanZipBytes 赫夫曼编码得到的字节数组
* @param huffmanCodeMap 赫夫曼编码表 map
* @return
*/
private static byte[] decode(byte[] huffmanZipBytes, Map<Byte, String> huffmanCodeMap){
// 1.将按照赫夫曼编码压缩过之后的字节数组转为二进制字符串
StringBuilder binaryString = new StringBuilder();
int zipBytesLen = huffmanZipBytes.length;
for (int i = 0; i < zipBytesLen; i++) {
byte zipByte = huffmanZipBytes[i];
String bitString = byteToBitString(zipByte);
// 如果是最后一个字节,则将其前边的0去掉
if (i == zipBytesLen-1) {
bitString = bitString.replaceAll("^(0+)", "");
}
binaryString.append(bitString);
}
// 2.将赫夫曼编码表中的key和value互换
// Map<Byte, String> => Map<String, Byte>
Map<String, Byte> turnHuffmanCodeMap = new HashMap<>();
for (Map.Entry<Byte, String> entry : huffmanCodeMap.entrySet()) {
turnHuffmanCodeMap.put(entry.getValue(), entry.getKey());
}
// 3.将赫夫曼编码对应的二进制字符串,按照赫夫曼编码,转为原始字节数组
int binaryStringLen = binaryString.length();
List<Byte> resByteList = new ArrayList<>();
StringBuilder binaryCode = new StringBuilder();
for (int i = 0; i < binaryStringLen; i++) {
char binaryChar = binaryString.charAt(i);
String binaryHuffmanCode = binaryCode.append(binaryChar).toString();
if (turnHuffmanCodeMap.containsKey(binaryHuffmanCode)) {
Byte resByte = turnHuffmanCodeMap.get(binaryHuffmanCode);
resByteList.add(resByte);
binaryCode.delete(0, binaryCode.length());
}
}
// 4.将resByteList中的原始字节转存到一个数组中,并返回
byte[] resByteArr = new byte[resByteList.size()];
for (int i = 0; i < resByteList.size(); i++) {
resByteArr[i] = resByteList.get(i);
}
return resByteArr;
}
/**
* 将一个字节,转为二进制字符串,如果不够8位,则前边补0
* @param b 要转的字节
* @return 当前字节对应的8位二进制数组
*/
private static String byteToBitString(byte b){
return String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0');
}
//=====================================================================================
// 文件压缩和解压
/**
* 文件的压缩
* @param originFilePath 源文件的地址
* @param targetZipFilePath 压缩后压缩文件的地址
*/
private static void huffmanZipFile(String originFilePath, String targetZipFilePath){
InputStream is = null;
OutputStream os = null;
ObjectOutputStream oos = null;
try {
// 获取当前文件的输入流
is = new FileInputStream(originFilePath);
// 创建一个和源文件大小一样的byte[]
byte[] originFileBytes = new byte[is.available()];
// 读取文件到byte[]中
is.read(originFileBytes);
// 对源文件进行压缩
byte[] originFileHuffmanZipBytes = getHuffmanZipBytes(originFileBytes);
// 获取目标压缩文件的输出流
os = new FileOutputStream(targetZipFilePath);
// 使用ObjectOutputStream写出压缩文件(originFileHuffmanZipBytes + huffmanCodeMap)
oos = new ObjectOutputStream(os);
// 把赫夫曼编码后文件对应的byte[]写出去
oos.writeObject(originFileHuffmanZipBytes);
// 把赫夫曼编码表写出去 注意:解压时需要赫夫曼编码表
oos.writeObject(huffmanCodeMap);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
oos.close();
os.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 压缩文件的解压
* @param originZipFilePath 解压文件的地址
* @param targetFilePath 解压后源文件的地址
*/
private static void unHuffmanZipFile(String originZipFilePath, String targetFilePath){
InputStream is = null;
ObjectInputStream ois = null;
OutputStream os = null;
try {
// 获取当前压缩文件的输入流
is = new FileInputStream(originZipFilePath);
// 使用ObjectInputStream将压缩文件进行读取(originFileHuffmanZipBytes + huffmanCodeMap)
ois = new ObjectInputStream(is);
// 读取原始文件经过Huffman编码生成的byte[]
byte[] originFileHuffmanZipBytes = (byte[])ois.readObject();
// 读取原始文件对应的huffman编码表
Map<Byte, String> huffmanCodeMap = (Map<Byte, String>)ois.readObject();
// 解压 经过Huffman编码生成的byte[]
byte[] originFileBytes = decode(originFileHuffmanZipBytes, huffmanCodeMap);
// 获取原始文件的输出流
os = new FileOutputStream(targetFilePath);
// 将原始文件写出去
os.write(originFileBytes);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
os.close();
ois.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 创建赫夫曼树的Node节点,带数据和权值
*/
class Node implements Comparable<Node>{
Byte data; //数据:'a' -> 97
int weight; //权值:a出现的次数
Node left;
Node right;
public Node(Byte data, int weight){
this.data = data;
this.weight = weight;
}
@Override
public String toString() {
return "Node [data:" + data + ",weight:" + weight + "]";
}
@Override
public int compareTo(Node o) {
return this.weight - o.weight;
}
/**
* 前序遍历
*/
public void preOrder(){
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
}
4.赫夫曼编码压缩文件注意事项
- 如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率将不会有明显变化,比如视频、ppt等文件
- 赫夫曼编码时按字节来处理的,因此可以处理所有的文件(二进制文件、文本文件)
- 如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显
本文详细介绍了赫夫曼编码的概念、原理和实战应用,包括定长编码、变长编码以及无前缀编码。通过对比,突出赫夫曼编码的无损压缩特性,展示了如何构建赫夫曼树并生成编码。此外,还提供了字符串和文件的压缩与解压缩实现代码,强调了赫夫曼编码在文件压缩中的作用和注意事项。
399

被折叠的 条评论
为什么被折叠?



