java实现赫夫曼编码实现文件的压缩与解压缩
一、基本概念
哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,哈夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码(有时也称为霍夫曼编码)。
赫夫曼码的码字(各符号的代码)是异前置码字,即任一码字不会是另一码字的前面部分,这使各码字可以连在一起传送,中间不需另加隔离符号,只要传送时不出错,收端仍可分离各个码字,不致混淆。
二、思路分析
哈夫曼编码就是利用哈夫曼树来实现的,通过构造哈夫曼树,一半在构造过程中,向左路径规定为0,向右路径规定为1,这样元素路径上的0和1就构成了哈夫曼编码。下面看一个例子,我们以在哈夫曼树中提到的序列为例:13,7,8,3,29,6,1,构造哈夫曼树如下:
如上图,我们可以看到已经给左右分支都编上了0/1,这样29的编码就是1,7的编码就是100,8的编码就是101,1的编码就是11000,剩下的以此类推。
利用哈夫曼编码的特征,我们可以使用哈夫曼编码进行压缩文件,以一个简单的字符串为例:“i like like like java do you like a java”,如果直接使用二进制进行编码,则编码长度为359,下面我们来看使用哈夫曼编码的情况。我们首先统计各个字符出现的次数来作为权值,构造一棵哈夫曼树,其中:d:1 y:1 u:1 j:1 v:2 o:2 l:2 k:4 e:4 i:5 a:5 空格:9。
然后使用上述方法进行编码:
o:1000 u:10010 d:100110 y:100111 i:101 a:110 k:1110 e:1111 j:0000 v:0001 l:001 空格:01
所以整个字符串的编码就是:1010100110111101111010011011110111101001101111011110100001100001110011001101000011001111000100100100110111101111011100100001100001110
长度:133,压缩率(359-133)/359=62.9%。
注意:在构造哈夫曼树时,由于哈夫曼树的结构可能不同,所以编码也可能不同,但是wpl是一样的,都是最小的。最后生成的哈夫曼编码长度也是一样的。
三、代码实现
利用哈夫曼编码进行压缩分为以下几步:
将待压缩文件存入byte[]数组中
将byte[]数组中的内容进行统计,并返回节点所组成的list
创建哈夫曼树
利用哈夫曼树进行哈夫曼编码并存放起来
根据哈夫曼编码得到的编码表,将进行哈夫曼编码后的0/1形式的串按8位划分并转化为十进制形式进行存储
解压操作是压缩操作的逆操作:
首先接收压缩后的以十进制存放的byte[]数组以及编码表
将编码后的十进制数还原为二进制数,即0/1形式
根据编码表,将二进制形式串还原为初始形式。
四、详细代码
构建结点类
package com.ma.tree.huffman;
public class NodeCode implements Comparable<NodeCode>{
//用来描述文字(字母)的十进制数
public Byte data;
//权重
public int weight;
public NodeCode left;
public NodeCode right;
public NodeCode(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
public NodeCode(int weight) {
this.weight = weight;
}
@Override
public int compareTo(NodeCode o) {
return this.weight - o.weight;
}
@Override
public String toString() {
return "NodeCode{" +
"data=" + data +
", weight=" + weight +
'}';
}
//前序遍历
public void preselect(){
System.out.println(this);
if (this.left != null){
this.left.preselect();
}
if (this.right != null){
this.right.preselect();
}
}
}
压缩与解压
package com.ma.tree.huffman;
import java.nio.charset.StandardCharsets;
import java.sql.SQLOutput;
import java.util.*;
public class HuffmanCodes {
/**
* 1.需要统计字母所出现的次数
* 2.创建赫夫曼树
* 3.获取赫夫曼编码
* 4.数据压缩
*/
private static Map<Byte,String> map = new HashMap<>();
private static StringBuilder stringBuilder = new StringBuilder();
public static void main(String[] args) {
String string = "I'm mawencheng what's your name";
byte[] bytes = string.getBytes();
System.out.println(Arrays.toString(bytes));
List<NodeCode> nodeWeight = getNodeWeight(bytes);
NodeCode root = createHuffmanTree(nodeWeight);
Map<Byte,String> huffmanCodes = createHuffmanCodes(root);
System.out.println("生成的赫夫曼编码:"+huffmanCodes);
byte[] zip = zip(bytes,huffmanCodes);
System.out.println("压缩后的数组:"+Arrays.toString(zip));
float p = ( (float)bytes.length - (float)zip.length ) / (float) bytes.length;
System.out.println("压缩比例:"+p);
byte[] decode = decode(map,zip);
System.out.println("解压后:"+new String(decode));
}
/*
* 统计字符串所出现的次数
* 返回集合[Node{data=32,weight=4},]
* */
public static List<NodeCode> getNodeWeight(byte[] bytes){
List<NodeCode> nodeCodes = new ArrayList<>();
//每一个byte出现的次数,使用一个map集合, key=byte value=weight
Map<Byte,Integer> map = new HashMap<>();
for (byte abyte : bytes) {
Integer count = map.get(abyte);
if (count == null){
map.put(abyte,1);
}else {
map.put(abyte,count+1);
}
}
//把每一个map里的对象转NodeCode对象,放在集合里
for (Map.Entry<Byte,Integer> entry : map.entrySet()){
nodeCodes.add(new NodeCode(entry.getKey(),entry.getValue()));
}
return nodeCodes;
}
//创建赫夫曼树
public static NodeCode createHuffmanTree(List<NodeCode> nodeCodes){
//循环处理
while (nodeCodes.size() > 1){
//排序,从小到大
Collections.sort(nodeCodes);
//找到最小的两个元素(结点)
NodeCode left = nodeCodes.get(0);
NodeCode right = nodeCodes.get(1);
//两个最小结点weight相加,生成新的结点
NodeCode root = new NodeCode(left.weight+right.weight);
//将新的结点作为父节点,构成一个新的二叉树
root.right = right;
root.left = left;
//移除最小的两个元素
nodeCodes.remove(left);
nodeCodes.remove(right);
//将新结点添加进集合
nodeCodes.add(root);
}
return nodeCodes.get(0);
}
/*获取赫夫曼编码,
* 1.往左取0 往右取1
* eg:
* a:101 b:1001
* 完整赫夫曼编码为: 1011001
* StringBuilder codes 用来拼接赫夫曼编码
* Map 用来存储叶子结点路径
*
* */
public static void getHuffmanCode(NodeCode nodeCode,String code,StringBuilder stringBuilder){
StringBuilder newString = new StringBuilder(stringBuilder);
newString.append(code);
if (nodeCode != null){
if (nodeCode.data == null){
//向左递归
getHuffmanCode(nodeCode.left,"0",newString);
//向右递归
getHuffmanCode(nodeCode.right,"1",newString);
}else {
map.put(nodeCode.data,newString.toString());
}
}
}
//设置统一入口
public static Map<Byte,String> createHuffmanCodes(NodeCode root){
if (root == null){
return null;
}
//处理左子树
getHuffmanCode(root.left,"0",stringBuilder);
//处理右子树
getHuffmanCode(root.right,"1",stringBuilder);
return map;
}
//数据压缩
public static byte[] zip(byte[] bytes,Map<Byte,String> huffmanCodes){
//用来拼接所有叶子结点路径
StringBuilder stringBuilder = new StringBuilder();
for (byte b : bytes){
stringBuilder.append(huffmanCodes.get(b));
}
int length;
if (stringBuilder.length() % 8 ==0){
length = stringBuilder.length() / 8;
}else {
length = stringBuilder.length() / 8 + 1;
}
//压缩数据的数组
byte[] huffmanBytes = new byte[length];
int index = 0;
for (int i = 0; i < stringBuilder.length(); i += 8) {
String str;
if ( i+8 > stringBuilder.length()){
str = stringBuilder.substring(i);
}else {
str = stringBuilder.substring(i,i+8);
}
huffmanBytes[index] = (byte) Integer.parseInt(str,2);
index++;
}
return huffmanBytes;
}
//如何对数据进行解压
public static String byteToString(boolean isRepair,byte b){
/*
* byte:占8个二进制位
* int:占32个二进制位
* 正数时需要补高8位,负数不需要补
* */
int temp = b;
if (isRepair){
temp = temp | 256;
}
String string= Integer.toBinaryString(temp);
if (isRepair){
return string.substring(string.length()-8);
}else {
return string;
}
}
public static byte[] decode(Map<Byte,String> huffmanCode,byte[] huffmanBytes){
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < huffmanBytes.length;i++) {
byte b = huffmanBytes[i];
boolean flag = false;
if (i == huffmanBytes.length-1 ){
flag = true;
}
stringBuilder.append(byteToString(!flag,b));
}
Map<String,Byte> map = new HashMap<>();
for (Map.Entry<Byte,String> entry : huffmanCode.entrySet()){
map.put(entry.getValue(),entry.getKey());
}
List<Byte> bytes = new ArrayList<>();
for (int i = 0; i < stringBuilder.length();) {
int count = 0;
boolean flag = true;
Byte b = null;
while (flag) {
String key = stringBuilder.substring(i, i + count);
b = map.get(key);
if (b == null) {
count++;
} else {
flag = false;
}
}
bytes.add(b);
i += count;
}
byte[] buff = new byte[bytes.size()];
for (int i = 0; i < buff.length; i++) {
buff[i] = bytes.get(i);
System.out.println(buff[i]);
}
return buff;
}
}