赫夫曼编码
赫夫曼树
基本介绍
1.给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree), 还有的书翻译为霍夫曼树。
2.赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
重要概念
1.路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1
2.结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积
3.树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL(weighted path length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。
4.WPL最小的就是赫夫曼树
举例说明
思路分析
- 构成赫夫曼树的步骤:
1.从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
2.取出根节点权值最小的两颗二叉树
3.组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
4.再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树
代码示例见下方的最佳实践 - 文件压缩
赫夫曼编码介绍
基本介绍
1.赫夫曼编码也翻译为 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法
2.赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。
3.赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间
4.赫夫曼码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,称之为最佳编码
原理剖析
通信领域中信息的处理方式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码
01101001 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101010 01100001 01110110 01100001 00100000 01100100 01101111 00100000 01111001 01101111 01110101 00100000 01101100 01101001 01101011 01100101 00100000 01100001 00100000 01101010 01100001 01110110 01100001 //对应的二进制
按照二进制来传递信息,总的长度是 359 (包括空格)
在线转码 工具 :https://www.mokuge.com/tool/asciito16/
通信领域中信息的处理方式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” 数据时,编码就是 10010110100…
字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码
通信领域中信息的处理方式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 // 各个字符对应的个数
按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值.
//根据赫夫曼树,给各个字符
//规定编码 , 向左的路径为0
//向右的路径为1 , 编码如下:
o: 1000 u: 10010 d: 100110 y: 100111 i: 101
a : 110 k: 1110 e: 1111 j: 0000 v: 0001
l: 001 : 01
按照上面的赫夫曼编码,我们的"i like like like java do you like a java" 字符串对应的编码为 (注意这里我们使用的无损压缩)
1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
长度为 : 133
说明:
原来长度是 359 , 压缩了 (359-133) / 359 = 62.9%
此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性
注意, 这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl 是一样的,都是最小的, 比如: 如果我们让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树为:
最佳实践 - 文件压缩
代码示例
/**
* @author haowu
* @create 2022-07-09-23:13
* @Description 赫夫曼编码 文件压缩
*/
public class HuffmanCode {
public static void main(String[] args) {
/* //40字节
String content = "i like like like java do you like a java";
byte[] contentBytes = content.getBytes();
List<Node> nodes = getNodes(contentBytes);
//根据nodes创建赫夫曼树
Node huffmanTree = createHaffmanTree(nodes);
//preOrder(huffmanTree);
//获取对应的赫夫曼编码
Map<Byte, String> huffmanCodes = getCodes(huffmanTree);
//压缩后为17字节 [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
byte[] huffmanZipByte = huffmanZip(contentBytes);
//System.out.println(Arrays.toString(huffmanZipByte));
//将数据进行解压(解码)
System.out.println(new String(decode(huffmanCodes, huffmanZipByte)));*/
System.out.println("-------------------------------");
zipFile("C:\\Users\\haowu\\Desktop\\资料\\面试宝典\\面试宝典1.pdf", "C:\\Users\\haowu\\Desktop\\资料\\面试宝典\\1.zip");
System.out.println("压缩文件成功");
unZipFile("C:\\Users\\haowu\\Desktop\\资料\\面试宝典\\1.zip","C:\\Users\\haowu\\Desktop\\资料\\面试宝典\\1.pdf");
System.out.println("解压文件成功");
}
//解压文件
public static void unZipFile(String srcFile, String dstFile){
try (final ObjectInputStream ois = new ObjectInputStream(new FileInputStream(srcFile));
FileOutputStream fos = new FileOutputStream(dstFile)){
byte[] huffmanByte = (byte[]) ois.readObject();
Map<Byte, String> huffmanCode = (Map<Byte, String>) ois.readObject();
byte[] decodeByte = decode(huffmanCode, huffmanByte);
fos.write(decodeByte);
}catch(IOException | ClassNotFoundException e){
e.printStackTrace();
}
}
//压缩文件
public static void zipFile(String srcFile,String dstFile){
try (FileInputStream fis = new FileInputStream(srcFile);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(dstFile))){
byte[] bytes = new byte[fis.available()];
fis.read(bytes);
//对源文件赫夫曼压缩
byte[] huffmanZip = huffmanZip(bytes);
//以对象流的方式写入压缩后的数据和赫夫曼编码
oos.writeObject(huffmanZip);
oos.writeObject(huffmanCodes);
} catch (IOException e) {
e.printStackTrace();
}
}
//解码
public static byte[] decode(Map<Byte,String> huffmanCode,byte[] bytes){
//将byte[]转为二进制字符串
StringBuilder stringBuilder = new StringBuilder();
byte b;
//for (int i = 0; i < bytes.length; i++) {
for (int i = 0; i < bytes.length-1; i++) {
//stringBuilder.append(byteToBitString(bytes[i], i!=bytes.length-1));
stringBuilder.append(byteToBitString(bytes[i], true));
}
//System.out.println("stringBuilder decode = " + stringBuilder.substring(stringBuilder.length()-6));
//System.out.println("stringBuilder decode = " + stringBuilder.length());
//重点: 单独处理一下最后一位byte ,不然解压时,最后一个字节转成二进制时,位数的不确定性可能导致解压后字符串错误。
// 比如,byte 存储为1,原先的编码后的字符串最后几位可能为 001 或者 0001。使用Integer中的字节转换二进制,解析byte1,
// 正数只会返回1,此时如果不做处理,解码后的字符串可能会少0,可能报异常或者解压错误
b = bytes[bytes.length-1];
String lastString = byteToBitString(b, false);
//将解压后的最后一次数据长度与压缩前的最后一次数据长度比较 如果方法不在一起 再以对象流的方式写入压缩后的数据和赫夫曼编码的时候
//可以多写入一个最后位数的对象流
if (lastString.length()==lengthRank){
stringBuilder.append(lastString);
}else {
stringBuilder.append(String.format("%"+lengthRank+"s", lastString).replace(" ","0"));
}
//System.out.println("stringBuilder decode = " + stringBuilder.substring(stringBuilder.length()-6));
//System.out.println("stringBuilder decode = " + stringBuilder.length());
//将赫夫曼编码key,value颠倒
Map<String,Byte> huffmanDecode =new HashMap<>();
for (Map.Entry<Byte, String> entry : huffmanCode.entrySet()) {
huffmanDecode.put(entry.getValue(),entry.getKey());
}
List<Byte> byteList= new ArrayList<>();
//将二进制字符串通过颠倒后的赫夫曼编码进行转换
for (int i = 0; i < stringBuilder.length(); ) {
int count = 1;
boolean flag = true;
String substring = null;
while (flag){
substring = stringBuilder.substring(i, i + count);
if (huffmanDecode.containsKey(substring)) {
flag = false;
}else {
count++;
}
}
byteList.add(huffmanDecode.get(substring));
i+=count;
}
byte[] arr = new byte[byteList.size()];
for (int i = 0; i < byteList.size(); i++) {
arr[i] =byteList.get(i);
}
return arr;
}
//将byte转成一个二进制的字符串(补码)
public static String byteToBitString(byte b, boolean flag){
int temp = b ;
//flag为true表示需要高位补码 最后一位不用高位补码
if (flag){
// 256: 1 0000 0000 |= temp
temp |= 256;
}
String binaryString = Integer.toBinaryString(temp);
if (flag||temp<0){
//if (flag){
return binaryString.substring(binaryString.length() - 8);
}
else {
return binaryString;
}
}
//赫夫曼压缩 方法封装
public static byte[] huffmanZip(byte[] bytes){
List<Node> nodes = getNodes(bytes);
//根据nodes创建赫夫曼树
Node huffmanTree = createHaffmanTree(nodes);
//preOrder(huffmanTree);
//获取对应的赫夫曼编码
Map<Byte, String> huffmanCodes = getCodes(huffmanTree);
//返回根据赫夫曼编码压缩后的赫夫曼数组
return zip(bytes, huffmanCodes);
}
static int lengthRank;
//将byte[]数组通过生成的赫夫曼编码返回一个压缩后的byte[]
public static byte[] zip(byte[] bytes, Map<Byte,String> huffmanCodes){
StringBuilder stringBuilder = new StringBuilder();
for (byte b : bytes) {
stringBuilder.append(huffmanCodes.get(b));
}
lengthRank = stringBuilder.length() % 8;
//System.out.println("stringBuilder = " + stringBuilder.substring(stringBuilder.length()-6));
//System.out.println("stringBuilder = " + stringBuilder.length());
int length = stringBuilder.length();
int len = (length +7)/8;//这个写法很好
int index = 0;
byte[] huffmanCodeByte = new byte[len];
for (int i = 0; i < length; i+=8) {
String sub;
if (i+8< length){
sub = stringBuilder.substring(i,i+8);
}else {
sub = stringBuilder.substring(i);
}
huffmanCodeByte[index++]=(byte)Integer.parseInt(sub,2);
}
return huffmanCodeByte;
}
//生成赫夫曼树对应的赫夫曼编码
static HashMap huffmanCodes= new HashMap<Byte,String>();
static StringBuilder stringBuilder=new StringBuilder();
//重构
public static Map<Byte,String> getCodes(Node node){
if (node==null){
return null;
}
getCodes(node.left,"0",stringBuilder);
getCodes(node.right,"1",stringBuilder);
return huffmanCodes;
}
public static void getCodes(Node node, String code, StringBuilder stringBuilder){
StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);
stringBuilder1.append(code);
if (node!=null){
if (node.data==null){//非叶子节点
getCodes(node.left,"0",stringBuilder1);
getCodes(node.right,"1",stringBuilder1);
}else {
huffmanCodes.put(node.data, stringBuilder1.toString());
}
}
}
//前序遍历
public static void preOrder(Node root){
if (root != null) {
root.preOrder();
}else {
System.out.println("该赫夫曼树为空!");
}
}
public static List<Node> getNodes(byte[] bytes) {
ArrayList<Node> nodes = new ArrayList<>();
HashMap<Byte, Integer> map = new HashMap<>();
for (byte b : bytes) {
Integer value = map.get(b);
if (value == null) {
map.put(b, 1);
} else {
map.put(b, value + 1);
}
}
for (Map.Entry<Byte, Integer> entry : map.entrySet()) {
nodes.add(new Node(entry.getKey(), entry.getValue()));
}
return nodes;
}
public static Node createHaffmanTree(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, rightNode.weight + leftNode.weight);
parent.left = leftNode;
parent.right = rightNode;
list.remove(rightNode);
list.remove(leftNode);
list.add(parent);
}
return list.get(0);
}
}
//节点类 带数据和权值
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 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();
}
}
}