哈夫曼编码
原理
众所周知:计算机是只看得懂二进制。一串字符,转换成二进制之后,数据是非常长的。哈夫曼编码的作用就是,根据字符出现的频率,来定义其二进制编码的长度,出现的频率越高,则编码长度越短。是不是听起来和哈夫曼树很像:权值越大,路径越短(废话,这俩明显是一个人搞出来的)。
举个例子:
Hello world 转换成十进制ASCII编码为以下这串数字
72 101 108 108 111 32 119 111 114 108 100
转换成二进制,是这样的:
01001000 01100101 01101100 01101100 01101111 00100000 01110111 01101111 01110010 01101100 01100100
而如果统计各个字符出现的次数就会发现:H:1, e:1, w:1, r:1, d:1, 空格:1, o:2, l:3,
将出现的频次当做权值,画出一棵哈夫曼树如下,若将向左为0向右为1,则每个叶子节点的路径能根据出现的频次组成前缀编码(编码本身不会成为其他编码的前缀)
如上图所示的话,各个字母对应的编码应为:H:000, e:001, w:1100, r:1101, d:1110, 空格:1111, o:01, l:10, 经过观察你会发现,以上编码没有任何一个是其他编码的前缀,即连在一起也不会分不清,而又比ASCII码压缩了一倍以上。
实现步骤
- 将一串String字符串转化为byte数组中存储,此时数组中存储的是字符的ASCII码值
- 遍历此byte数组,将字符和每个字符出现的频次,当做每个节点的数据和权值,并将节点存进集合
- 依照此集合的节点,构建哈夫曼树,最终集合中仅剩一根节点
- 遍历此哈夫曼树的叶子节点,向左为0向右为1,求叶子节点的路径,并将其中数据(即字符的ASCII码值)与路径分别作为 key:value 存入map,此map集合就是根据你输入的字符串生成的哈夫曼码表
- 此时遍历最初的那个byte数组,与哈夫曼码表对照,生成一串由0、1组成的字符串。
- 将此字符串看成每八位为一组的二进制补码,转换byte类型存入数组中。
- 至此,完成了对一开始字符串的压缩
对第6步的一些额外知识:
八位的二进制数转换为byte类型,首先二进制数在内存中以补码的形式存储,即 此八位二进制数为补码,若要转换成byte类型并存入数组,则需要
- 将补码转换为反码
- 将反码转换成原码
- 忽略符号位,将原码转换为十进制
如十进制数 1,其原码为00000001,因其为正数所以首位为0,其反码为00000001,其补码为00000001(正数的补码反码为其原码自身)。
十进制数-1,其原码为10000001,因其为负数,所以首位为1,其反码为111111110,其补码为反码+1,即11111111
更多关于原码反码补码的知识,请移步:点击查看 这篇博客写的很详细。
压缩代码实现
package huffman_code;
import java.util.*;
public class HuffmanCode {
public static void main(String[] args) {
String str = "Hello hello world";
byte[] bytes = toHuffmanCode(str);
System.out.println(Arrays.toString(bytes));
}
static Map<Byte,String> codeMap = new HashMap<>();
static StringBuilder stringBuilder = new StringBuilder();
//整合之前的方法
public static byte[] toHuffmanCode(String str){
byte[] bytes = str.getBytes();
List<Node> nodeList = nodeToList(bytes);
Node root = creatHuffmanTree(nodeList);
toHuffmanCodeMap(root, "", stringBuilder);
byte[] zipCode = zip(bytes, codeMap);
return zipCode;
}
/**
* 压缩哈夫曼数组为一个byte数组
* @param bytes 需要压缩的字符串对应的byte数组
* @param huffmanCode 根据每次字符出现的频率构建的哈夫曼编码表
*/
public static byte[] zip(byte[] bytes, Map<Byte,String> huffmanCode){
//将字符依照码表转换成二进制数并拼接字符串
StringBuilder string = new StringBuilder();
for (byte aByte : bytes) {
string.append(huffmanCode.get(aByte));
}
//byte的数组长度,防止不能整除八的情况下数组长度也足够
int len = (string.length() + 7) / 8;
byte[] codeBytes = new byte[len];
//统计该向codeBytes数组的哪个下标添加数据
int index = 0;
for (int i = 0; i < string.length(); i+=8) {
String str;
//防止越界,如果剩余长度不足8,则剩余的全部加入数组
if (i+8 > string.length()){
str = string.substring(i);
} else {
str = string.substring(i, i+8);
}
//Integer.parseInt(str, 2),表示输出的是2进制数。
codeBytes[index] = (byte) Integer.parseInt(str, 2);
index++;
}
return codeBytes;
}
public static void toHuffmanCodeMap(Node node, String code, StringBuilder stringBuilder){
//将上一次递归拼接的字符串获取
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
//继续此次拼接
stringBuilder2.append(code);
if (node != null){
if (node.data == null){
//说明不是叶子节点,向左递归
toHuffmanCodeMap(node.left, "0", stringBuilder2);
//向右递归
toHuffmanCodeMap(node.right, "1", stringBuilder2);
} else {
//没有左右子节点,说明是叶子节点
//将上面递归拼接的路径 存入map中
codeMap.put(node.data, String.valueOf(stringBuilder2));
}
}
}
//创建哈夫曼树
public static Node creatHuffmanTree(List<Node> nodes){
//当集合中的节点只剩一个的时候,说明构建完成,退出循环
while (nodes.size() > 1){
//首先升序排列
Collections.sort(nodes);
//取出最小和次小权值的两个节点,作为左子节点和右子节点
Node leftNode = nodes.get(0);
Node rightNode = nodes.get(1);
//新建一个父节点,权值为左右子节点权值之和
Node parentsNode = new Node(leftNode.weight+rightNode.weight);
//将父节点的左右子节点设置为上面的两个左右节点
parentsNode.left = leftNode;
parentsNode.right = rightNode;
//将左右子节点从集合中删除
nodes.remove(leftNode);
nodes.remove(rightNode);
//将父节点加入集合,在下次循环的时候会重新排序
nodes.add(parentsNode);
}
return nodes.get(0);
}
//将每个字符和出现的频率作为数据、权值创建节点并储存入集合中,方便创建哈夫曼树
public static List<Node> nodeToList(byte[] bytes){
//定义存储字符、频次的map集合
Map<Byte, Integer> hashMap = new HashMap<>();
//遍历集合
for (byte aByte : bytes) {
//判断此字符是否为第一次出现
Integer count = hashMap.get(aByte);
if (count == null){
//第一次出现
hashMap.put(aByte, 1);
} else {
//不是第一次出现
hashMap.put(aByte, count+1);
}
}
//遍历map集合,将其封装成节点存入集合
List<Node> nodeList = new ArrayList<>();
for (Map.Entry<Byte, Integer> entry : hashMap.entrySet()) {
Node node = new Node(entry.getKey(), entry.getValue());
nodeList.add(node);
}
return nodeList;
}
}
数据解压实现步骤
- 首先需要将压缩后的byte数组,转换回根据编码表编码而成的二进制的字符串。
- 步骤1,其中需要用到位运算的知识
- 然后,将编码表的key value调换,做成解码表
- 将二进制字符串根据解码表,解码成byte数组
- 将此数组转换成String字符串
关于步骤2中的一些额外知识:
byte类型转换成int类型二进制数时,如果是正数,则为其二进制数。如果是负数,不足32位的用1补全,后八位为其补码。
也就是说,byte= -88,转换成int,就是111111111111111111111110101000
byte = 1,转换成int就是 1,2就是10。
在上面压缩数据的过程中,我们是截取每八位一个字符串当做二进制补码转换成byte的,因此,解压缩时也要将其转换成二进制的补码。
所以如果是正数则需要与256进行 按位或运算:1 | 256 = 000000001 | 100000000(8个0),结果为100000001,然后截取后8位则为00000001 即为1的补码。
以上步骤中的 按位或 运算的具体方法是:
参加运算的两个数,按二进制位进行“或”运算。
运算规则:参加运算的两个数只要两个数中的一个为1,结果就为1。
即 0 | 0= 0 , 1 | 0= 1 , 0 | 1= 1 , 1 | 1= 1 。
例:2 | 4 即 00000010 | 00000100 = 00000110 ,所以2 | 4的值为 6 。
还有其他位运算符,可以移步:点击查看:位运算(按位与、按位或、异或)
代码实现
package huffman_code;
import java.util.*;
public class HuffmanCode {
public static void main(String[] args) {
String str = "Hello hello world love haha";
System.out.println("原数据:"+str);
byte[] zip = zip(str);
System.out.println("压缩后:"+Arrays.toString(zip));
String unzip = unzip(zip);
System.out.println("解压后:"+unzip);
}
static Map<Byte,String> codeMap = new HashMap<>();
static StringBuilder stringBuilder = new StringBuilder();
//整合解压的方法
public static String unzip(byte[] bytes){
String str = byteToStr(bytes);
return decoding(str, codeMap);
}
//将二进制字符串解码
public static String decoding(String hashCode, Map<Byte,String> codeMap){
//先将编码表的key value反过来,变成解码表
Map<String, Byte> decodingMap = new HashMap<>();
for (Map.Entry<Byte, String> entry : codeMap.entrySet()) {
decodingMap.put(entry.getValue(), entry.getKey());
}
//存放每个符号的ASCII码值
List<Byte> byteList = new ArrayList<>();
//用来表示开始对比的首位1,或0的下标,循环中的i为subString截取的最后一个0、1,它后面一位的下标。
int start = 0;
for (int i = 0; i < hashCode.length(); i++) {
//遍历传入的二进制hashCode字符串,与解码表对比,得到一个字符,就加入到集合中
String substring = hashCode.substring(start, i);
if (decodingMap.get(substring) != null){
//表示在解码表中有此字符
byteList.add(decodingMap.get(substring));
//让start=i,重新开始对比扫描
start = i;
} else if (i == hashCode.length()-1 ){
//因为subString是左闭右开,在最后一段的时候会少截一个字符,所以需要判断一下,i是否到达最后一个字符
substring = hashCode.substring(start);
byteList.add(decodingMap.get(substring));
}
}
//将集合中的byte取出,放入数组,方便转换成字符串
byte[] decodingBytes = new byte[byteList.size()];
for (int i = 0; i < decodingBytes.length; i++) {
//遍历集合,放入数组
decodingBytes[i] = byteList.get(i);
}
//将数组中的byte依照ASCII码表转换成字符串
String decodingStr = new String(decodingBytes);
return decodingStr;
}
//解码:1、将压缩的byte数组,变成二进制字符串
public static String byteToStr(byte[] bytes){
String string;//表示将byte转换成的String字符串
String substring;//因为负数转换成二进制补码的时候是32位,系统会自动用1补全前面的位数,所以需要截取字符串
StringBuilder stringBuilder = new StringBuilder();//用来拼接转换后的字符串
for (int i = 0; i < bytes.length; i++) {
//如果是正数,会不足八位,需要用0填充,所以使用 按位或 256运算,256的二进制位100000000,后面是8个0.
if (bytes[i] >= 0 && i < bytes.length - 1){
int temp = bytes[i] | 256;
string= Integer.toBinaryString(temp);
//按位或运算以后,是首位为1的9位二进制数,所以需要截取后八位
substring = string.substring(string.length() - 8);
} else if (bytes[i] >= 0 && i == bytes.length - 1){
//表示是最后一串正数二进制,在压缩的时候可能是不足八位的,所以无需补位。
substring = Integer.toBinaryString(bytes[i]);
} else {
//表示是负数,截取后八位即可
string= Integer.toBinaryString(bytes[i]);
substring = string.substring(string.length() - 8);
}
//拼接截取后的字符串
stringBuilder.append(substring);
}
return stringBuilder.toString();
}
//整合之前的方法
public static byte[] zip(String str){
byte[] bytes = str.getBytes();
List<Node> nodeList = nodeToList(bytes);
Node root = creatHuffmanTree(nodeList);
toHuffmanCodeMap(root, "", stringBuilder);
byte[] zipCode = toHuffmanCodeByte(bytes, codeMap);
return zipCode;
}
/**
* 依照编码表 编码 原字符串转换而成的byte数组为二进制字符串
* 并看做每八位为一补码,压缩其为一个byte数组
* @param bytes 需要压缩的字符串对应的byte数组
* @param huffmanCode 根据每次字符出现的频率构建的哈夫曼编码表
*/
public static byte[] toHuffmanCodeByte(byte[] bytes, Map<Byte,String> huffmanCode){
//将字符依照码表转换成二进制数并拼接字符串
StringBuilder string = new StringBuilder();
for (byte aByte : bytes) {
string.append(huffmanCode.get(aByte));
}
//byte的数组长度,防止不能整除八的情况下数组长度也足够
int len = (string.length() + 7) / 8;
byte[] codeBytes = new byte[len];
//统计该向codeBytes数组的哪个下标添加数据
int index = 0;
for (int i = 0; i < string.length(); i+=8) {
String str;
//防止越界,如果剩余长度不足8,则剩余的全部加入数组
if (i+8 > string.length()){
str = string.substring(i);
} else {
str = string.substring(i, i+8);
}
//Integer.parseInt(str, 2),表示输出的是2进制数。
codeBytes[index] = (byte) Integer.parseInt(str, 2);
index++;
}
return codeBytes;
}
//获得哈夫曼编码表
public static void toHuffmanCodeMap(Node node, String code, StringBuilder stringBuilder){
//将上一次递归拼接的字符串获取
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
//继续此次拼接
stringBuilder2.append(code);
if (node != null){
if (node.data == null){
//说明不是叶子节点,向左递归
toHuffmanCodeMap(node.left, "0", stringBuilder2);
//向右递归
toHuffmanCodeMap(node.right, "1", stringBuilder2);
} else {
//没有左右子节点,说明是叶子节点
//将上面递归拼接的路径 存入map中
codeMap.put(node.data, String.valueOf(stringBuilder2));
}
}
}
//创建哈夫曼树
public static Node creatHuffmanTree(List<Node> nodes){
//当集合中的节点只剩一个的时候,说明构建完成,退出循环
while (nodes.size() > 1){
//首先升序排列
Collections.sort(nodes);
//取出最小和次小权值的两个节点,作为左子节点和右子节点
Node leftNode = nodes.get(0);
Node rightNode = nodes.get(1);
//新建一个父节点,权值为左右子节点权值之和
Node parentsNode = new Node(leftNode.weight+rightNode.weight);
//将父节点的左右子节点设置为上面的两个左右节点
parentsNode.left = leftNode;
parentsNode.right = rightNode;
//将左右子节点从集合中删除
nodes.remove(leftNode);
nodes.remove(rightNode);
//将父节点加入集合,在下次循环的时候会重新排序
nodes.add(parentsNode);
}
return nodes.get(0);
}
//将每个字符和出现的频率作为数据、权值创建节点并储存入集合中,方便创建哈夫曼树
public static List<Node> nodeToList(byte[] bytes){
//定义存储字符、频次的map集合
Map<Byte, Integer> hashMap = new HashMap<>();
//遍历集合
for (byte aByte : bytes) {
//判断此字符是否为第一次出现
Integer count = hashMap.get(aByte);
if (count == null){
//第一次出现
hashMap.put(aByte, 1);
} else {
//不是第一次出现
hashMap.put(aByte, count+1);
}
}
//遍历map集合,将其封装成节点存入集合
List<Node> nodeList = new ArrayList<>();
for (Map.Entry<Byte, Integer> entry : hashMap.entrySet()) {
Node node = new Node(entry.getKey(), entry.getValue());
nodeList.add(node);
}
return nodeList;
}
}