2.4 赫夫曼树
2.4.1 创建赫夫曼树
赫夫曼树介绍:
-
赫夫曼树也称为哈夫曼数、霍夫曼树
-
赫夫曼树是WPL带权路径长度最短,权值越大越处于树的顶端
-
路径、路径长度:在一棵树内,从节点A到节点B经过的节点的通路为路径,经过节点数目为路径长度
-
节点的权与带权路径长度:给每一个节点赋上一个值作为权值,带权路径长度就是从根节点到该节点的路径长度 X 该节点的权
创建赫夫曼树思路: -
理论步骤在离散数学里学过,在此不在赘述
实现代码与输出结果:
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
class hfumanTree implements Comparable<hfumanTree>{
private int value;
hfumanTree left;
hfumanTree right;
public hfumanTree(int value){
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public hfumanTree getLeft() {
return left;
}
public void setLeft(hfumanTree left) {
this.left = left;
}
public hfumanTree getRight() {
return right;
}
public void setRight(hfumanTree right) {
this.right = right;
}
@Override
public int compareTo(hfumanTree node) {
return this.value - node.value;
}
@Override
public String toString() {
String leftValue,rightValue;
if (left==null)
leftValue = "null";
else
leftValue = String.valueOf(left.value);
if (right==null)
rightValue = "null";
else
rightValue = String.valueOf(right.value);
return "left: "+leftValue+" value: "+value+" right: "+rightValue;
}
}
public class hfumanTreeDemo {
// 前序遍历赫夫曼树
public static void preOrder(hfumanTree node){
System.out.println(node);
if (node.left!=null)
preOrder(node.left);
if (node.right!=null)
preOrder(node.right);
}
public static void main(String[] args) {
// 1.实现节点的创建和排序
int[] array = {13,7,8,3,29,6,1}; // 1 3 6 7 8 13 29
List<hfumanTree> list = new ArrayList<>();
for (int i:array)
list.add(new hfumanTree(i));
Collections.sort(list);
// System.out.println(list);
while (list.size() > 1){ // 当列表元素个数只有一个时代表哈弗曼树创建成功
hfumanTree hfumanTree01 = list.get(0);
hfumanTree hfumanTree02 = list.get(1);
// 原最小两个二叉树合并得到parent二叉树
hfumanTree parent = new hfumanTree(hfumanTree01.getValue() + hfumanTree02.getValue());
parent.setLeft(hfumanTree01);
parent.setRight(hfumanTree02);
// 在list内添加parent二叉树 移出原最小两个二叉树
list.add(parent);
list.remove(hfumanTree01);
list.remove(hfumanTree02);
// 记得重新排序下
Collections.sort(list);
}
preOrder(list.get(0));
System.out.println(list.get(0));
}
}
输出结果:
left: 29 value: 67 right: 38
left: null value: 29 right: null
left: 15 value: 38 right: 23
left: 7 value: 15 right: 8
left: null value: 7 right: null
left: null value: 8 right: null
left: 10 value: 23 right: 13
left: 4 value: 10 right: 6
left: 1 value: 4 right: 3
left: null value: 1 right: null
left: null value: 3 right: null
left: null value: 6 right: null
left: null value: 13 right: null
left: 29 value: 67 right: 38
根据输出结果画的二叉树:
2.4.2 赫夫曼编码
介绍:
- 赫夫曼编码也称为哈弗曼编码、霍夫曼编码,是一种编码方式、算法;
- 赫夫曼码是一种可变字长编码
- 赫夫曼编码广泛应用于数据文件压缩之中,压缩率处于**20%—90%**之间
- 赫夫曼编码是赫夫曼树在电讯通信中经典应用之一
- 赫夫曼编码是无损压缩,在解码时不会造成数据精度丢失
- 同一个文件经赫夫曼编码后创建的赫夫曼树可能会不一样,但是只要编码后生成的字符串长度一致那就问题不大
相关知识点了解: 定长编码与可变字长编码
- 为什么字符出现越多 编码数越小?
答:编码数越小,所需使用位数越小,即占用空间越小,而字符次数出现越多,代表需要使用对应编码数的次数也越多,这个时候使用 越小的编码数就越节省占用空间
- 但是使用编码代替字符会出现一个问题?
-
识别问题,识别的编码是一个字符的还是多个字符混合起来的,这个就会有二义性
-
这里就要引出一个新的概念,前缀编码(字符的编码的前缀不能是其他字符编码的前缀),前缀编码就解决了识别二义性的问题
- 为什么赫夫曼编码可以压缩? 使用可变长编码节省了原定长编码多余位上的资源浪费,实现了空间压缩
实现数据压缩:
- 创建赫夫曼树和生成对应赫夫曼编码
代码:
import javax.xml.soap.Node;
import java.util.*;
class node implements Comparable<node>{
private Byte data; // 将字符转为字节存储
private int weight; // 权重
private node left; // 左子节点
private node right; // 右子节点
public node(Byte data,int weight){
this.data = data;
this.weight = weight;
}
public Byte getData() {
return data;
}
public void setData(byte data) {
this.data = data;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
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: [data: "+data+" weight: "+weight+"]";
}
@Override
public int compareTo(node o) {
return this.weight - o.weight;
}
// 前序遍历
public void preOrder(){
System.out.println(this);
if (this.left != null)
this.left.preOrder();
if (this.right != null)
this.right.preOrder();
}
}
public class hfumanCodeDemo {
private static Map<Byte,String> hfumanCode = new HashMap<>();
private static StringBuilder stringBuilder = new StringBuilder();
public static node createHfumanTree(String str){
byte[] bytes = str.getBytes();
// 使用map统计字符出现次数
Map<Byte,Integer> map = new HashMap<>();
for (byte b1 : bytes){
Integer count = map.get(b1);
if (count == null)
map.put(b1,1);
else
map.put(b1,count+1);
}
// 遍历map创建node节点添加到List内
List<node> list = new ArrayList<>();
for (Map.Entry<Byte,Integer> entry : map.entrySet()){
list.add(new node(entry.getKey(),entry.getValue()));
}
Collections.sort(list);
// 开始创建赫夫曼树
while (list.size() > 1){
node node1 = list.get(0);
node node2 = list.get(1);
node parent = new node(null,node1.getWeight()+node2.getWeight());
parent.setLeft(node1);
parent.setRight(node2);
list.remove(node1);
list.remove(node2);
list.add(parent);
Collections.sort(list);
}
return list.get(0);
}
// 重载getCode
public static void getCode(node root){
getCode(root,"",stringBuilder);
}
/**
*
* @param node node节点
* @param code 传入路径选择 左子节点:0 右节点:1
* @param stringBuilder 用户拼接路径的StringBuilder
*/
public static void getCode(node node,String code,StringBuilder stringBuilder){
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
stringBuilder2.append(code);
if (node!=null){
// 判断是否为非叶子节点
if (node.getData() == null){
getCode(node.getLeft(),"0",stringBuilder2);
getCode(node.getRight(),"1",stringBuilder2);
}else { // 若为叶子节点的话 则代表遍历到路径终点了
hfumanCode.put(node.getData(),stringBuilder2.toString());
}
}
}
public static void main(String[] args) {
String str = "i like like like java do you like a java";
node root = createHfumanTree(str);
// root.preOrder();
getCode(root);
for(Map.Entry<Byte,String> entry: hfumanCode.entrySet()){
System.out.println(entry);
}
}
}
输出结果:
32=01
97=100
100=11000
117=11001
101=1110
118=11011
105=101
121=11010
106=0010
107=1111
108=000
111=0011
-
使用赫夫曼编码表生成赫夫曼编码压缩数据和实现压缩数据的恢复
-
先将原字符串按照我们的赫夫曼树转为对应的赫夫曼编码字符串,再将该字符串转为byte数组,压缩成功
- 解压:
- 1.先将hfumanBytes恢复为二进制字符串
- 先处理除最后一位之外的所有字节
- 特意将最后一位字节挑出来处理
- 2.将之前的哈弗曼编码表反转得到一个新的map表
- 3.根据第二步得到的新map表 反转二进制编码,得到原数据的字节数组
- 1.先将hfumanBytes恢复为二进制字符串
实现代码与输出实例:
import javax.xml.soap.Node;
import java.util.*;
class node implements Comparable<node>{
private Byte data; // 将字符转为字节存储
private int weight; // 权重
private node left; // 左子节点
private node right; // 右子节点
public node(Byte data,int weight){
this.data = data;
this.weight = weight;
}
public Byte getData() {
return data;
}
public void setData(byte data) {
this.data = data;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
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: [data: "+data+" weight: "+weight+"]";
}
@Override
public int compareTo(node o) {
return this.weight - o.weight;
}
// 前序遍历
public void preOrder(){
System.out.println(this);
if (this.left != null)
this.left.preOrder();
if (this.right != null)
this.right.preOrder();
}
}
public class hfumanCodeDemo {
private static Map<Byte,String> hfumanCode = new HashMap<>();
private static StringBuilder stringBuilder = new StringBuilder();
private static Long zipLen;
public static node createHfumanTree(String str){
byte[] bytes = str.getBytes();
// 使用map统计字符出现次数
Map<Byte,Integer> map = new HashMap<>();
for (byte b1 : bytes){
Integer count = map.get(b1);
if (count == null)
map.put(b1,1);
else
map.put(b1,count+1);
}
// 遍历map创建node节点添加到List内
List<node> list = new ArrayList<>();
for (Map.Entry<Byte,Integer> entry : map.entrySet()){
list.add(new node(entry.getKey(),entry.getValue()));
}
Collections.sort(list);
// 开始创建赫夫曼树
while (list.size() > 1){
node node1 = list.get(0);
node node2 = list.get(1);
node parent = new node(null,node1.getWeight()+node2.getWeight());
parent.setLeft(node1);
parent.setRight(node2);
list.remove(node1);
list.remove(node2);
list.add(parent);
Collections.sort(list);
}
return list.get(0);
}
// 重载getCode
public static void getCode(node root){
getCode(root,"",stringBuilder);
}
/**
*
* @param node node节点
* @param code 传入路径选择 左子节点:0 右节点:1
* @param stringBuilder 用户拼接路径的StringBuilder
*/
public static void getCode(node node,String code,StringBuilder stringBuilder){
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
stringBuilder2.append(code);
if (node!=null){
// 判断是否为非叶子节点
if (node.getData() == null){
getCode(node.getLeft(),"0",stringBuilder2);
getCode(node.getRight(),"1",stringBuilder2);
}else { // 若为叶子节点的话 则代表遍历到路径终点了
hfumanCode.put(node.getData(),stringBuilder2.toString());
}
}
}
// 实现对数据进行赫夫曼压缩压缩
public static byte[] zip(byte[] bytes){
StringBuilder stringBuilder1 = new StringBuilder();
// 1.遍历传入的byte数组,根据生成的哈弗曼code表进行转换,得到哈弗曼编码后的字符串
for (byte b1 : bytes){
stringBuilder1.append(hfumanCode.get(b1));
}
System.out.println("压缩二进制1:"+stringBuilder1);
zipLen = (long)stringBuilder1.length();
// 2.将字符串转换为byte数组
int len = (stringBuilder1.length() % 8 == 0)?stringBuilder1.length()/8:stringBuilder1.length()/8+1;
// System.out.println(len);
byte[] newBytes = new byte[len]; // 用于承载压缩过后的byte[]
int count = 0;
for(int i=0;i<stringBuilder1.length();i += 8){
if (i+8 > stringBuilder1.length()){
// 将字符串转为int 以二进制表示radix = 2
newBytes[count] = (byte) Integer.parseInt(stringBuilder1.substring(i),2);
count++;
}else {
newBytes[count] = (byte) Integer.parseInt(stringBuilder1.substring(i, i + 8), 2);
count++;
}
}
return newBytes;
}
// 将创建哈弗曼树,生成哈弗曼码,实现哈弗曼编码压缩封装起来
public static byte[] hfumanZip(String str){
byte[] bytes = str.getBytes();
getCode(createHfumanTree(str));
byte[] zip = zip(bytes);
System.out.println("压缩成功! 压缩率为:"+(double)(bytes.length-zip.length)/bytes.length*100+"%");
return zip;
}
/**
* 解码哈弗曼编码
* @param hfumanCode 哈弗曼编码表
* @param hfumanBytes 哈弗曼码字节数组
*/
public static byte[] decode(Map<Byte,String> hfumanCode,byte[] hfumanBytes){
// 1.先将hfumanBytes恢复为二进制字符串
// 1.1 先处理除最后一位之外的所有字节
StringBuilder hfumanStringBuilder = new StringBuilder();
for (int i=0;i<hfumanBytes.length-1;i++){
hfumanStringBuilder.append(byteToBitString(true,hfumanBytes[i]));
}
// 1.2 特意将最后一位挑出来单独处理
String str = byteToBitString(false, hfumanBytes[hfumanBytes.length - 1]);
// 1.3 开始对最后一位字节恢复的二进制字符串进行补零操作
// 如果不用补零刚刚好等长,那就很nice 直接得到正确完整的二进制字符串
// System.out.println("zipLen: "+zipLen);
if (str.length()+hfumanStringBuilder.length() == zipLen){
hfumanStringBuilder.append(str);
}else { // 如果不等长,那就补零,直到等长为止
while (str.length()+hfumanStringBuilder.length() < zipLen){
hfumanStringBuilder.append("0");
}
hfumanStringBuilder.append(str);
}
// System.out.println("压缩二进制2:"+hfumanStringBuilder);
// 2.将之前的哈弗曼编码表反转得到一个新的map表
// 第一步结束,得到了正确的二进制字符串
Map<String,Byte> map = new HashMap<>();
for (Map.Entry<Byte,String> entry : hfumanCode.entrySet()){
map.put(entry.getValue(),entry.getKey());
}
// 3.根据第二步得到的新map表 反转二进制编码 得到原数据byte[]
// 先将恢复的数据存入list最后在一起转化为byte[]返回
int j = 0; // 指向当前扫描到的字符串位置
int count; // 标识往j后多少位截取字符串
boolean flag; // 标识是否匹配成功
String temp;
List<Byte> list = new ArrayList<>();
// System.out.println(hfumanStringBuilder.length());
while (j < hfumanStringBuilder.length()){
flag = false;
count = 1;
while (j+count <= hfumanStringBuilder.length() && !flag){
temp = hfumanStringBuilder.substring(j,j+count);
if (map.containsKey(temp)) { // 匹配成功 退出循环 添加byte到列表
list.add(map.get(temp));
// System.out.println(temp);
flag = true;
}else // 匹配失败 count++ 重新匹配
count++;
}
// if (flag)
j = j+count; // 将j指针移位
// System.out.println(j);
}
// 4.将list转为byte[]返回
byte[] bytes = new byte[list.size()];
for (int i=0;i<list.size();i++)
bytes[i] = list.get(i);
return bytes;
}
// 将byte转换为二进制表示的字符串
// 需要考虑最后一个字节原本就不满8位的情况
// flag = true 代表该字节不是最后一个字节 false代表该字节为最后一个字节
// 为什么需要截取8位? 因为int默认输出4个字节32位(负数),正数的话该几位输出几位(有可能出现不满8位的情况)
// 但是最后一位字节在压缩的时候到底是几位压缩成一个字节我们是不知道的(如: 01-->00000001 , 001--->00000001),所以在此方法默认不做处理,补零交给decode方法处理
public static String byteToBitString(boolean flag,byte b){
int temp = b;
if (flag){ // flag为true 需要补高位
temp = temp|256;
}
String str = Integer.toBinaryString(temp);
if (flag || temp<0){ // 最后一位字节 或 负数都需要截取8位。
return str.substring(str.length()-8);
}else // 最后一位且是正数的情况下 不在此方法做补零处理 直接返回
return str;
}
public static void main(String[] args) {
String str = "i like like like java do you like a java";
byte[] zip = hfumanZip(str);
byte[] decode = decode(hfumanCode, zip);
System.out.println("压缩前:"+ Arrays.toString(str.getBytes()));
System.out.println("压缩后:"+ Arrays.toString(decode));
}
}
输出结果:
压缩二进制1:1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
压缩成功! 压缩率为:57.49999999999999%
压缩前:[105, 32, 108, 105, 107, 101, 32, 108, 105, 107, 101, 32, 108, 105, 107, 101, 32, 106, 97, 118, 97, 32, 100, 111, 32, 121, 111, 117, 32, 108, 105, 107, 101, 32, 97, 32, 106, 97, 118, 97]
解压后:[105, 32, 108, 105, 107, 101, 32, 108, 105, 107, 101, 32, 108, 105, 107, 101, 32, 106, 97, 118, 97, 32, 100, 111, 32, 121, 111, 117, 32, 108, 105, 107, 101, 32, 97, 32, 106, 97, 118, 97]
2.4.3 赫夫曼编码实现文件的压缩与解压
压缩思路:
- 在之前写的压缩方法上进行优化
- 添加对文件的读取和将压缩完的字节数组和赫夫曼编码表以对象流的形式输出到指定文件
/**
* 实现对文件的压缩
* @param zipFile 需要压缩文件的路径
* @param destFile 压缩文件存放路径
*/
public static void FileZip(String zipFile,String destFile){
BufferedInputStream bufferedInputStream = null;
ObjectOutputStream objectOutputStream = null;
try {
// 读取文件
bufferedInputStream = new BufferedInputStream(Files.newInputStream(new File(zipFile).toPath()));
objectOutputStream = new ObjectOutputStream(Files.newOutputStream(Paths.get(destFile)));
byte[] bytes = new byte[bufferedInputStream.available()]; // 创建byte[] 承载流数据
bufferedInputStream.read(bytes);
byte[] zip = hfumanZip(bytes); // 开始压缩
System.out.println("zip: "+zip.length);
System.out.println("bytes: "+bytes.length);
System.out.println("压缩成功! 压缩率为:"+((double)(bytes.length-zip.length)/bytes.length)*100+"%");
objectOutputStream.writeObject(zip); // 实现压缩文件的写
objectOutputStream.writeObject(hfumanCode); // 实现赫夫曼编码表的写
}catch (Exception e){
System.out.println(e.getMessage());
}finally { // 最后关闭流资源
if (bufferedInputStream!=null && objectOutputStream!=null)
try {
bufferedInputStream.close();
objectOutputStream.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
解压思路:
- 在之前写的解压方法上进行优化
- 添加读取以对象流方式读取压缩文件,读取出之前存入压缩文件的赫夫曼编码表与压缩的字节数组,通过解码方法将字节数组恢复为原先的形式,最后将恢复字节数组以输出流的方式输出到指定文件
/**
* 实现对压缩文件的解压
* @param zipFile 压缩文件路径
* @param destFile 解压恢复文件路径
*/
public static void unZip(String zipFile,String destFile){
// 对象输入流读取压缩文件
ObjectInputStream objectInputStream = null;
// 输出流将数据输出到指定恢复文件
BufferedOutputStream bufferedOutputStream = null;
try {
objectInputStream = new ObjectInputStream(Files.newInputStream(new File(zipFile).toPath()));
bufferedOutputStream = new BufferedOutputStream(Files.newOutputStream(new File(destFile).toPath()));
byte[] bytes = (byte[])objectInputStream.readObject(); // 读取编码后的字节数组
// 读取哈弗曼编码表
Map<Byte,String> hfumanCode = (Map<Byte, String>) objectInputStream.readObject();
byte[] decode = decode(hfumanCode, bytes); // 得到解码字节数组
bufferedOutputStream.write(decode);
}catch (Exception e){
System.out.println(e.getMessage());
}finally {
if (objectInputStream!=null && bufferedOutputStream!=null)
try {
bufferedOutputStream.close();
objectInputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
完整代码与输出结果:
import javax.xml.soap.Node;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
class node implements Comparable<node>{
private Byte data; // 将字符转为字节存储
private int weight; // 权重
private node left; // 左子节点
private node right; // 右子节点
public node(Byte data,int weight){
this.data = data;
this.weight = weight;
}
public Byte getData() {
return data;
}
public void setData(byte data) {
this.data = data;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
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: [data: "+data+" weight: "+weight+"]";
}
@Override
public int compareTo(node o) {
return this.weight - o.weight;
}
// 前序遍历
public void preOrder(){
System.out.println(this);
if (this.left != null)
this.left.preOrder();
if (this.right != null)
this.right.preOrder();
}
}
public class hfumanCodeDemo {
private static Map<Byte,String> hfumanCode = new HashMap<>();
private static StringBuilder stringBuilder = new StringBuilder();
private static Long zipLen;
public static node createHfumanTree(byte[] bytes){
// byte[] bytes = str.getBytes();
// 使用map统计字符出现次数
Map<Byte,Integer> map = new HashMap<>();
for (byte b1 : bytes){
Integer count = map.get(b1);
if (count == null)
map.put(b1,1);
else
map.put(b1,count+1);
}
// 遍历map创建node节点添加到List内
List<node> list = new ArrayList<>();
for (Map.Entry<Byte,Integer> entry : map.entrySet()){
list.add(new node(entry.getKey(),entry.getValue()));
}
Collections.sort(list);
// 开始创建赫夫曼树
while (list.size() > 1){
node node1 = list.get(0);
node node2 = list.get(1);
node parent = new node(null,node1.getWeight()+node2.getWeight());
parent.setLeft(node1);
parent.setRight(node2);
list.remove(node1);
list.remove(node2);
list.add(parent);
Collections.sort(list);
}
return list.get(0);
}
// 重载getCode
public static void getCode(node root){
getCode(root,"",stringBuilder);
}
/**
*
* @param node node节点
* @param code 传入路径选择 左子节点:0 右节点:1
* @param stringBuilder 用户拼接路径的StringBuilder
*/
public static void getCode(node node,String code,StringBuilder stringBuilder){
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
stringBuilder2.append(code);
if (node!=null){
// 判断是否为非叶子节点
if (node.getData() == null){
getCode(node.getLeft(),"0",stringBuilder2);
getCode(node.getRight(),"1",stringBuilder2);
}else { // 若为叶子节点的话 则代表遍历到路径终点了
hfumanCode.put(node.getData(),stringBuilder2.toString());
}
}
}
// 实现对数据进行赫夫曼压缩压缩
public static byte[] zip(byte[] bytes){
StringBuilder stringBuilder1 = new StringBuilder();
// 1.遍历传入的byte数组,根据生成的哈弗曼code表进行转换,得到哈弗曼编码后的字符串
for (byte b1 : bytes){
stringBuilder1.append(hfumanCode.get(b1));
}
// System.out.println("压缩二进制1:"+stringBuilder1);
zipLen = (long)stringBuilder1.length();
// 2.将字符串转换为byte数组
int len = (stringBuilder1.length() % 8 == 0)?stringBuilder1.length()/8:stringBuilder1.length()/8+1;
// System.out.println(len);
byte[] newBytes = new byte[len]; // 用于承载压缩过后的byte[]
int count = 0;
for(int i=0;i<stringBuilder1.length();i += 8){
if (i+8 > stringBuilder1.length()){
// 将字符串转为int 以二进制表示radix = 2
newBytes[count] = (byte) Integer.parseInt(stringBuilder1.substring(i),2);
count++;
}else {
newBytes[count] = (byte) Integer.parseInt(stringBuilder1.substring(i, i + 8), 2);
count++;
}
}
return newBytes;
}
// 将创建哈弗曼树,生成哈弗曼码,实现哈弗曼编码压缩封装起来
public static byte[] hfumanZip(byte[] bytes){
// byte[] bytes = str.getBytes();
getCode(createHfumanTree(bytes));
return zip(bytes);
}
/**
* 解码哈弗曼编码
* @param hfumanCode 哈弗曼编码表
* @param hfumanBytes 哈弗曼码字节数组
*/
public static byte[] decode(Map<Byte,String> hfumanCode,byte[] hfumanBytes){
// 1.先将hfumanBytes恢复为二进制字符串
// 1.1 先处理除最后一位之外的所有字节
StringBuilder hfumanStringBuilder = new StringBuilder();
for (int i=0;i<hfumanBytes.length-1;i++){
hfumanStringBuilder.append(byteToBitString(true,hfumanBytes[i]));
}
// 1.2 特意将最后一位挑出来单独处理
String str = byteToBitString(false, hfumanBytes[hfumanBytes.length - 1]);
// 1.3 开始对最后一位字节恢复的二进制字符串进行补零操作
// 如果不用补零刚刚好等长,那就很nice 直接得到正确完整的二进制字符串
// System.out.println("zipLen: "+zipLen);
if (str.length()+hfumanStringBuilder.length() == zipLen){
hfumanStringBuilder.append(str);
}else { // 如果不等长,那就补零,直到等长为止
while (str.length()+hfumanStringBuilder.length() < zipLen){
hfumanStringBuilder.append("0");
}
hfumanStringBuilder.append(str);
}
// System.out.println("压缩二进制2:"+hfumanStringBuilder);
// 2.将之前的哈弗曼编码表反转得到一个新的map表
// 第一步结束,得到了正确的二进制字符串
Map<String,Byte> map = new HashMap<>();
for (Map.Entry<Byte,String> entry : hfumanCode.entrySet()){
map.put(entry.getValue(),entry.getKey());
}
// 3.根据第二步得到的新map表 反转二进制编码 得到原数据byte[]
// 先将恢复的数据存入list最后在一起转化为byte[]返回
int j = 0; // 指向当前扫描到的字符串位置
int count; // 标识往j后多少位截取字符串
boolean flag; // 标识是否匹配成功
String temp;
List<Byte> list = new ArrayList<>();
// System.out.println(hfumanStringBuilder.length());
while (j < hfumanStringBuilder.length()){
flag = false;
count = 1;
while (j+count <= hfumanStringBuilder.length() && !flag){
temp = hfumanStringBuilder.substring(j,j+count);
if (map.containsKey(temp)) { // 匹配成功 退出循环 添加byte到列表
list.add(map.get(temp));
// System.out.println(temp);
flag = true;
}else // 匹配失败 count++ 重新匹配
count++;
}
// if (flag)
j = j+count; // 将j指针移位
// System.out.println(j);
}
// 4.将list转为byte[]返回
byte[] bytes = new byte[list.size()];
for (int i=0;i<list.size();i++)
bytes[i] = list.get(i);
return bytes;
}
// 将byte转换为二进制表示的字符串
// 需要考虑最后一个字节原本就不满8位的情况
// flag = true 代表该字节不是最后一个字节 false代表该字节为最后一个字节
// 为什么需要截取8位? 因为int默认输出4个字节32位(负数),正数的话该几位输出几位(有可能出现不满8位的情况)
// 但是最后一位字节在压缩的时候到底是几位压缩成一个字节我们是不知道的(如: 01-->00000001 , 001--->00000001),所以在此方法默认不做处理,补零交给decode方法处理
public static String byteToBitString(boolean flag,byte b){
int temp = b;
if (flag){ // flag为true 需要补高位
temp = temp|256;
}
String str = Integer.toBinaryString(temp);
if (flag || temp<0){ // 最后一位字节 或 负数都需要截取8位。
return str.substring(str.length()-8);
}else // 最后一位且是正数的情况下 不在此方法做补零处理 直接返回
return str;
}
/**
* 实现对文件的压缩
* @param zipFile 需要压缩文件的路径
* @param destFile 压缩文件存放路径
*/
public static void FileZip(String zipFile,String destFile){
BufferedInputStream bufferedInputStream = null;
ObjectOutputStream objectOutputStream = null;
try {
// 读取文件
bufferedInputStream = new BufferedInputStream(Files.newInputStream(new File(zipFile).toPath()));
objectOutputStream = new ObjectOutputStream(Files.newOutputStream(Paths.get(destFile)));
byte[] bytes = new byte[bufferedInputStream.available()]; // 创建byte[] 承载流数据
bufferedInputStream.read(bytes);
byte[] zip = hfumanZip(bytes); // 开始压缩
System.out.println("zip: "+zip.length);
System.out.println("bytes: "+bytes.length);
System.out.println("压缩成功! 压缩率为:"+((double)(bytes.length-zip.length)/bytes.length)*100+"%");
objectOutputStream.writeObject(zip); // 实现压缩文件的写
objectOutputStream.writeObject(hfumanCode); // 实现赫夫曼编码表的写
}catch (Exception e){
System.out.println(e.getMessage());
}finally { // 最后关闭流资源
if (bufferedInputStream!=null && objectOutputStream!=null)
try {
bufferedInputStream.close();
objectOutputStream.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
/**
* 实现对压缩文件的解压
* @param zipFile 压缩文件路径
* @param destFile 解压恢复文件路径
*/
public static void unZip(String zipFile,String destFile){
// 对象输入流读取压缩文件
ObjectInputStream objectInputStream = null;
// 输出流将数据输出到指定恢复文件
BufferedOutputStream bufferedOutputStream = null;
try {
objectInputStream = new ObjectInputStream(Files.newInputStream(new File(zipFile).toPath()));
bufferedOutputStream = new BufferedOutputStream(Files.newOutputStream(new File(destFile).toPath()));
byte[] bytes = (byte[])objectInputStream.readObject(); // 读取编码后的字节数组
// 读取哈弗曼编码表
Map<Byte,String> hfumanCode = (Map<Byte, String>) objectInputStream.readObject();
byte[] decode = decode(hfumanCode, bytes); // 得到解码字节数组
bufferedOutputStream.write(decode);
}catch (Exception e){
System.out.println(e.getMessage());
}finally {
if (objectInputStream!=null && bufferedOutputStream!=null)
try {
bufferedOutputStream.close();
objectInputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
// String str = "i like like like java do you like a java";
// byte[] zip = hfumanZip(str);
// byte[] decode = decode(hfumanCode, zip);
// System.out.println("压缩前:"+ Arrays.toString(str.getBytes()));
// System.out.println("压缩后:"+ Arrays.toString(decode));
FileZip("t1.txt","t1.rar");
unZip("t1.rar","t2.txt");
}
}
输出结果:
zip: 173847
bytes: 251537
压缩成功! 压缩率为:30.8861121823032%