霍夫曼编码实现文件的压缩、解压

/**
霍夫曼编码算法描述:

  • 1、第一件事情就是将每个node存入到list中,后续进行排序、建树等等操作,但现在node的构造方法里面需要符号以及权值,如何做到?
    首先应该根据输入的字符串,统计每一个byte出现的次数(权值),存入到map中!(每个byte是key,权值是value)
    上面这个思想还是很重要的,做完之后遍历map(去复习),循环将node new出来,并存到list中

  • 2、通过第一步的nodes ,构建二叉树。
    第一步:排序; 第二步:取最小节点组合为新节点; 第三步:删除旧节点,挂上新节点 第四步:循环

  • 3、将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合。
    叶子节点是需要编码的,非叶子是合成的不需要编码。叶子节点判断方法:node.getData ?= null
    如果是非叶子节点,就向左递归、向右递归。如果是叶子节点。马上存入到map中

  • 4、编写一个方法,将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte[] (当做一个小算法好好练练)
    也就是说,原来的byte数组就是字符串对应转来的那个数组。
    现在得到的数组是每个字符进行编码后(由于每个字符编码后是二进制一串数字很长,所以每8位取做一个),每八位取做一个byte 存入数组中
    方法:

    • 1、先将每个字符串对应的byte数组遍历,并通过map得到每一个byte的哈夫曼编码,用stringBuilder将所有byte对应的编码拼在一起
    • 2、拼在一起之后长度肯定是比较长,现在每八位作为一个byte将其取出,放到一个新的byte[]中(确定出数组的长度),其中需要注意很多小细节。
    • 3、注意StringBuilder组装后的编码是字符串格式,如何每八位操作为一个byte?
    • —用subString取出后,先转为int 再转为byte (byte)Integer.parseInt(strByte, 2);
  • 5、解码第一步:将每个byte转为一个二进制的字符串,这里有很多要注意的点,涉及二进制。

  • 第一,先思考这个编码后的byte[]是怎么来的?
    首先字符每个元素串转为byte[];之后将byte[]每个元素按照霍夫曼编码;
    再后是 每8个二进制01,(以补码的形式)转为一个十进制数,存在byte[]: (byte) Integer.parseInt(temp,2);
    所以现在目标是把byte[]中每一个元素(十进制形式)转为对应的二进制

  • 第二:问题是,Java中直接转Integer.toBinaryString(十进制值)得到的是二进制补码(补码是原码(符号位不动)取反+1)
    另外一个问题是,int的补码和byte的补码的关系,只取int补码的后八位就是byte的补码
  • 第三:第三个问题,比如正数2 Integer.toBinaryString(2)返回的是 10 其他位都没有,怎么办呢?
    方法是和256(第九位是1,其他是0)进行按位或运算,这样其他位就或上了。最后只取8位就是该正数的8位数补码了(正数原码反码补码都一样)
  • 第四:第四个问题,如果是最后一位,可能不满8位,那就直接将它对应的补码输出就好了,不需要补够8位。
  • 6、写一个解码的大方法
  • 第一步:通过上面第五步得到了每一个字符对应的二进制字符串,现在批量进行拼接,把整个byte数组考虑进去。需要考虑最后一个字节,无需补高位。
  • 第二步:(这一块算法很好,多练练)把字符串按照指定的赫夫曼编码进行解码,注意需要把map的key和value倒装。
  • 如果遇到了对应的key,就转为value,放到数组中取。否则继续取下一位
  • 7、对文件进行压缩
  • 第一步:先创建文件的输入输出流。使用文件输入流.available()获取文件大小,并以该大小创建数组,用于后续将该文件读到内存中.read(数组),
  • 第二步:直接对该文件进行压缩,之前封装好了的方法huffmanZip(b) 只需要传入字符串对应的byte[]就行
  • 第三步:创建输出流,目的是为了存放压缩文件,再创建一个对象流,和输出流进行流的关联嵌套。需要输出压缩表(那个map)以及压缩后的文件
  • 8、对文件进行解压
  • 第一步:先定义文件输入流嵌入进对象输入流。调用对象输入流的readObject()方法,调一次,输出第一次的写入内容,调两次,输出第二次的…
  • 第二步:decode(huffmanCodes, huffmanBytes)解压
  • 第三步:使用输出流,直接write(bytes)就行
    */
package huffmanCode;

