一、压缩
思路:一个文件中,都会出现重复的字节,有些字节出现的次数多,有些字节出现的次数少,这样我们就可以根据出现次数的多少,构造哈夫曼树,并进行编码,出现次数越多的编码长度越短,出现次数越少的编码长度越长。而我们有知道一个英文字母占用一个byte,一个中文占用两个byte。我们将文件中的字节化为对应的编码后,形成01串,每次取八个01串写入压缩后的文件。因为编码的长度大多情况下都比文件中数据占用bit数更少,所以就文件就压缩了。
具体步骤:
①、统计文件中字节出现的次数
②、根据次数构建哈弗曼树
③、获取每个结点的哈弗曼编码
④将字节用01字符数组表示
/**
* 压缩编码表元素
* @author hpw
*
*/
public class CodeTableNode {
private byte bt;
private Byte[] code;
/**
* 构造器,传入一个byte和代表这个byte的int数组code,code每个元素代表一个二进制位
* 只有0,1
* @param bt 传入的byte
* @param code2 对应byte的二进制编码
*/
public CodeTableNode(byte bt,Byte[] code2){
this.bt=bt;
this.code=code2;
}
public byte getByte() {
return bt;
}
public Byte[] getCode() {
return code;
}
}
/**
* 建立数组的方法(TreeNodeArray:类似优先队列的方法)
*
* @return 根据byte出现次数进行排序的数组
* @throws IOException
*/
private TreeNodeArray getList() throws IOException {
TreeNodeArray tList = new TreeNodeArray();
// 创建输入输出流
java.io.InputStream inputStr = new java.io.FileInputStream(file);
java.io.BufferedInputStream bus=new java.io.BufferedInputStream(inputStr,2048);
int bt=bus.read();
while(bt!=-1){
byte by=(byte)bt;
TreeNode node = new TreeNode(by);
tList.add(node);
bt=bus.read();
}
inputStr.close();// 关闭输入流
bus.close();
return tList;
}
/**
* 通过节点队列创建二叉树,取出前2个节点组成二叉树,将根节点排序插入到队列中,
* 如此循环,最后剩下的就是根节点
*
* @return 二叉树头节点
* @throws Exception
*/
public TreeNode getTree() throws Exception {
TreeNodeArray list = getList();
while (list.size() > 1) {
TreeNode LNode = list.remove(0);
TreeNode RNode = list.remove(0);
TreeNode NNode = new TreeNode(LNode, RNode);
list.insert(NNode);
}
TreeNode rootNode = list.get(0);
return rootNode;
}
/**
* 生成编码表方法
* @return CodeMap格式的编码表
* @throws Exception
*/
public CodeTable getMap() throws Exception{
TreeNode rootNode=this.getTree();
//遍历树
TreeStack<Byte> rootStack=new TreeStack<Byte>();//堆栈记录路径
CodeTable codes=new CodeTable();
scanTree(rootNode,rootStack,codes);
return codes;
}
/**
* 遍历树的方法,递归
* @param root 节点
* @param rootStk 堆栈记录,存储路径
* @param map 编码表
*/
private void scanTree(TreeNode root,TreeStack<Byte> rootStk,CodeTable map){
if(root.getType()==1){
//根节点
TreeNode LNode=root.getLNode();
rootStk.add((byte)0);
//递归遍历左节点
scanTree(LNode,rootStk,map);
TreeNode RNode=root.getRNode();
rootStk.add((byte)1);
//递归遍历右节点
scanTree(RNode,rootStk,map);
}
if(root.getType()==2){
//叶节点
Object[] cd=rootStk.getCode();
Byte[] code=new Byte[cd.length];
int ind=0;
for(Object o:cd){
byte ob=(Byte)o;
code[ind]=ob;
ind++;
}
byte bt=root.getB();
CodeTableNode nd=new CodeTableNode(bt,code);
map.add(nd);
}
rootStk.pop();//栈回溯
}
⑤、每个取一个长度为八的字节数组,写入文件,若最后剩下的数组不足八位,则添加码表中没有的元素知道够8位。并写入码表
/**
* 获取文件重新编码,并直接写入文件
*
* @param file
* 文件对象
* @param map
* 编码表
* @throws Exception
*/
private void getFileCode(File file, OutputStream op, CodeTable map)
throws Exception {
java.io.InputStream in = new java.io.FileInputStream(file);
Byte[] nBt = new Byte[8];// 储存字节的每位,8位
int emptyN = 8;// 剩余的空位,候补
int btn = in.read();
while (btn != -1) {
byte bn = (byte) btn;
Byte[] code = map.get(bn);// 获取b对应的编码
for (int t = 0; t < code.length; t++) {// 插入值到bit位数组中
nBt[8 - emptyN] = code[t];
emptyN--;
// 写满一个字节后加入队列
if (emptyN == 0) {
byte nByte = toByte(nBt);
// 写入文件
System.out.println("写入byte:"+nByte);
byte[] bt = { nByte };
op.write(bt);
emptyN = 8;// 空位重置
nBt = new Byte[8];// 数组重置
}
}
btn = in.read();
}
in.close();
// 写完后检测是否写满最后一个字节,如未满则填充一个map中不存在的值
if (emptyN > 0) {
Byte[] addCode = new Byte[emptyN];
// 检测到可行的结尾填充
if (scanUsableAddons(map, addCode, 0)) {
for (byte bi : addCode) {
nBt[8 - emptyN] = bi;
emptyN--;
}
byte endB = toByte(nBt);
// 添加到重新编码区结尾
byte[] bt = { endB };
op.write(bt);
}
}
}
/**
* 将CodeMap编码表对象转换成byte数组 规则:
* 每条编码为3部分,第一部分占1个byte,储存编码的, 第二部分占一个byte,存储原byte值
* 第三部分为一个byte[]数组,储存对应的编码,每个二进制位占一个byte,只为0或1
*
* @param map
* 传入一个CodeMap对象
* @return 转换完成的byte数组
*/
private byte[] getMapByte(CodeTable map) {
List<Byte> allBy = new ArrayList<Byte>();
for (CodeTableNode mNode : map.getAllNodes()) {
// 取得map中MapNode对象
byte bt = mNode.getByte();// 取得byte值
Byte[] co = mNode.getCode();// 取得编码
Byte len = (byte) co.length;// 取得编码长度
allBy.add(len);
allBy.add(bt);
for (byte b : co) {// 取得编码中每个二进制位值传入队列
allBy.add(b);
}
}
// 转换队列为byte[]数组
byte[] abts = new byte[allBy.size()];
int index = 0;
for (byte b : allBy) {
abts[index] = b;
index++;
}
return abts;
}
private void compress() {
String src = srcField.getText();
String tar = tarField.getText();
File srcFile = new File(src);
File tarFile = new File(tar);
lab.setText("开始压缩 O(∩_∩)O");
//lab.setText("正在压缩。。。 →_→");
ReadFile sf = new ReadFile(srcFile);
CodeTable map = null;// 获取编码表
try {
//得到码表
map = sf.getMap();
byte[] maptoByte = getMapByte(map);
tarFile.createNewFile();// 创建文件
// 建立输出流
java.io.OutputStream out = new java.io.FileOutputStream(tarFile);
// 建立对象输出流
int mapLen = maptoByte.length;
java.io.DataOutputStream dou = new java.io.DataOutputStream(out);
dou.writeInt(mapLen);
dou.flush();
// 输出编码表
out.write(maptoByte);
// 写入转码后的文件数据
// lab.setText("正在压缩。。。 →_→");
getFileCode(srcFile, out, map);// 直接队文件转码并写入
out.flush();
out.close();
//lab.setText("压缩成功! (^o^)");
} catch (Exception e1) {
e1.printStackTrace();
}
}
二、解压
思路:从压缩的文件中读取字节+码表,将每个字节转化为一个八位的字符数组,逐一对比码表,若有相同的则根据码表转化为相应的字节,若没有一致的,则继续读取字节化为八位的字数数组,如此循环,知道读取完文件。然后将获取的字节写入文件,这样解压就算完成了。其实解压是压缩的逆过程,关键就是写入码表与获取码表,这是压缩与解压万和城呢过的关键。
/**
* 解压方法,先获取编码表,再对文件进行解码
*
* @throws Exception
*/
private void inCompress() throws Exception {
File srcF = new File(srcField.getText());
File tarF = new File(tarField.getText());
// lab.setText("开始解压 O(∩_∩)O");
lab.setText("正在解压。。。 →_→");
tarF.createNewFile();
java.io.InputStream inStr = new java.io.FileInputStream(srcF);
java.io.DataInputStream daIns = new java.io.DataInputStream(inStr);
java.io.OutputStream ouStr = new java.io.FileOutputStream(tarF);
// 取得编码表的长度
int byteCount = daIns.readInt();
int readByte = inStr.read();
byteCount--;
CodeTable getMap = new CodeTable();// 编码表对象,备用
while (byteCount > 0) {
int readCount = readByte;// 读取时的计数变量,以确定当前读取的数据类型
// 获取本条编码的长度为readCount
Byte[] byteCode = new Byte[readCount];
readByte = inStr.read();
byteCount--;// 读取了一个字节,计数器减一
if (readByte == -1) {// 异常文件结尾,抛出
throw new Exception("Unexcepted End");
}
byte bt = (byte) readByte;// 当前编码对应的字节
for (int i = 0; i < readCount; i++) {
readByte = inStr.read();
byteCount--;// 读取了一个字节,计数器减一
if (readByte == -1) {// 异常文件结尾,抛出
throw new Exception("Unexcepted End");
}
byte tb = (byte) readByte;
byteCode[i] = tb;
}
// 一条编码读取完毕,建立一个编码表节点对象并导入了
CodeTableNode getNode = new CodeTableNode(bt, byteCode);
getMap.add(getNode);
readByte = inStr.read();
byteCount--;// 读取下一个数据
}
// lab.setText("正在解压。。。 →_→");
// 编码表建立完毕,开始读取文件正文并转码
// 转码方法:每次读取一位,在编码表中检索,如果存在则转码,不存在则增加一位并重新检测
List<Byte> bs = new java.util.ArrayList<Byte>();// 存储编码的byte队列
while (readByte != -1) {
byte b = (byte) readByte;
byte[] bArray = toByteArray(b);// 将字节每位提出转成数组
for (byte bn : bArray) {
bs.add(bn);
byte[] nNode = checkMapCode(getMap, bs);
if (nNode != null) {
ouStr.write(nNode);
ouStr.flush();
bs.clear();// 清空队列
}
if (bs.size() > getMap.getAllNodes().size()) {
// 队列长度已经大于Map中的编码条数
throw new Exception("Erro File,Not Found Code");
}
}
readByte = inStr.read();// 继续读取
}
// lab.setText("解压成功! (^o^)");
// 关闭输入输出流
inStr.close();
ouStr.close();
}
/**
* 递归检索编码表map中无对应值的结尾二进制位作为结尾
*
* @param map
* 编码表
* @param bits
* 剩余的二进制位
* @param index
* 目前的指向
* @return 找到了则返回true,检索完毕都未找到则返回false
*/
private boolean scanUsableAddons(CodeTable map, Byte[] bits, int index) {
if (index != bits.length - 1) {
bits[index] = 0;
if (scanUsableAddons(map, bits, index + 1)) {
return true;
}
bits[index] = 1;
if (scanUsableAddons(map, bits, index + 1)) {
return true;
}
}
if (index == bits.length - 1) {
bits[index] = 0;
if (map.get(bits) == null) {
return true;
}
bits[index] = 1;
if (map.get(bits) == null) {
return true;
}
}
return false;
}
三、感想
做完哈夫曼压缩后,感想最大的就是速度,速度太慢,就像蜗牛爬一样慢,相对于自己用地360压缩是简直没得比,压缩个7M多点的要16分钟,解压更是成倍增长。还有就是对于一些doc格式、ppt格式的等,压缩比也不大高,可能是文件大小的原因。
界面图:
源文件、压缩后、解压后的对比图: