赫夫曼编码基本介绍:
赫夫曼编码的基本过程:
- 先将要进行编码的字符串(“Keep doing it and then you will succeed”)转为字节数组,然后通过字节数组创建出所有的节点,并将节点放入List集合中
//每个节点的属性
private Byte content;//节点的内容(一个字节)
private int value;//节点的权值
private Node left;//节点的左子节点
private Node right;//节点的右子节点
- 通过第一步存放所有节点的List集合创建赫夫曼树并返回根节点
- 规定向左遍历为"0"、向右遍历为"1",通过根节点遍历赫夫曼树得到赫夫曼编码表Map<Byte, String> (如:{32=111, 97=01010, 99=0000, 100=1011, 101=100, 103=01011, 104=01100, 105=1100, 75=01101, 108=0001, 110=1101, 111=0010, 112=01110, 115=01111, 116=0011, 117=0100, 119=10100, 121=10101})
如:
- 通过赫夫曼编码表得到赫夫曼编码字节数组(即将源数组按照赫夫曼编码表转译为赫夫曼编码String类型的二进制字符串,如:011011001000111011110110010110…),然后每八个二进制字符串放入一个字节中(最后一个字节可能不满八位,仍放入一个字节中),最后得到字节数组为赫夫曼编码字节数组,到这里赫夫曼编码过程完成
赫夫曼解码过程:
要想解码要有两个东西,一个是编码完成得到的赫夫曼编码字节数组,另一个是赫夫曼编码表,试想一下,这不就跟拿着一句英文一个一个单词去查英汉字典类似?
-
将赫夫曼编码字节数组转为String类型的二进制字符串(如:0110110010001…),这里特别要注意对补位和截位的处理,因为是用Integer.toBinaryString()来做的转换,所以如果是负数的话,会有32位(如 -75:11111111111111111111111110110101
),因为一个字节只有八位,我们只需要取八位即可(-75:10110101),如果是正数,可能不够八位(如 75:1001011),所以得补0(01001011)最后一个字节也得做特殊处理,前面我们说过,因为最后一个字节可能不够八位,但仍放入一个字节中,所以取出来的时候,需要判断这个字节够不够八位,不够八位就补几个0(要想得到最后一个字节有几位,我们可以定义一个全局变量在进行赫夫曼编码的时候用于存放最后一个字节的二进制字符串)
byte b = -1;
System.out.println(Integer.toBinaryString(b));
//为负数的结果:11111111111111111111111111111111
byte b = 1;
System.out.println(Integer.toBinaryString(b));
//为整数的结果:1
- 第一步中我们取出来的是赫夫曼编码String类型的二进制字符串,由于我们的赫夫曼码表是Map<Byte, String>这种类型,而我们要想通过String类型取出Byte类型的值,得将赫夫曼编码表进行反转,变成Map<String, Byte>(如:{1011=100, 0010=111, 01101=75, 01100=104, 01111=115, 111=32, 100=101, 01011=103, 01110=112, 01010=97, 0000=99, 1101=110, 0011=116, 1100=105, 0001=108, 0100=117, 10101=121, 10100=119})
代码:
/**
* 节点类
* @author codewen
*
*/
public class Node implements Comparable<Node>{
private Byte content;//节点的内容(一个字节)
private int value;//节点的权值
private Node left;//节点的左子节点
private Node right;//节点的右子节点
public Node(int value, Byte b) {
this.value = value;
this.content = b;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public Byte getB() {
return content;
}
public void setB(Byte b) {
this.content = b;
}
public Node getLeft() {
return left;
}
public void setLeft(Node left) {
this.left = left;
}
public Node getRight() {
return right;
}
public void setRight(Node right) {
this.right = right;
}
@Override
public String toString() {
return "Node [content=" + content + ", value=" + value + "]";
}
@Override
public int compareTo(Node o) {
//升序
return this.value-o.value;
}
}
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 赫夫曼编码和解码
*对"Keep doing it and then you will succeed"进行编码和解码
* @author codewen
*
*/
public class HuffmanEnCodeAndDecode {
/**
* 1.赫夫曼编码
* @param srcBytes 源字节数组
* @return huffmanCodeBytes 赫夫曼编码字节数组
*/
public static byte[] huffmanEncode(byte[] srcBytes) {
//1.1将源字节数组的每个字节转为节点,返回节点集合
List<Node> nodes = getNodes(srcBytes);
//1.2通过节点集合,创建赫夫曼树,返回根节点
root = buildHuffmanTree(nodes);
//1.3遍历赫夫曼树,得到赫夫曼编码表,返回赫夫曼编码表
huffmanCodeTable = getHuffmanCodeTable(root, "", new StringBuilder());
System.out.println("赫夫曼编码表:"+huffmanCodeTable);
//1.4通过赫夫曼编码表将源字节数组转为赫夫曼编码的二进制字符串,然后将其转为赫夫曼编码字节数组并返回
byte[] huffmanCodeBytes = getHuffmanCodeBytes(srcBytes, huffmanCodeTable);
return huffmanCodeBytes;
}
//1.1将源字节数组的每个字节转为节点,返回节点集合
private static List<Node> getNodes(byte[] srcBytes) {
//使用map用来统计相同字节的个数
//(如:K:1 a:1 c:2 d:3 e:5 g:1 h:1 i:3 l:2 n:3 o:2 p:1 s:1 t:2 u:2 w:1 y:1 //各个字符对应的个数)
Map<Byte, Integer> nodeMap = new HashMap<Byte, Integer>();
for (byte b : srcBytes) {
if(nodeMap.get(b) == null) {
nodeMap.put(b, 1);
}else {
nodeMap.replace(b, nodeMap.get(b)+1);
}
}
//遍历map,创建节点
List<Node> nodes = new ArrayList<Node>();
for(Map.Entry<Byte, Integer> m : nodeMap.entrySet()) {
nodes.add(new Node(m.getValue(), m.getKey()));
}
return nodes;
}
//1.2通过节点集合,创建赫夫曼树,返回根节点
private static Node buildHuffmanTree(List<Node> nodes) {
while(nodes.size() > 1) {
//根据value从小到大排序
Collections.sort(nodes);
//取出最小的两个节点作为左右子节点
Node leftNode = nodes.get(0);
Node rightNode = nodes.get(1);
//创建父节点 并 设置左右子节点
Node parentNode = new Node(leftNode.getValue()+rightNode.getValue(), null);
parentNode.setLeft(leftNode);
parentNode.setRight(rightNode);
//从集合中移除左右子节点,将父节点加入集合
nodes.remove(leftNode);
nodes.remove(rightNode);
nodes.add(parentNode);
//之后重复以上操作,知道剩下一个节点——根节点
}
return nodes.get(0);
}
//1.3遍历赫夫曼树,得到赫夫曼编码表,返回赫夫曼编码表
/**
* 规定向左为"0",规定向右为"1"
* @param node 节点
* @param s 二进制字符串(代表遍历节点的方向)
* @param sb 用于拼接方向,直到找到一个字符(叶子节点),最后代表从根节点到叶子节点的路径
* @return
*/
private static Node root;//定义根节点
private static Map<Byte, String> huffmanCodeTable = new HashMap<Byte, String>();//定义赫夫曼编码表
private static Map<Byte, String> getHuffmanCodeTable(Node node, String s, StringBuilder sb) {
if(node == root && node == null) {
System.out.println("该赫夫曼树为空,无法得到赫夫曼编码表!");
return null;
}
StringBuilder path = new StringBuilder(sb);
path.append(s);
if(node.getLeft() != null) {//向左递归遍历
getHuffmanCodeTable(node.getLeft(), "0", path);
}else {//找到叶子节点
huffmanCodeTable.put(node.getB(), path.toString());
return huffmanCodeTable;
}
if(node.getRight() != null) {//向右递归遍历
getHuffmanCodeTable(node.getRight(), "1", path);
}else {
huffmanCodeTable.put(node.getB(), path.toString());
}
return huffmanCodeTable;
}
//1.4通过赫夫曼编码表将源字节数组转为赫夫曼编码的二进制字符串,然后将其转为赫夫曼编码字节数组并返回
private static String lastByte;//用于保存最后一个字节的二进制字符串
private static byte[] getHuffmanCodeBytes(byte[] srcBytes, Map<Byte, String> huffmanCodeTable) {
//遍历源字节数组,取出二进制字符串
String huffmanCode = "";//定义赫夫曼编码二进制字符串
for (byte b : srcBytes) {
huffmanCode += huffmanCodeTable.get(b);
}
//将二进制字符串转为赫夫曼字节数组
int len = huffmanCode.length() % 8 == 0 ? huffmanCode.length()/8 : huffmanCode.length()/8+1;
//或者int len = (huffmanCode.length() + 7) / 8;
byte[] huffmanCodeBytes = new byte[len];//定义赫夫曼字节数组
int t = 0;//辅助指针
for(int i=0; i<huffmanCode.length(); i+=8) {
if(t == len-1) {//如果是最后一个字节,可能不够8位
lastByte = huffmanCode.substring(i);
huffmanCodeBytes[t++] = (byte) Integer.parseInt(lastByte, 2);
}else {
huffmanCodeBytes[t++] = (byte) Integer.parseInt(huffmanCode.substring(i, i+8), 2);
}
}
return huffmanCodeBytes;
}
/**
* 2.赫夫曼解码
* @param huffmanCodeBytes 赫夫曼编码字节数组
* @return destBytes 目标字节数组
*/
public static byte[] huffmanDecode(byte[] huffmanCodeBytes) {
//2.1将赫夫曼编码字节数组转化为赫夫曼编码的二进制字符串
String huffmanCode = getHuffmanCode(huffmanCodeBytes);
//2.2将赫夫曼编码表反转,利用二进制字符串为key,从反转编码表中转译出对应的字节
byte[] destBytes = getDestBytes(huffmanCode);
return destBytes;
}
//2.1将赫夫曼编码字节数组转化为赫夫曼编码的二进制字符串
private static String getHuffmanCode(byte[] huffmanCodeBytes) {
//遍历赫夫曼编码字节数组,将每个字节转为赫夫曼编码的二进制字符串
String huffmanCode = "";//定义赫夫曼编码二进制字符串
boolean flag = true;//判断是否要补位
for(int i=0; i<huffmanCodeBytes.length; i++) {
if(i == huffmanCodeBytes.length-1) {//最后一个字节要做特殊处理(如果进行编码时最后一个字节是8位则要进行补位,否则不需要)
flag = false;
}
huffmanCode += getCode(flag, huffmanCodeBytes[i]);
}
return huffmanCode;
}
//2.1.1将每个字节转为赫夫曼编码的二进制字符串
private static String getCode(boolean flag, byte b) {
int temp = b;//辅助变量
//如果是正数,要考虑补位(补0即可),负数不需要补位(负数|256结果仍是一样,所以负数按正数一样处理)
//如果是和夫曼编码字节数组的最后一个字节的话就特殊处理,否则都要补位
if(flag) {
temp |= 256;//进行或运算temp | 1 0000 0000 到时候只截取八位就够了
}
//除了最后一个字节可能不够八位,前面的字节都截取最后八位
String str = Integer.toBinaryString(temp);
if(flag) {
return str.substring(str.length()-8);
}else {//最后一个字节
int d = lastByte.length() - str.length();
if(d != 0) {//需要进行补位
for(int i=0; i<d; i++) {
str = "0" + str;
}
}
return str;
}
}
//2.2将赫夫曼编码表反转,利用二进制字符串为key,从反转编码表中转译出对应的字节
private static byte[] getDestBytes(String huffmanCode) {
//将赫夫曼编码表反转
Map<String, Byte> reHuffmanCodeTable = new HashMap<String, Byte>();
for(Map.Entry<Byte, String> map : huffmanCodeTable.entrySet()) {
reHuffmanCodeTable.put(map.getValue(), map.getKey());
}
//根据发展编码表转译出对应的字节
int index;//辅助指针
Byte value;//根据key取出的value
List<Byte> destBytesLsit = new ArrayList<Byte>();//存放目标字节集合
for(int i=0; i<huffmanCode.length(); ) {
for(index=i+1; index<=huffmanCode.length(); index++) {
value = reHuffmanCodeTable.get(huffmanCode.substring(i, index));
if(value != null) {
destBytesLsit.add(value);
break;
}
}
i = index;
}
byte[] destBytes = new byte[destBytesLsit.size()];//用于存放目标字节的数组
int t = 0;//辅助指针
for(int i=0; i<destBytes.length; i++) {//将集合的目标字节放入到数组中
destBytes[t++] = destBytesLsit.get(i);
}
return destBytes;
}
public static void main(String[] args) {
String srcContent = "Keep doing it and then you will succeed";
System.out.println("源字符串内容:"+srcContent);
byte[] srcBytes = srcContent.getBytes();//源字节数组
System.out.println("源字节数组:"+Arrays.toString(srcBytes));
byte[] huffmanCodeBytes = huffmanEncode(srcBytes);
System.out.println("赫夫曼编码字节数组:"+Arrays.toString(huffmanCodeBytes));
byte[] destBytes = huffmanDecode(huffmanCodeBytes);
System.out.println("目标字节数组:"+Arrays.toString(destBytes));
System.out.println("目标字符串内容:"+new String(destBytes));
}
}
结果: