给你一个图片文件,要求对其进行无损压缩, 看看压缩效果如何。
思路:读取文件-> 得到赫夫曼编码表 -> 完成压缩
将前面压缩的文件,重新恢复成原来的文件。
思路:读取压缩文件(数据和赫夫曼编码表)-> 完成解压(文件恢复)
package com.zhen;
import java.io.*;
import java.util.*;
public class Huffmancode {
public static void main(String[] args) throws Exception {
/*String srcfile="e://src.jpg";
String dstfile="e://dst.zip";
zipfile(srcfile, dstfile);*/
String zipfile="e://dst.zip";
String dstfile="e://src1.jpg";
unzip(zipfile, dstfile);
}
//实现文件的无损压缩
public static void zipfile(String srcfile,String dstfile) throws IOException {
//创建输入流
FileInputStream is=null;
//创建输出流
OutputStream os=null;
ObjectOutputStream oos=null;
try {
is=new FileInputStream(srcfile);
//创建一个和原文件一样大小的byte数组
//.available()可以在读写操作前先得知数据流里有多少个字节可以读取
byte[] b=new byte[is.available()];
//读取文件
is.read(b);
//直接对源文件进行压缩
byte[] huffmanzip = huffmanzip(b);
//创建文件的输出流,存放压缩文件
os=new FileOutputStream(dstfile);
//封装输出流为对象流
oos=new ObjectOutputStream(os);
//把编码后的字节数组写入文件
oos.writeObject(huffmanzip);
//写入哈夫曼编码表
oos.writeObject(huffman);
} catch (FileNotFoundException e) {
System.out.println(e);
}finally {
try {
is.close();
os.close();
oos.close();
} catch (IOException e) {
}
}
}
//实现文件的解压
public static void unzip(String zipfile,String dstfile) throws Exception{
//定义文件输入流
InputStream is = null;
//定义对象输入流
ObjectInputStream ois = null;
//定义文件输出流
OutputStream os = null;;
try {
//创建输入流
is=new FileInputStream(zipfile);
ois=new ObjectInputStream(is);
//存入byte数组
byte[] b=(byte[])ois.readObject();
//读取编码表
Map<Byte, String> code=(Map<Byte, String>)ois.readObject();
//解码
byte[] uncode=decode(code, b);
//写入目标文件
os=new FileOutputStream(dstfile);
//写入数据
os.write(uncode);
} catch (FileNotFoundException e) {
}finally {
is.close();
ois.close();
os.close();
}
}
//三步对应的调用方法
private static byte[] huffmanzip(byte[] bytes) {
//第一步,加入集合并生成哈夫曼树
List<Node> list=getNode(bytes);
Node tree=huffman(list);
//第二步,生成哈夫曼编码
getcode(tree);
//第三步,生成压缩后的数组
byte[] zipb = zip(bytes, huffman);
return zipb;
}
//将哈夫曼表存入map<Byte(数值),String(对应的编码值)>例如:32->01,97->100
static Map<Byte, String> huffman=new HashMap<>();
//生成哈夫曼编码表示,需要去拼接路径,定义一个StringBuilder,存储某个叶子节点的路径
static StringBuffer str=new StringBuffer();
/**
* 第一步
* 生成哈夫曼树
* @param bytes 接收字节数组
* @return 返回List的形式
*/
public static List<Node> getNode(byte[] bytes) {
ArrayList<Node> list = new ArrayList<Node>();
//遍历bytes统计每一个字符出现的次数->map[字符,次数]
Map<Byte,Integer> map = new HashMap<Byte,Integer>();
for(byte b:bytes) {
Integer count=map.get(b);
//未出现过
if (count == null) {
map.put(b, 1);
}else {
map.put(b, count+1);
}
}
//将每一个键值对转成Node对象,加入list
for(Map.Entry<Byte, Integer> entry : map.entrySet()) {
list.add(new Node(entry.getKey(), entry.getValue()));
}
return list;
}
public static Node huffman(List<Node> list) {
while(list.size() > 1) {
//排序
Collections.sort(list);
//取出值最小的两颗二叉树
Node left=list.get(0);
Node right = list.get(1);
//构建一颗新的二叉树
Node parent = new Node(null,left.weight+right.weight);
parent.left=left;
parent.right = right;
//删除使用过的节点
list.remove(left);
list.remove(right);
//将新产生的节点加入list
list.add(parent);
}
return list.get(0);
}
/**
* 第二步
* 生成哈夫曼树对应的编码
*/
//为了调用方便重载getcode,实现传入根节点就返回对应的哈夫曼编码
private static Map<Byte, String> getcode(Node root) {
if (root==null) {
return null;
}
//处理root的左子树
getcode(root.left, "0", str);
//处理root的右左子树
getcode(root.right, "1", str);
return huffman;
}
/**
* 将传入的Node节点的所有叶子节点的哈夫曼编码得到,并存入集合
* @param node 传入节点
* @param code 路径:左子节点为0,右为1
* @param stringBuf 用于拼接路径
*/
private static void getcode(Node node,String code,StringBuffer stringBuf) {
StringBuffer str2 = new StringBuffer(stringBuf);
//将code加入str2
str2.append(code);
//如果为null不处理
if (node!=null) {
//判断是否为叶子节点
//不是
if (node.data==null) {
//递归处理
//向左递归
getcode(node.left, "0", str2);
//向右递归
getcode(node.right, "1", str2);
}else {
//是叶子节点表示找到了最后
huffman.put(node.data, str2.toString());
}
}
}
/**
* 第三步:将字符串对应的byte[]数组,通过生成的哈夫曼编码表,返回一个压缩后的byte[]
* @param bytes 原始字符串对应的byte[]
* @param huffmancode 生成的哈夫曼编码表
* @return 返回压缩后的byte[]
*/
private static byte[] zip(byte[] bytes,Map<Byte, String> huffmancode) {
//利用huffmancode将bytes转成哈夫曼编码对应的字符串
StringBuffer strc = new StringBuffer();
//遍历加入
for(byte b:bytes) {
strc.append(huffmancode.get(b));
}
//统计返回byte[]huffmancode的长度
int len;
if (strc.length()%8==0) {
len=strc.length()/8;
}else {
len=strc.length()/8+1;
}
//创建存储压缩后的byte数组
byte[] zipb=new byte[len];
int index=0;
//每8位对应一个byte
for(int i=0;i<strc.length();i+=8) {
String strbyte;
if (i+8>strc.length()) {
//不够8位,有多少取多少
strbyte=strc.substring(i);
}else {
//够8位,每次取出8位
strbyte=strc.substring(i,i+8);
}
//存入byte数组
zipb[index]=(byte)Integer.parseInt(strbyte,2);
index++;
}
return zipb;
}
//将压缩后的byte数组进行解压得到原来的字符串
/**
* 第一步:取出byte[]中的每一个byte转成二进制字符串
* @param flag true表示为正数则需要补高位,false表示不补
* @param b byte[]中取出的每一个byte
* @return b对应的二进制字符串(补码)
*/
private static String bytetos(boolean flag,byte b) {
//因为int类型有转二进制的方法可以直接用,所以首先将byte存为int
int temp=b;
if (flag) {
//与运算补高位,例如:1 0000 0000|0000 0001->1 0000 0001
temp|=256;
}
//返回对应的二进制补码
String str=Integer.toBinaryString(temp);
if (flag) {
//原数是32位的,我们只需要后8位
return str.substring(str.length()-8);
}else {
//不需要补齐则直接返回
return str;
}
}
/**
* 第二步:得到原字符串
* @param hcode 哈夫曼编码表
* @param huffmanb 哈夫曼编码得到的字节数组(17位)
* @return 原来字符串对应的数组
*/
private static byte[] decode(Map<Byte, String> hcode,byte[] huffmanb) {
//先得到对应的二进制字符串
StringBuilder str = new StringBuilder();
//将byte数组转成二进制的字符串
for(int i=0;i<huffmanb.length; i++) {
byte b = huffmanb[i];
//判断是否为最后一个字节
boolean flag = (i==huffmanb.length-1);
str.append(bytetos(!flag, b));
}
//将字符串按照原来的编码表进行解压
//将编码表进行反转key->value,value->key,因为要进行反向查询
Map<String, Byte> map=new HashMap<>();
for(Map.Entry<Byte, String> entry : hcode.entrySet()) {
map.put(entry.getValue(), entry.getKey());
}
//存放byte
List<Byte> list = new ArrayList<>();
for (int i = 0; i < str.length();) {
int count=1;
boolean flag=true;
Byte b=null;
while(flag) {
//i不动,count移动去匹配每一个字符
String key = str.substring(i,i+count);
b=map.get(key);
if (b == null) {
count++;
}else {
//匹配到就退出
flag = false;
}
}
list.add(b);
//i直接移动到count
i+=count;
}
//for循环后list中就存入了原先的字符,将list中的数据存入byte并返回
byte[] b=new byte[list.size()];
for (int i = 0; i < b.length; i++) {
b[i]= list.get(i);
}
return b;
}
}
class Node implements Comparable<Node> {
//存放数据(字符)本身,比如"a"->97
Byte data;
//字符出现的次数(权值)
int weight;
Node left;
Node right;
public Node(Byte data, int weight) {
super();
this.data = data;
this.weight = weight;
}
@Override
public int compareTo(Node o) {
// 从小到大排序
return this.weight-o.weight;
}
@Override
public String toString() {
return "Node [data=" + data + ", weight=" + weight + "]";
}
}
赫夫曼编码压缩文件注意事项
1)如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化, 比如视频,ppt 等等文件 [举例压一个 .ppt]
2)赫夫曼编码是按字节来处理的,因此可以处理所有的文件(二进制文件、文本文件) [举例压一个.xml文件]
3)如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显.