Java哈夫曼二叉树之数据压缩,解压

压缩原理:将原文件的字符的种类,出现的次数为权值,构造哈夫曼二叉树,再根据二叉树给各个字母编码,再根据编码把源文件的字符转成字节数据
解压原理:将原文件压缩过程中的编码表,写入压缩文件中。解压过程中,先读取编码表,再读取压缩的字符的字节数据,根据编码表还原字符数据,生成新的文件。


压缩的过程
哈曼二叉树 给一个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字节以上压缩的过程特别慢,而且不能读取中文字。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值