import java.io.*;
import java.util.*;
public class HuffmanCode {
    public static void main(String[] args) {
        String s = "i like like like java do you like java";
        byte[] bytes = s.getBytes();//字符串转换为数组
        /*List<Node02> nodes = getNodes(bytes);
        Node02 node02 = huffmanTree(nodes);
        //preOrder(node02);

        System.out.println("------------------");
        Map<Byte, String> codes = getCodes(node02);
        //System.out.println(codes);

        byte[] zip = zip(bytes, codes);
        for (int i = 0; i < zip.length; i++) {
            System.out.print(zip[i] + " ");
        }
        System.out.println();
        System.out.println(zip.length);
        //将以上步骤封装到一个方法中*/

     /*   byte[] bytes1 = huffmanZip(bytes);
        System.out.println(Arrays.toString(bytes1));
        System.out.println("--------------------");

        byte[] decode = decode(huffmanCodes, bytes1);
        //System.out.println(Arrays.toString(decode));
        System.out.println("原来的字符串=" + new String(decode));*/


        String src = "D:\\360Downloads\\wpcache\\360wallpaper.jpg";
        String dst = "D:\\360Downloads\\wpcache\\360wallpaper.zip";
        String dst1 = "D:\\360Downloads\\wpcache\\unzip1.jpg";
        zipFile(src,dst);
        System.out.println("压缩成功!");

        unZipFile(dst,dst1);
        System.out.println("解压成功");
    }

    /**
     * 1、将每个node存入到list中,用于后续进行排序、建树等等操作
     *
     * @param bytes 接收字节数组
     * @return 返回list结合,里面封装好了每个node信息(此时还没挂上左右节点,要在建树的时候挂上)
     */
    public static List<Node02> getNodes(byte[] bytes) {

        Map<Byte, Integer> map = new HashMap<>();
        for (byte data : bytes) {
            if (map.get(data) == null) { //通过key获取value,value是权值
                map.put(data, 1);
            } else {
                map.put(data, map.get(data) + 1);//如果再次遍历到同样的字符,统计次数+1
            }
        }
        //map构建完成后,以及统计好了每个字符对应的权值(出现次数),现在应该遍历该map,去new出对应的节点 ,并存到list中
        List<Node02> node02List = new ArrayList<>();
        //遍历去复习
        for (Map.Entry<Byte, Integer> entry : map.entrySet()) {
            node02List.add(new Node02(entry.getKey(), entry.getValue()));
        }
        return node02List;

    }

    /**
     * 2、构建HuffmanTree
     *
     * @param node02s
     * @return
     */
    public static Node02 huffmanTree(List<Node02> node02s) {
        while (node02s.size() > 1) {
            //第一步,排序
            Collections.sort(node02s);
            //
            Node02 leftNode = node02s.get(0);
            Node02 rightNode = node02s.get(1);
            //注意组合成的中间节点,值是null
            Node02 parent = new Node02(null, leftNode.getWeight() + rightNode.getWeight());
            parent.setLeft(leftNode);
            parent.setRight(rightNode);
            //
            node02s.add(parent);
            //
            node02s.remove(leftNode);
            node02s.remove(rightNode);
        }
        return node02s.get(0);
    }

    /**
     * 遍历
     */
    public static void preOrder(Node02 root) {
        if (root != null) {
            root.preOrder();
        }
    }

    //全局变量?
    static StringBuilder stringBuilder = new StringBuilder();
    static Map<Byte, String> huffmanCodes = new HashMap<>();

    /**
     * 3、将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合
     *
     * @param node02        传入的节点
     * @param code          路径: 左子结点是 0, 右子结点 1
     * @param stringBuilder 该节点的value对应的哈夫曼编码
     */
    public static void getCodes(Node02 node02, String code, StringBuilder stringBuilder) {
        StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);
        stringBuilder1.append(code);

