1、哈夫曼编码
- Huffman Coding:是哈夫曼树在电讯通信领域的经典应用,被用于数据文件压缩,其压缩率在20%-90%之间,是可变字长编码(VLC)的一种,属于无损压缩;
- 固定长度编码:使用固定长度的符号编码,如使用8位二进制数进行编码的ASCII码就是最常用的定长编码;定长编码后的数据文件较大;
- 可变字长编码:根据字符在文件中的出现率大小分配不同长度的编码,出现几率高的字符使用较短的编码,反之亦然。这能使编码后的文件得到一定压缩,但可能使解码产生二义性;
- 哈夫曼编码是变长编码,但通过巧妙的哈夫曼树构建规则,同一文件中每个字符的编码都不可能是其他任何字符编码的前缀,这消除了编码的二义性;
- 哈夫曼树建立过程不唯一(当某些字符在文件中出现的次数相同时,它们的排序不同,构建的哈夫曼树也不同),对应生成的哈夫曼编码也不唯一。但编码后的编码长度都一样(WPL相同),即压缩率相同。
- 哈夫曼编码压缩的步骤:
- 输入需要编码的字符串,统计不同字符对应出现的次数;
- 将次数作为权值,构建一棵哈夫曼树;
- 根据二叉树向左路径为0,向右路径为1的规则,可对各个字符完成编码,进而形成一个编码表;
- 根据编码表,将原字符串按顺序转换为二进制编码字符串;
- 以8位为1个字节,将二进制编码字符串换算为十进制数(第一位为符号位),如 (bit)(1000 1000) => (byte)(-8)。依次执行后,形成一个字节数组,这就是压缩后的数据;
- 哈夫曼编码解压的步骤:
- 即压缩步骤的反向操作。
2、Java代码 -- 字符串的压缩与解压
// 用哈夫曼编码压缩一串字符串后,再解压显示
package DataStructures.Tree;
import java.util.*;
/**
* @author yhx
* @date 2020/10/17
*/
public class HuffmanCode {
/**
* 生成哈夫曼树对应的哈夫曼编码
* 1、将哈夫曼编码存放在 Map<Byte,String> 中
* 2、生成哈夫曼编码时,需要拼接路径,定义一个StringBuilder存储某个叶子节点的路径
*/
static Map<Byte, String> huffmanCodes = new HashMap<>();
static StringBuilder stringBuilder = new StringBuilder();
public static void main(String[] args) {
String str = "I like java very very like java, do you like java?";
// 压缩
byte[] huffmanCodeBytes = huffmanZip(str);
// 解压
byte[] sourceBytes = decode(huffmanCodes, huffmanCodeBytes);
System.out.println("9、原来的字符串为:\n" + new String(sourceBytes));
}
/**
* 接收字符串数组,转换为哈夫曼节点集合,节点统计了每个字符出现的次数
*
* @param bytes 字符串数组
* @return 返回哈夫曼节点集合
*/
private static List<HuffmanNode> getNodes(byte[] bytes) {
// 1、创建一个ArrayList
List<HuffmanNode> nodes = new ArrayList<>();
/*
2、统计每一个字符出现的次数
键Key:字符,byte整型
值Value:次数,Integer整型
*/
HashMap<Byte, Integer> counts = new HashMap<>();
for (byte ch : bytes) {
// 获取每个字符在哈希表中值(次数)
Integer count = counts.get(ch);
// 如果值为空,说明该字符第一次被统计到,将该字符ch(Key)的次数count(Value)改为1
if (count == null) {
counts.put(ch, 1);
}
// 值不为空,说明该字符不是第一次出现,只需将出现次数加1
else {
counts.put(ch, count + 1);
}
}
// 3、遍历哈希表,将每个“键值对”转成一个HuffmanNode对象,加入到Nodes集合中
for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
nodes.add(new HuffmanNode(entry.getKey(), entry.getValue()));
}
return nodes;
}
/**
* 创建哈夫曼树
*
* @param nodes 节点集合
* @return 返回树根节点
*/
private static HuffmanNode createHuffmanTree(List<HuffmanNode> nodes) {
while (nodes.size() > 1) {
// 排序
Collections.sort(nodes);
// 取出最小的二叉树
HuffmanNode leftNode = nodes.get(0);
// 取出第二小的二叉树
HuffmanNode rightNode = nodes.get(1);
// 创建一棵新的二叉树,没有data,只有权值
HuffmanNode parent = new HuffmanNode(null, leftNode.weight + rightNode.weight);
parent.left = leftNode;
parent.right = rightNode;
// 移除两棵处理过的二叉树
nodes.remove(leftNode);
nodes.remove(rightNode);
// 将新的二叉树加入到nodes集合
nodes.add(parent);
}
// 最后的节点就是哈夫曼树的根节点
return nodes.get(0);
}
/**
* 得到 node节点的所有叶子节点的哈夫曼编码,并存放到容器huffmanCodes中
*
* @param node 传入的节点(根节点)
* @param code 叶子节点的路径。左子节点是0,右子节点是1
* @param stringBuilder 拼接路径
*/
private static void getCodes(HuffmanNode node, String code, StringBuilder stringBuilder) {
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
// 将code拼接到stringBuilder2后
stringBuilder2.append(code);
if (node != null) {
// 判断当前node节点是否是叶子节点
if (node.data == null) {
// 左右递归处理
getCodes(node.left, "0", stringBuilder2);
getCodes(node.right, "1", stringBuilder2);
}
// 叶子节点
else {
// 说明找到了某个叶子节点,将对应的路径(哈夫曼编码)存入到容器huffmanCodes中
huffmanCodes.put(node.data, stringBuilder2.toString());
}
}
}
/**
* 为了调用方便,重载getCodes方法
*
* @param root 根节点
* @return 返回哈夫曼编码表
*/
private static Map<Byte, String> getCodes(HuffmanNode root) {
if (root == null) {
return null;
}
// 处理根节点的左右子树
getCodes(root.left, "0", stringBuilder);
getCodes(root.right, "1", stringBuilder);
return huffmanCodes;
}
/**
* 输入一个字符串数组,返回哈夫曼编码压缩后的 byte[]
*
* @param bytes 待压缩字符串数组
* @param huffmanCodes 生成的哈夫曼编码表
* @return 返回压缩结果
*/
private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
// 先利用huffmanCodes将字符数组bytes转为哈夫曼编码对应的字符串
StringBuilder stringBuilder = new StringBuilder();
for (byte b : bytes) {
// 按顺序得到每个字符对应的哈夫曼编码,并将其拼接到一起
stringBuilder.append(huffmanCodes.get(b));
}
System.out.println("5、根据哈夫曼编码表,将字符串转为二进制编码字符串:\n" + stringBuilder.toString() + "\n");
// 统计返回byte[]的长度
// 以下6行代码等价于:int len = (stringBuilder.length() + 7) / 8;
int len;
// 因为每8位对应一个byte,所以步长为8
int stride = 8;
if (stringBuilder.length() % stride == 0) {
len = stringBuilder.length() / stride;
} else {
len = stringBuilder.length() / stride + 1;
}
// 存储压缩后的byte数组
byte[] huffmanCodeBytes = new byte[len];
// 记录是第几个byte
int index = 0;
for (int i = 0; i < stringBuilder.length(); i += stride) {
String strByte;
// 不足8位
if (i + stride > stringBuilder.length()) {
strByte = stringBuilder.substring(i);
}
// 位数足够
else {
strByte = stringBuilder.substring(i, i + stride);
}
// 将strByte转成一个byte,放入到huffmanCodeBytes数组中
// parseInt(String s, int radix)函数:表示将s字符串(radix表示其进制)转为10进制整型
huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
index++;
}
return huffmanCodeBytes;
}
/**
* 将以上方法封装,传入待压缩字符串,返回压缩后的哈夫曼编码字节数组
*
* @param str 待压缩字符串
* @return 哈夫曼编码
*/
private static byte[] huffmanZip(String str) {
System.out.println("1、待压缩的字符串为:");
System.out.println(str);
System.out.println();
// 将字符串中的字符存到byte整型数组中
System.out.println("2、待压缩字符串转为ASCII码后为:");
byte[] contentBytes = str.getBytes();
System.out.println(Arrays.toString(contentBytes));
System.out.println();
// 将每个字符转为node节点
List<HuffmanNode> nodes = getNodes(contentBytes);
// 创建哈夫曼树
HuffmanNode huffmanTreeRoot = createHuffmanTree(nodes);
System.out.println("3、哈夫曼树前序遍历的结果:");
huffmanTreeRoot.preOrder();
System.out.println();
// 根据哈夫曼树创建哈夫曼编码表
System.out.println("4、生成哈夫曼编码表:");
Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
System.out.println(huffmanCodes);
System.out.println();
// 根据哈夫曼编码表将字符串压缩为哈夫曼编码字节数组
byte[] huffmanCodeBytes = zip(contentBytes, huffmanCodes);
System.out.println("6、字符串转为哈夫曼编码字节数组:\n" + Arrays.toString(huffmanCodeBytes) + "\n");
// 返回
return huffmanCodeBytes;
}
/**
* 将一个byte转成一个二进制字符串,如 8 --> 0000 1000
*
* @param b 字节
* @param flag 标识是否需要补高位,true表示需要。
* 如果是最后一个字节,不需要补高位。
* 因为最后一个字节在压缩时不一定有8位,解码时如果强行补位就改变其值了。
* @return 二进制字符串,按照补码返回
*/
private static String byteToBitString(boolean flag, byte b) {
int temp = b;
if (flag) {
/*
按位或,用于将正数补高位
256 二进制 => 1 0000 0000
*/
temp = temp | 256;
}
String str = Integer.toBinaryString(temp);
if (flag) {
// 取后八位
return str.substring(str.length() - 8);
} else {
return str;
}
}
/**
* 解码
*
* @param huffmanCodes 哈夫曼编码表
* @param huffmanBytes 哈夫曼编码得到的字节数组
* @return 原来字符串对应的数组
*/
private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
// 先得到哈夫曼编码字节数组对应的二进制字符串
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < huffmanBytes.length; i++) {
// 判断是否是最后一个字节
boolean flag = (i == huffmanBytes.length - 1);
stringBuilder.append(byteToBitString(!flag, huffmanBytes[i]));
}
System.out.println("7、(哈夫曼字节数组 -> 二进制编码字符串)解码后:\n" + stringBuilder.toString() + "\n");
// 解码
Map<String, Byte> map = new HashMap<>();
// 将哈夫曼编码表的键K值V反转
for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
map.put(entry.getValue(), entry.getKey());
}
System.out.println("8、哈夫曼编码表的键K值V反转后为:\n" + map + "\n");
// 将二进制编码字符串转换为字符数组
List<Byte> list = new ArrayList<>();
for (int i = 0; i < stringBuilder.length(); ) {
// 递增计数器
int count = 1;
boolean flag = true;
Byte b = null;
while (flag) {
// 递增取字符串中的0或1
String key = stringBuilder.substring(i, i + count);
// 从编码表中找,是否有字符串对应的数值(ASCII)
b = map.get(key);
// 没有匹配到
if (b == null) {
count++;
} else {
flag = false;
}
}
// 将匹配到的数值(ASCII)添加到集合
list.add(b);
i += count;
}
// 将集合中的数据放入数组,返回结果
byte[] bs = new byte[list.size()];
for (int i = 0; i < bs.length; i++) {
bs[i] = list.get(i);
}
return bs;
}
/**
* 前序遍历的方法
*
* @param root 根节点
*/
private static void preOrder(HuffmanNode root) {
if (root != null) {
root.preOrder();
} else {
System.out.println("哈夫曼树为空");
}
}
}
/**
* 创建节点Node,带数据和权值
*/
class HuffmanNode implements Comparable<HuffmanNode> {
Byte data;
int weight;
HuffmanNode left;
HuffmanNode right;
/**
* 构造器
*
* @param data 以ASCII码存放数据,如'a' = 97
* @param weight 权值,表示字符出现的次数
*/
public HuffmanNode(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 int compareTo(HuffmanNode o) {
// 升序排序
return this.weight - o.weight;
}
/**
* 重写方法:打印节点信息
*
* @return 打印形式
*/
@Override
public String toString() {
return "Huffman Node [ data = " + data + " weight = " + weight + " ]";
}
}
3、运行结果
1、待压缩的字符串为:
I like java very very like java, do you like java?
2、待压缩字符串转为ASCII码后为:
[73, 32, 108, 105, 107, 101, 32, 106, 97, 118, 97, 32, 118, 101, 114, 121, 32, 118, 101, 114, 121, 32, 108, 105, 107, 101, 32, 106, 97, 118, 97, 44, 32, 100, 111, 32, 121, 111, 117, 32, 108, 105, 107, 101, 32, 106, 97, 118, 97, 63]
3、哈夫曼树前序遍历的结果:
Huffman Node [ data = null weight = 50 ]
Huffman Node [ data = null weight = 21 ]
Huffman Node [ data = null weight = 10 ]
Huffman Node [ data = 101 weight = 5 ]
Huffman Node [ data = 118 weight = 5 ]
Huffman Node [ data = null weight = 11 ]
Huffman Node [ data = null weight = 5 ]
Huffman Node [ data = null weight = 2 ]
Huffman Node [ data = 44 weight = 1 ]
Huffman Node [ data = 117 weight = 1 ]
Huffman Node [ data = 105 weight = 3 ]
Huffman Node [ data = 97 weight = 6 ]
Huffman Node [ data = null weight = 29 ]
Huffman Node [ data = null weight = 12 ]
Huffman Node [ data = null weight = 6 ]
Huffman Node [ data = 106 weight = 3 ]
Huffman Node [ data = 107 weight = 3 ]
Huffman Node [ data = null weight = 6 ]
Huffman Node [ data = 108 weight = 3 ]
Huffman Node [ data = 121 weight = 3 ]
Huffman Node [ data = null weight = 17 ]
Huffman Node [ data = null weight = 7 ]
Huffman Node [ data = null weight = 3 ]
Huffman Node [ data = 63 weight = 1 ]
Huffman Node [ data = 111 weight = 2 ]
Huffman Node [ data = null weight = 4 ]
Huffman Node [ data = 114 weight = 2 ]
Huffman Node [ data = null weight = 2 ]
Huffman Node [ data = 100 weight = 1 ]
Huffman Node [ data = 73 weight = 1 ]
Huffman Node [ data = 32 weight = 10 ]
4、生成哈夫曼编码表:
{32=111, 97=011, 100=110110, 101=000, 105=0101, 73=110111, 106=1000, 107=1001, 44=01000, 108=1010, 111=11001, 114=11010, 117=01001, 118=001, 121=1011, 63=11000}
5、根据哈夫曼编码表,将字符串转为二进制编码字符串:
11011111110100101100100011110000110010111110010001101010111110010001101010111111010010110010001111000011001011010001111101101100111110111100101001111101001011001000111100001100101111000
6、字符串转为哈夫曼编码字节数组:
[-33, -46, -56, -16, -53, -28, 106, -7, 26, -65, 75, 35, -61, 45, 31, 108, -5, -54, 125, 44, -113, 12, -68, 0]
7、(哈夫曼字节数组 -> 二进制编码字符串)解码后:
11011111110100101100100011110000110010111110010001101010111110010001101010111111010010110010001111000011001011010001111101101100111110111100101001111101001011001000111100001100101111000
8、哈夫曼编码表的键K值V反转后为:
{110110=100, 11010=114, 110111=73, 1000=106, 1011=121, 1010=108, 01001=117, 011=97, 000=101, 111=32, 001=118, 0101=105, 01000=44, 1001=107, 11001=111, 11000=63}
9、原来的字符串为:
I like java very very like java, do you like java?