1. 基础概念
1.基本介绍
赫夫曼编码是变长(不定长)编码的一种。
将【字符出现频率】作为权重,权重越高的节点路劲长度越短
编码规则,从根点触发,通路分支向左为0,向右为1
压缩率在20%-90%
2.编码步骤
1.将文本中的字符各个封转为节点,以出现次数为权重
2.将节点构造哈夫曼树,叶子节点为数据节点
3.根据路径左0右1规则给数据节点编码,得到数据-编码的映射(编码表)
4.根据编码表对原始文本的byte数组进行编码,得到二进制编码字符串
5.将二进制字符串解析成byte数组(最后的编码,将其长度/原始文本bytes长度=压缩率)
3.解码步骤
1.将byte数据,二进制字符串(8位)
2.将编码表逆向<byte,String> -> <String,byte>
3.同逆向编码表得到byte数组
4.直接将byte数组转成String
代码实现
import com.alibaba.fastjson.JSONObject;
import lombok.Getter;
import lombok.Setter;
import org.junit.Test;
import javax.persistence.criteria.CriteriaBuilder;
import java.io.*;
import java.util.*;
@Setter
@Getter
class HeffmanNode implements Comparable{
// 字符的ascall码
private Byte value;
// 字符在文本中出现的次数(权重)
private int weight;
// 左子节点
private HeffmanNode left;
private HeffmanNode right;
public HeffmanNode(Byte value, int weight) {
this.value = value;
this.weight = weight;
}
@Override
public int compareTo(Object o) {
return this.weight - ((HeffmanNode)o).weight;
}
}
public class HeffmanEncodeTest {
/**
* NodeList (初始森林)
* @param sourceBytes
* @return
*/
private List<HeffmanNode> getNodeList(byte[] sourceBytes){
Map<Byte, Integer> countMap = new HashMap<>();
// 统计出现频率
for(byte b : sourceBytes) {
Integer count = countMap.get(b);
if(count==null){
countMap.put(b,1);
}else {
countMap.put(b, count+1);
}
}
// 构建NodeList
List<HeffmanNode> nodes = new ArrayList<>();
countMap.forEach((key,value)-> {
HeffmanNode node = new HeffmanNode(key, value);
nodes.add(node);
});
return nodes;
}
/**
* 构建赫夫曼树(根节点)
* @param nodes
* @return
*/
private HeffmanNode buildHeffmantree(List<HeffmanNode> nodes) {
while (nodes.size()>1) {
Collections.sort(nodes);
HeffmanNode min1 = nodes.get(0);
HeffmanNode min2 = nodes.get(1);
HeffmanNode newNode = new HeffmanNode(null, min1.getWeight() + min2.getWeight());
newNode.setLeft(min1);
newNode.setRight(min2);
nodes.remove(min1);
nodes.remove(min2);
nodes.add(newNode);
}
return nodes.get(0);
}
/**
* 根据赫夫曼树建立编码表
* 编码顺序:从上往下
* 编码规则:左 0 右 1
* @param root
* @return
*/
private Map<Byte,String> getCodeTable(HeffmanNode root) {
if(root==null){
return null;
}
//编码表
StringBuilder sb = new StringBuilder();
Map<Byte,String> codeMap = new HashMap<>();
// 处理左节点
getCodeStr(root.getLeft(), "0",sb,codeMap);
// 处理右边节点
getCodeStr(root.getRight(), "1",sb,codeMap);
return codeMap;
}
/**
* 功能:
* 细节:因为该方法有递归,所有编码表定义为成员变量
* @param node: 当前待处理的节点
* @param subPath: 当前需要添加的路径(左0右1)
* @param sb : 前边非叶子节点走下来的路径
*/
private void getCodeStr(HeffmanNode node,String subPath, StringBuilder sb,Map<Byte,String> codeMap) {
// 先拼上前面的路径(这里一定要照sb2,否则会对右边的路径有影响)
StringBuilder sb2 = new StringBuilder(sb);
sb2.append(subPath);
// 非叶子节点,继续往后走
if(node.getValue() == null) {
// 左0 ,右1
if(node.getLeft()!=null) {
getCodeStr(node.getLeft(),"0",sb2, codeMap);
}
if(node.getRight()!=null) {
getCodeStr(node.getRight(),"1",sb2, codeMap);
}
}
// 将叶子节点的编码放入编码表
else {
codeMap.put(node.getValue(),sb2.toString());
}
}
/**
* 根据编码表,通过ASCALL码Byte获取编码字符串
*/
private StringBuilder getEncodeStringByCodeTable(byte[] sourceBytes, Map<Byte,String> codeMap) {
StringBuilder sb = new StringBuilder();
for(byte b : sourceBytes) {
sb.append(codeMap.get(b));
}
return sb;
}
/**
* 将编码字符串作为二进制补码生成byte[],
* 一个byte字节,8bit
*/
private byte[] getHeffmanEncodeBytesByEncodeString(StringBuilder encodeStringBuilder) {
// 计算byte长度
// 下面的计算过程可以简写: int len = (encodeStringBuilder.length() + 7)/ 8+1 ;
int len = 0;
if(encodeStringBuilder.length()%8==0){
len = encodeStringBuilder.length()/8;
}else{
len = encodeStringBuilder.length()/8+1;
}
byte[] bytes = new byte[len];
int j = 0;
for(int i=0; i<encodeStringBuilder.length(); i+=8) {
String bitStr = null;
// 截取的最后端可能不满8位
if(i+8 >= encodeStringBuilder.length()) {
bitStr = encodeStringBuilder.substring(i);
}else {
bitStr = encodeStringBuilder.substring(i,i+8);
}
byte bit = (byte)Integer.parseInt(bitStr, 2);
bytes[j] = bit;
j++;
}
return bytes;
}
/**
* 根据被编码数据动态创建编码表
* @param source 被编码数据
* @return: 生成的赫夫曼编码表{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
*/
public Map<Byte,String>buildEncodeTable(String source) {
// 1.获得原始森林
List<HeffmanNode> nodes = getNodeList(source.getBytes());
// 2. 根据原始森林构建赫夫曼树
HeffmanNode root = buildHeffmantree(nodes);
// 3. 由于赫夫曼树构建编码表
return getCodeTable(root);
}
/**
* 编码
* @param source: 被编码的原数据
*/
public byte[] encode(String source,Map<Byte,String> encodeTable){
// 4. 根据编码表,对元数据进行编码
StringBuilder sb = getEncodeStringByCodeTable(source.getBytes(),encodeTable);
// 5. 为了减小数据长度,将编码后的字符串"100111....",放入HeffmanEncodeByte[], 一个字节八位,
byte[] targetBytes = getHeffmanEncodeBytesByEncodeString(sb);
return targetBytes;
}
///////////////////////////解码////////////////////////////////////
/**
* 将一个byte 转成一个二进制的字符串, 如果看不懂,可以参考我讲的Java基础 二进制的原码,反码,补码
* @param b 传入的 byte
* @param flag 标志是否需要补高位如果是true ,表示需要补高位,如果是false表示不补, 如果是最后一个字节,无需补高位
* @return 是该b 对应的二进制的字符串,(注意是按补码返回)
*/
private static String byteToBitString(boolean flag, byte b) {
//使用变量保存 b
int temp = b; //将 b 转成 int
//如果是正数我们还存在补高位
if(flag) {
temp |= 256; //按位与 256 1 0000 0000 | 0000 0001 => 1 0000 0001
}
String str = Integer.toBinaryString(temp); //返回的是temp对应的二进制的补码
if(flag) {
return str.substring(str.length() - 8);
} else {
return str;
}
}
//完成数据的解压
//思路
//1. 将huffmanCodeBytes [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
// 重写先转成 赫夫曼编码对应的二进制的字符串 "1010100010111..."
//2. 赫夫曼编码对应的二进制的字符串 "1010100010111..." =》 对照 赫夫曼编码 =》 "i like like like java do you like a java"
//编写一个方法,完成对压缩数据的解码
/**
*
* @param encodeTable 赫夫曼编码表 map
* @param huffmanBytes 赫夫曼编码得到的字节数组
* @return 就是原来的字符串对应的数组
*/
private static byte[] decode(byte[] huffmanBytes, Map<Byte,String> encodeTable) {
//1. 先得到 huffmanBytes 对应的 二进制的字符串 , 形式 1010100010111...
StringBuilder stringBuilder = new StringBuilder();
//将byte数组转成二进制的字符串
for(int i = 0; i < huffmanBytes.length; i++) {
byte b = huffmanBytes[i];
//判断是不是最后一个字节
boolean flag = (i == huffmanBytes.length - 1);
stringBuilder.append(byteToBitString(!flag, b));
}
//2. 把赫夫曼编码表进行调换,因为反向查询 a->100 100->a
Map<String, Byte> map = new HashMap<String,Byte>();
for(Map.Entry<Byte, String> entry: encodeTable.entrySet()) {
map.put(entry.getValue(), entry.getKey());
}
//创建要给集合,存放byte
List<Byte> list = new ArrayList<>();
//i 可以理解成就是索引,扫描 stringBuilder
for(int i = 0; i < stringBuilder.length(); ) {
int count = 1; // 小的计数器
boolean flag = true;
Byte b = null;
while(flag) {
//1010100010111...
//递增的取出 key 1
String key = stringBuilder.substring(i, i+count);//i 不动,让count移动,指定匹配到一个字符
b = map.get(key);
if(b == null) {//说明没有匹配到
count++;
}else {
//匹配到
flag = false;
}
}
list.add(b);
i += count;//i 直接移动到 count
}
//当for循环结束后,我们list中就存放了所有的字符 "i like like like java do you like a java"
//把list 中的数据放入到byte[] 并返回
byte b[] = new byte[list.size()];
for(int i = 0;i < b.length; i++) {
b[i] = list.get(i);
}
return b;
}
/**
* 测试解压
*/
@Test
public void test() {
// 被编码数据
String source = "i like like like java do you like a java";
// 动态创建编码表解码
Map<Byte, String> encodeTable = buildEncodeTable(source);
// 编码
byte[] encodeBytes = encode(source, encodeTable);
// 解码
byte[] retByte = decode(encodeBytes,encodeTable);
System.out.println("decodeStr:" + new String(retByte));
}
}