        if (node02 != null) {
            //去判断是叶子还是非叶子
            if (node02.getValue() == null) {
                //说明不是叶子节点

                //向左遍历
                getCodes(node02.getLeft(), "0", stringBuilder1);
                //向右遍历
                getCodes(node02.getRight(), "1", stringBuilder1);
            } else {
                //到了叶子节点,应该把当前的stringBuilder放入到map中
                huffmanCodes.put(node02.getValue(), stringBuilder1.toString());
            }
        }
    }

    /**
     * 3、对上面的方法进行重载
     * @param node02
     * @return
     */
    public static Map<Byte, String> getCodes(Node02 node02) {
        if (node02 == null) {
            return null;
        }
        //向左、向右遍历 并传入全局变量。
        getCodes(node02,"0",stringBuilder);
        getCodes(node02,"1",stringBuilder);

        //变量完成后,全局变量map已经被改变了,直接返回
        return huffmanCodes;
    }

    /**
     *  4、编写一个方法,将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte[]
     * @param bytes 这时原始的字符串对应的 byte[]
     * @param huffmanCodes 生成的赫夫曼编码map
     * @return 返回赫夫曼编码处理后的 byte[]
     * 举例: String content = "i like like like java do you like a java"; =》 byte[] contentBytes = content.getBytes();
     * 返回的是 字符串 "1101111110111101101010011111101111011010100111111011110110101001111111000100011101110001111111000110011111111010110011110011111101111011010100111111100010001110111000"
     * => 对应的 byte[] huffmanCodeBytes  ,即 8位对应一个 byte,放入到 huffmanCodeBytes
     * huffmanCodeBytes[0] =  10101000(补码) => byte  [推导  10101000=> 10101000 - 1 => 10100111(反码)=> 11011000= -88 ]
     * huffmanCodeBytes[1] = -88
     */
    private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
        //先得到byte数组(输入)对应的哈夫曼编码,存入到stringBuilder
        StringBuilder stringBuilder2 = new StringBuilder();
        for(byte b : bytes){
            stringBuilder2.append(huffmanCodes.get(b));//huffmanCodes是map,通过key获取value
        }
        //System.out.println(stringBuilder2);//测试

        //第二步得出数组大小
        int length;
        if (stringBuilder2.length() / 8 == 0) {
            length = stringBuilder2.length() / 8;
        } else {
            length = stringBuilder2.length() / 8 + 1;
        }

        //第三步,根据数组大小,new出压缩后的数组,并将字符串转换过去
        byte[] zipBytes = new byte[length];
        int index = 0;
        for (int i = 0; i < stringBuilder2.length(); i+=8) {
            String temp;
            if (i+8 > stringBuilder2.length()){
                //长度不够了,直接从当前位置开始取到最后就行了。
                temp = stringBuilder2.substring(i);
            }else {
                temp = stringBuilder2.substring(i,i+8);
            }
            zipBytes[index] = (byte) Integer.parseInt(temp,2);
            index++;
        }

        return zipBytes;
    }

    /**
     * 1234、将前面四步进行封装,只调用当前方法即可
     * @param bytes 将字符串 语句 转为 byte数组 后的原始数据
     * @return 返回一个压缩后的byte数组
     */
    public static byte[] huffmanZip(byte[] bytes){
        //1、将每个node存入到list中,用于后续进行排序、建树等等操作
        List<Node02> node02s = getNodes(bytes);
        //2、构建HuffmanTree
        Node02 node02 = huffmanTree(node02s);
        //3、将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合
        Map<Byte, String> codes = getCodes(node02);
        //4、将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte[]
        byte[] zip = zip(bytes, codes);

        return zip;
    }

    /**
     * 5、将一个byte 转成一个二进制的字符串, 如果看不懂,可以参考我讲的Java基础 二进制的原码,反码,补码
     * @param b 传入的 byte
     * @param flag 标志是否需要补高位如果是true ,表示需要补高位,如果是false表示不补, 如果是最后一个字节,无需补高位
     * @return 是该b 对应的二进制的字符串,(注意是按补码返回)
     */
    private static String byteToBitString(boolean flag, byte b) {
        //使用变量保存 b
        int temp = b; //将 b 转成 int,int才有toBinaryString()方法
        //如果是正数我们还存在补高位
        if(flag) {
            temp |= 256; //按位与 256  1 0000 0000  | 0000 0001 => 1 0000 0001
        }
        String str = Integer.toBinaryString(temp); //返回的是temp对应的二进制的补码
        if(flag) {
            return str.substring(str.length() - 8);//只取8位
        } else {
            return str;
        }
    }


    /**
     *   6、编写一个方法,完成对压缩数据的解码
     * @param huffmanCodes 赫夫曼编码表 map
     * @param huffmanBytes 赫夫曼编码得到的字节数组
     * @return 就是原来的字符串对应的数组
     */
    public static byte[] decode(Map<Byte,String> huffmanCodes,byte[] huffmanBytes){
        StringBuilder stringBuilder1 = new StringBuilder();
        //得到二进制字符串。
        for (int i = 0; i < huffmanBytes.length; i++) {
            byte b = huffmanBytes[i];

            boolean flag = !(i == huffmanBytes.length - 1);//最后一位设为false
            stringBuilder1.append(byteToBitString(flag, b));
        }
        //得到倒装的map
        Map<String, Byte> inverseHuffmanCodes = new HashMap<>();
        for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
            inverseHuffmanCodes.put(entry.getValue(), entry.getKey());
        }
        //根据解码表,将二进制字符串转为最原始输入的字符串!

        //创建要给集合,存放byte
        List<Byte> list = new ArrayList<>();

      /*  int i = 0;
        while (i < stringBuilder1.length()) {//注意!这里stringBuilder才是二进制字符串!
            //练习算法!设置一个flag的思想不是一开始就有的,是写的过程中需要,想到加入的
            int count = 1;
            Byte value = 0;
            boolean flag = true;
            while (flag) {
                String key = stringBuilder1.substring(i, i + count);
                value = inverseHuffmanCodes.get(key);
                if (value == null){
                    count++;
                }else {
                    flag=false;
                }
            }
            i += count;
            list.add(value);
        }*/
        for(int  i = 0; i < stringBuilder1.length(); ) {
            int count = 1; // 小的计数器
            boolean flag = true;
            Byte b = null;

            while(flag) {
                //1010100010111...
                //递增的取出 key 1
                String key = stringBuilder1.substring(i, i+count);//i 不动,让count移动,指定匹配到一个字符
                b = inverseHuffmanCodes.get(key);
                if(b == null) {//说明没有匹配到
                    count++;
                }else {
                    //匹配到
                    flag = false;
                }
            }
            list.add(b);
            i += count;//i 直接移动到 count
        }

        //当for循环结束后,我们list中就存放了所有的字符  "i like like like java do you like a java"对应的 byte[] 数组。string.getBytes()方法拿到的
        //把list 中的数据放入到byte[] 并返回
        byte b[] = new byte[list.size()];
        for(int j = 0;j < b.length; j++) {
            b[j] = list.get(j);
        }
        return b;
        //new String(上面的数组),就能将 byte数组 转回为原来的 "i like like like java do you like a java" string语句
    }

    /**
     *  7、对文件进行压缩
     * @param srcFile
     * @param dstFile
     */
    public static void zipFile(String srcFile,String dstFile){
        FileInputStream fis = null;
        OutputStream fos = null;
        ObjectOutputStream oos = null;
        byte[] stringBytes = null;
        byte[] huffmanZipBytes = null;

        try {
            fis = new FileInputStream(srcFile);
            stringBytes = new byte[fis.available()];

            fis.read(stringBytes);
            //读完了之后应该开始压缩
            huffmanZipBytes = huffmanZip(stringBytes);
            //压缩完毕之后想着输出
            fos = new FileOutputStream(dstFile);
            oos = new ObjectOutputStream(fos);
            //写进去
            oos.writeObject(huffmanZipBytes);
            //还有map对应的哈夫曼编码表别忘了写,否则无法解压
            oos.writeObject(huffmanCodes);

        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            try {
                fis.close();
                oos.close();
                fos.close();

            } catch (IOException e) {
                //throw new RuntimeException(e);
                System.out.println(e.getMessage());
            }
        }
    }

    /**
     * 8、文件解压
     * @param zipFile 准备解压的文件
     * @param dstFile 将文件解压到哪个路径
     */
    public static void unZipFile(String zipFile,String dstFile){
        InputStream is = null;
        ObjectInputStream ois = null;
        OutputStream fos = null;

        try {
            is = new FileInputStream(zipFile);
            //完成流的嵌套
            ois = new ObjectInputStream(is);
            //读出之前存的压缩后的数据
            byte[] huffmanBytes = (byte[]) ois.readObject();
            //读出编码表
            Map<Byte,String> huffmanCodes = (Map<Byte, String>) ois.readObject();
            //进行解压
            byte[] decodedBytes = decode(huffmanCodes, huffmanBytes);
            //输出
            fos = new FileOutputStream(dstFile);
            fos.write(decodedBytes);


        } catch (Exception e) {
            System.out.println(e.getMessage());
        }finally {
            try {
                if (fos!=null){
                    fos.close();
                }

                ois.close();
                is.close();
            } catch (IOException e2) {
                //throw new RuntimeException(e);
                System.out.println(e2.getMessage());
            }
        }
    }

}


class Node02 implements Comparable<Node02> {

    private Byte value;//每个符号,以ASCII形式存放
    private Integer weight;//权值,即为每个符号出现的次数
    private Node02 right;
    private Node02 left;

    @Override//left和right不要写出来,方便到时候 遍历二叉树 主要输出当前节点的信息就好了
    public String toString() {
        return "Node02{" +
                "value=" + value +
                ", weight=" + weight +
                '}';
    }

    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }

    public Byte getValue() {
        return value;
    }

    public void setValue(Byte value) {
        this.value = value;
    }

    public Integer getWeight() {
        return weight;
    }

    public void setWeight(Integer weight) {
        this.weight = weight;
    }

    public Node02 getRight() {
        return right;
    }

    public void setRight(Node02 right) {
        this.right = right;
    }

    public Node02 getLeft() {
        return left;
    }

    public void setLeft(Node02 left) {
        this.left = left;
    }

    public Node02(Byte value, Integer weight) {
        this.value = value;
        this.weight = weight;
    }


    @Override
    public int compareTo(Node02 o) {
        return this.weight - o.weight;
    }
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值