压缩原理:将原文件的字符的种类,出现的次数为权值,构造哈夫曼二叉树,再根据二叉树给各个字母编码,再根据编码把源文件的字符转成字节数据
解压原理:将原文件压缩过程中的编码表,写入压缩文件中。解压过程中,先读取编码表,再读取压缩的字符的字节数据,根据编码表还原字符数据,生成新的文件。
压缩的过程
哈曼二叉树 给一个abbccddeee
这样我们就能得到每个字母所对应的编码。再将编码保存起来
将我们要压缩的源文件所有的字母按照编码表变成0/1串,这里abbccddeee就变成了10010110100000101111111
将这个编码串8个8个取出来,最后多的补0;并且定义一个变量保存补0的个数;把8个01串当作一个二进制数,把它转成十进制,就能得到一个整数。把这个整数写入压缩出来的文件,假设一个8位01串代表3个字母,那么文件就从3个字母(6个byte)变成1个byte,这样文件的大小就压缩了;
解压的过程
解压需要用到编码表,所以压缩文件中应该先写入压缩表,再写入编码串,再写入补0的个数
下图是压缩文件中应该存储的东西
根据我们各种存储的长度,就能得到我们应该怎样区分读出来的内容是什么,什么时候开始读要翻译的01串
再根据编码表翻译01串,把翻译结果输出到解压后的文件。解压过程就结束啦
代码部分
public class Manage2 {
private static String[] ss = new String[8];//声明一个8为字符串数组
private static String jieguo = "";//声明编码结果
private static String[] bian = new String[128];// 声明一个编码数组保存对应字母的对应编码
public static void main(String[] args) throws IOException {
yasuo();
System.out.println("压缩成功");
jieya();
System.out.println("解压成功");
}
/**
* 压缩
*/
public static void yasuo() throws IOException {
// 创建文件输入流
FileInputStream fis = new FileInputStream("C:\\Users\\Administrator\\Desktop\\新建文本文档.txt");
// 创建缓存区域
byte buffer[] = new byte[fis.available()];
// 读入所有的文件字节
fis.read(buffer);
// 对字节进行处理
String str = new String(buffer);
// System.out.println(str);
// 关闭流
fis.close();
// 对读取的字符进行编码,转换成int型整数
// 创建树的对象
HalfmenTree ht = new HalfmenTree();
ht.add(str);
System.out.println("建树完毕");
// 写入文件
// 创建文件输出流
OutputStream os = new FileOutputStream("C:\\Users\\Administrator\\Desktop\\123456.txt");
// 写入子叶节点和对应的编码
// 写入根节点data的字符串的长度
int a = ht.root.getData().toString().length();
os.write(a);
// 写入每个字符和对应编码
for (int i = 0; i < ht.bianma.length; i++) {
if (ht.bianma[i] != null) {
// 写入编码的长度
os.write(ht.bianma[i].length());
os.write(i);// 写入字母的ascll码
// System.out.println("写入的编码长度为" + ht.bianma[i].length() + "++");
// System.out.println("写入字母的ascll码是" + i);
for (int j = 0; j < ht.bianma[i].length(); j++) {
char ch = ht.bianma[i].charAt(j);
int x = ch - 48;
// System.out.println("写入单个0/1码::" + x);
os.write(x);// 写入编码的单个0/1字符
}
}
}
// 写入得到的整数
// 测试输出得到的整数
// System.out.println();
// System.out.println();
// System.out.println("得到的整型数据:======队列的大小:" + ht.intlist.size());
for (int i = 0; i < ht.intlist.size(); i++) {
int x = (int) ht.intlist.get(i);
System.out.println("得到的整型数为" + x);
os.write(x);
}
// 写入余数:
os.write(ht.yushu);
// 关闭流
os.close();
}
/**
* 解压文件
*
* @throws IOException
*/
public static void jieya() throws IOException {
// 读文件
FileInputStream fis = new FileInputStream("C:\\Users\\Administrator\\Desktop\\123456.txt");
byte buffer[] = new byte[fis.available()];
// 读入文件所有的字节
fis.read(buffer);
fis.close();
// 首先读第一个,字母的个数
// System.out.println("有" + buffer[0] + "种字母");
int count = buffer[0];
// 再读下一个字母
int ii = 1;// 编码长度字节的下标
int zimu = 0;
/**
* 读编码表
*/
for (int i = 0; i < count; i++) {
int m = buffer[ii];// 读出编码长度
zimu = buffer[ii + 1];// 读出编码代表的字母
bian[zimu] = "";
for (int t = 0; t < m; t++) {// 读出接下来的0/1编码,并放入编码数组
bian[zimu] += buffer[ii + t + 2];
}
System.out.println("编码数组第" + zimu + "为" + bian[zimu]);
ii += m + 2;// 赋给下一个编码长度节点
}
// 得到整数,再转成二进制字符串
for (int i = 0; i < buffer.length - 1; i++) {
if (i > ii - 1) {// 只读编码部分
if (buffer[i] < 0) {
erjinzhi(buffer[i] + 256);
// System.out.println(buffer[i] + 256);
} else {
erjinzhi(buffer[i]);
// System.out.println(buffer[i]);
}
}
}
// System.out.println("---------转成二进制---------");
// 把的到的二进制串在一起
// System.out.println("解压后的编码为" + jieguo);
// 把补0的个数读出来
int x = 8 - buffer[buffer.length - 1];
// 二进制串删除补0
String sss = "";//中间变量,只取它的删除补0后的部分
for (int i = 0; i < jieguo.length() - x; i++) {
sss += jieguo.charAt(i);
}
jieguo = sss;//把中间变量 的值再赋给jieguo
// System.out.println("删除补0的编码" + jieguo);
/*
* 将得到的编码转成字母写入新文件
* 0/1串一个一个找。把值赋给s,找不到,s加上下一位继续找,直到找到位置,再清空s
*/
// 创建文件输入流
OutputStream os1 = new FileOutputStream("C:\\Users\\Administrator\\Desktop\\jieya.txt");
String s = "";
for (int i = 0; i < jieguo.length(); i++) {
char ch = jieguo.charAt(i);
s += ch;
// System.out.println(s+"--");
for (int j = 0; j < bian.length; j++) {
if (s.equals(bian[j])) {// 如果编码能翻译出结果
s = "";
char ch1 = (char) j;// 把对应的下标转成ascll码代表的字母
// System.out.print(ch1);// 输出判断字母是否正确
// 写入文件
String ss = ch1 + "";
os1.write(ss.getBytes("gbk"));
}
}
}
os1.close();
}
/**
* 把的到的数字转成2进制
*/
private static void erjinzhi(int num) {
int n = 7;
while (num > 0) {
int x = num % 2;// 取余
String str = x + "";
ss[n] = str;// 从第8位开始倒序存入
n--;
num = num / 2;// 整除
}
if (n >= 0) {
for (int i = 0; i < n + 1; i++) {
ss[i] = 0 + "";
}
}
for (int i = 0; i < ss.length; i++) {
// System.out.print(ss[i]);
jieguo += ss[i];
}
// System.out.println();
}
}
构造哈夫曼二叉树的类:
public class HalfmenTree<E> {
public Node root;
private int[] num = new int[128];// 权值数组
public String[] bianma = new String[128];// 编码数组
private String jieguo = "";// 编码结果
public byte yushu;
// 声明一个队列存字符的Ascll码对象
private ArrayList<Object> numlist = new ArrayList<Object>();
// 声明一个队列存贮节点
private ArrayList<Node> list = new ArrayList<Node>();
//声明一个队列存放01串转成整型的数据
public ArrayList intlist = new ArrayList<>();
// 根据字母的权值排序从小到大(冒泡排序)
public void paixu() {
for (int i = 0; i < list.size(); i++) {
for (int j = i + 1; j < list.size(); j++) {
if (list.get(i).getWeight() > list.get(j).getWeight()) {
Node node = list.get(i);
list.set(i, list.get(j));
list.set(j, node);
}
}
}
}
public void add(String str) {
// 将str拆成单个字符,并根据其对应的Ascll码存放在num权值数组中
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
int n = (int) ch;
num[n]++;
if (num[n] == 1) {// 当每个字母第一次出现时,将这个字母的Ascll码存进numlist
numlist.add(n);
}
}
// 生成节点
for (int i = 0; i < numlist.size(); i++) {
int n = (int) numlist.get(i);
int m = num[n];
char ch = (char) n;
Node node = new Node(ch, m);
list.add(node);// 存入节点列表中
}
// 根据字母的权值排序从小到大(冒泡排序)
paixu();
// 排序后输出节点的字母和权值
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i).getData() + " " + list.get(i).getWeight());
}
System.out.println("=======================");
// 取出权值最小的两个节点,构建一个新节点
while (list.size() > 1) {
Node left = list.remove(0);
Node right = list.remove(0);
String st = left.getData().toString() + right.getData().toString();// st为左右子节点的字母并在一起
Node father = new Node(st, left.getWeight() + right.getWeight());// 新建节点,权值相加
father.setLeft(left);// 设置新节点的左子节点
father.setRight(right);// 设置新节点的右子节点
list.add(0, father);// 将新节点放入节点列表中
root = father;// 让根节点为头结点
paixu();// 排序
}
// 递归遍历,得到字母对应的编码
huofuman();
// 将字符串转成编码
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
int n = ch;
jieguo += bianma[n];
}
// System.out.println();
// System.out.println();
System.out.println("编码为:" + jieguo);
// 把编码转换成字节数据
System.out.println();
String bytes = "";// 记录8位字节
String tranString = "";// 保存所转换的所有字符串
int count = jieguo.length() / 8;// 有count个8位
yushu =(byte) (jieguo.length() % 8);
// System.out.println("余数为:" +yushu);
for (int i = 0; i < count; i++) {
for (int j = 0; j < 8; j++) {
char ch = jieguo.charAt(j);
bytes += ch;
}
// System.out.println(bytes);
//将一个8位字符串转成一个整数:
int intw = changeString(bytes);
intlist.add(intw);
// System.out.println(intw);
// 删除jieguo的前八位
for (int r = 8; r < jieguo.length(); r++) {
tranString += jieguo.charAt(r);
}
jieguo = tranString;
tranString = "";
bytes = "";
}
// 最后不满足8位的
if (jieguo.length() != 0) {
int x = 8 - jieguo.length();
for (int i = 0; i < jieguo.length(); i++) {
char ch = jieguo.charAt(i);
bytes += ch;
}
for (int j = 0; j < x; j++) {
bytes += 0;
}
// System.out.println(bytes);
int intw = changeString(bytes);
intlist.add(intw);
}
// System.out.println("执行--------");
// System.out.println(intw);
}
/**
* 递归遍历 根据树给对应的字母赋霍夫曼编码 左为0,右为1
*/
public void huofuman() {
huofuman(root);
}
public void huofuman(Node node) {
// 后序遍历
if (node.getLeft() != null) {
node.getLeft().setSt(node.getSt() + 0);
huofuman(node.getLeft());
if (node.getRight() != null) {
node.getRight().setSt(node.getSt() + 1);
huofuman(node.getRight());
}
}
System.out.println(node.getData() + " " + node.getWeight() + " 编码" + node.getSt());
if (node.getData().toString().length() < 2) {// 如果只有一个字符(排除多字符节点)
char ch = (char) node.getData();
int n = ch;
bianma[n] = node.getSt();// 在编码数组中存入编码
}
}
//二进制转十进制
public int changeString(String s) {
return (((int) s.charAt(0) - 48) * 128 + ((int) s.charAt(1) - 48) * 64 + ((int) s.charAt(2) - 48) * 32
+ ((int) s.charAt(3) - 48) * 16 + ((int) s.charAt(4) - 48) * 8 + ((int) s.charAt(5) - 48) * 4
+ ((int) s.charAt(6) - 48) * 2 + (int) s.charAt(7) - 48);
}
}
节点类和上一篇哈夫曼 二叉树是一样的。这里就不写了
总结
递归的使用可以减少代码量。
解压后的文件大概是源文件的1/2大小。但系统的压缩文件是大约1/7大小。而且文件有1w字节以上压缩的过程特别慢,而且不能读取中文字。