Java数据结构与算法-树结构实际应用Ⅱ(赫夫曼编码、解码,压缩、解压文件)[day09]

树结构实际应用


赫夫曼编码

赫夫曼编(霍夫曼、哈夫曼)码基本介绍

1.赫夫曼编码也翻译为 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法
2.赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。
3.赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间
4.赫夫曼码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,称之为最佳编码

赫夫曼编码的原理图解
变长编码的举例说明

1. 通信领域中信息的处理方式1-定长编码

i like like like java do you like a java // 共40个字符(包括空格) 105
32 108 105 107 101 32 108 105 107 101 32 108 105 107 101 32 106 97 118
97 32 100 111 32 121 111 117 32 108 105 107 101 32 97 32 106 97 118 97
//对应Ascii码
01101001 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101010 01100001 01110110 0110000100100000 01100100 01101111 00100000 01111001 01101111 01110101 00100000 01101100 01101001 01101011 01100101 00100000 01100001 00100000 01101010 01100001 01110110 01100001
//对应的二进制 按照二进制来传递信息,总的长度是359

2.通信领域中信息的处理方式2-变长编码
  • i like like like java do you like a java // 共40个字符(包括空格)

  • d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数0= , 1=a, 10=i, 11=e, 100=k, 101=l, 110=o, 111=v, 1000=j, 1001=u,1010=y, 1011=d

  • 说明:按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小,比如 空格出现了9 次, 编码为0 ,其它依次类推.

  • 按照上面给各个字符规定的编码,则我们在传输 “i like like like java do you like a java” 数据时,编码就是 10010110100…

  • 字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码(这个在赫夫曼编码中,还要进行举例说明,看不懂,不着急)

赫夫曼编码的原理图解

要传输的字符串:

i like like like java do you like a java

统计各个字符:d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9
// 各个字符对应的个数

按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值

构成赫夫曼树的步骤:

  1. 从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树

  2. 取出根节点权值最小的两颗二叉树

  3. 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和

  4. 再将这颗新的二叉树,以根节点的权值大小再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树

在这里插入图片描述

  1. 根据赫夫曼树,给各个字符,规定编码 (前缀编码), 向左的路径为0 向右的路径为1 , 编码如下: o: 1000 u:
    10010 d: 100110 y: 100111 i: 101 a : 110 k: 1110 e: 1111
    j: 0000 v: 0001 l: 001 : 01

  2. 按照上面的赫夫曼编码,我们的"i like like like java do you like a java"
    字符串对应的编码为 (注意这里我们使用的无损压缩)
    1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110 通过赫夫曼编码处理 长度为 133

  1. 长度为 : 133 说明: 原来长度是 359 , 压缩了 (359-133) / 359 = 62.9% 此编码满足前缀编码,
    即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性 赫夫曼编码是无损处理方案
  • 注意事项
    这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl 是一样的,都是最小的, 比如: 如果我们让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树为:
    在这里插入图片描述

数据压缩-创建赫夫曼树思路及实现

将给出的一段文本,比如 “i like like like java do you like a java” ,
根据前面叙述的赫夫曼编码原理,对其进行数据压缩处理 ,形式如:
“1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110”

步骤1:根据赫夫曼编码压缩数据的原理,需要创建 “i like like like java do you like a java” 对应的赫夫曼树.

具体思路:

  • 创建Node { data (存放数据), weight (权值), left 和 right }

  • 得到 “i like like like java do you like a java” 对应的 byte[] 数组

  • 编写一个方法,将准备构建赫夫曼树的Node 节点放到 List , 形式 [Node[date=97 ,weight = 5], Node[date=32,weight = 9]…], 体现 d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9

  • 可以通过List 创建对应的赫夫曼树

代码实现:
public class HuffmanCode {
    public static void main(String[] args) {
        String content = "i like like like java do you like a java";
        byte[] contentBytes = content.getBytes();
        //System.out.println(contentBytes.length);// 40

        List<Node> nodes = getNodes(contentBytes);
        System.out.println("nodes=" + nodes);

        //测试创建的二叉树
        System.out.println("赫夫曼树");
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        System.out.println("前序遍历");
        huffmanTreeRoot.preOrder();
    }

    /**
     * @param bytes 接收字节数组
     * @return 返回的就是 List形式,形式 [Node[date=97 ,weight = 5], Node[date=32,weight = 9]…]
     */
    private static List<Node> getNodes(byte[] bytes) {

        //1.创建一个ArrayList
        ArrayList<Node> nodes = new ArrayList<Node>();

        //遍历bytes,统计 每一个byte出现的次数 -> map[key,value]
        //byte数据,integer出现的次数
        Map<Byte, Integer> counts = new HashMap<Byte, Integer>();
        for (byte b : bytes) {
            Integer count = counts.get(b);
            if (count == null) {//Map还没有这个字符数据
                counts.put(b, 1);
            } else {
                counts.put(b, count + 1);
            }
        }

        //把每个键值对转成一个Node对象,并加入到nodes集合中
        for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
            nodes.add(new Node(entry.getKey(), entry.getValue()));
        }
        return nodes;
    }

    //可以通过List 创建对应的赫夫曼树
    private static Node createHuffmanTree(List<Node> nodes) {
        while (nodes.size() > 1) {
            //排序,从小到大
            Collections.sort(nodes);
            //取出第一颗最小的二叉树
            Node leftNode = nodes.get(0);
            //取出第二颗最小的二叉树
            Node rightNode = nodes.get(1);
            //创建一颗新的二叉树,它的根节点没有date
            Node parent = new Node(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;

            //将已经处理的两颗二叉树从nodes移除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //将新的二叉树加入到nodes
            nodes.add(parent);
        }
        //最终剩一个元素,返回的结点就是这棵HuffmanTree的根节点
        return nodes.get(0);
    }

    //前序遍历的方法
    private static void preOrder(Node root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("赫夫曼树为空,不能遍历");
        }

    }
}

//创建Node,带数据和权值
class Node implements Comparable<Node> {
    Byte date;// 存放数据本身,本身'a' => 97 ' ' => 32
    int weight;//权值,表示该字符出现次数
    Node left;
    Node right;

    public Node(Byte date, int weight) {
        this.date = date;
        this.weight = weight;
    }


    @Override
    public int compareTo(Node o) {
        //代表从小到大排序
        return this.weight - o.weight;
    }


    @Override
    public String toString() {
        return "Node{" +
                "date=" + date +
                ", weight=" + weight +
                '}';
    }

    //前序遍历
    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }
}


数据压缩-赫夫曼编码字节数组

使用赫夫曼编码来生成赫夫曼编码数据 ,即按照上面的赫夫曼编码
将"i like like like java do you like a java" 字符串生成对应的编码数据, 形式如下.

1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100

代码实现:
public class HuffmanCode {
    public static void main(String[] args) {
        String content = "i like like like java do you like a java";
        byte[] contentBytes = content.getBytes();
        //System.out.println(contentBytes.length);// 40

        List<Node> nodes = getNodes(contentBytes);
        System.out.println("nodes=" + nodes);

        //测试创建的二叉树
        System.out.println("赫夫曼树");
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        System.out.println("前序遍历");
        huffmanTreeRoot.preOrder();

        //测试,是否生成了对应的赫夫曼编码
        getCodes(huffmanTreeRoot);
        System.out.println("生成的赫夫曼编码表" + huffmanCodes);//{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
    }

    /**
     * @param bytes 接收字节数组
     * @return 返回的就是 List形式,形式 [Node[date=97 ,weight = 5], Node[date=32,weight = 9]…]
     */
    private static List<Node> getNodes(byte[] bytes) {

        //1.创建一个ArrayList
        ArrayList<Node> nodes = new ArrayList<Node>();

        //遍历bytes,统计 每一个byte出现的次数 -> map[key,value]
        //byte数据,integer出现的次数
        Map<Byte, Integer> counts = new HashMap<Byte, Integer>();
        for (byte b : bytes) {
            Integer count = counts.get(b);
            if (count == null) {//Map还没有这个字符数据
                counts.put(b, 1);
            } else {
                counts.put(b, count + 1);
            }
        }

        //把每个键值对转成一个Node对象,并加入到nodes集合中
        for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
            nodes.add(new Node(entry.getKey(), entry.getValue()));
        }
        return nodes;
    }

    //可以通过List 创建对应的赫夫曼树
    private static Node createHuffmanTree(List<Node> nodes) {
        while (nodes.size() > 1) {
            //排序,从小到大
            Collections.sort(nodes);
            //取出第一颗最小的二叉树
            Node leftNode = nodes.get(0);
            //取出第二颗最小的二叉树
            Node rightNode = nodes.get(1);
            //创建一颗新的二叉树,它的根节点没有date
            Node parent = new Node(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;

            //将已经处理的两颗二叉树从nodes移除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //将新的二叉树加入到nodes
            nodes.add(parent);
        }
        //最终剩一个元素,返回的结点就是这棵HuffmanTree的根节点
        return nodes.get(0);
    }

    //前序遍历的方法
    private static void preOrder(Node root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("赫夫曼树为空,不能遍历");
        }
    }

    //生成赫夫曼树对应的赫夫曼编码
    //思路:
    //1.将赫夫曼编码表存放在 Map<Byte,String>中
    //形式: 32-> =01,97->a=100,100->d=11000...
    static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();
    //2.在生成赫夫曼编码表时,需要去拼接路径,定义一个StringBuilder 存储某个叶子结点的路径
    static StringBuilder stringBuilder = new StringBuilder();


    //为了调用方便,我们重载getCodes方法
    private static Map<Byte, String> getCodes(Node root) {
        if (root == null){
            return null;
        }
        //处理root的左子树
        getCodes(root.left, "0", stringBuilder);
        //处理root的右子树
        getCodes(root.right, "1", stringBuilder);
        return huffmanCodes;
    }

    /**
     * 功能:将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入huffmanCodes集合中
     *
     * @param node          传入结点
     * @param code          路径的值:左子节点是0,右子结点是1
     * @param stringBuilder 是用于拼接路径(字符串)
     */
    private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //将code加入到stringBuilder2
        stringBuilder2.append(code);
        if (node != null) {//如果node == null 不处理
            //判断当前node是叶子结点还是非叶子结点
            if (node.date == null) {//说明它是非叶子非叶子结点
                //递归处理
                //向左递归
                getCodes(node.left, "0", stringBuilder2);
                //向右递归
                getCodes(node.right, "1", stringBuilder2);
            } else {//说明是一个叶子结点
                //就表示找到了某个叶子结点的最后,放入huffmanCodes中
                huffmanCodes.put(node.date, stringBuilder2.toString());
            }
        }
    }
}

//创建Node,带数据和权值
class Node implements Comparable<Node> {
    Byte date;// 存放数据本身,本身'a' => 97 ' ' => 32
    int weight;//权值,表示该字符出现次数
    Node left;
    Node right;

    public Node(Byte date, int weight) {
        this.date = date;
        this.weight = weight;
    }


    @Override
    public int compareTo(Node o) {
        //代表从小到大排序
        return this.weight - o.weight;
    }


    @Override
    public String toString() {
        return "Node{" +
                "date=" + date +
                ", weight=" + weight +
                '}';
    }

    //前序遍历
    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }
}

数据压缩-赫夫曼字节数组封装

使用赫夫曼编码来解码数据,具体要求是:
前面我们得到了赫夫曼编码和对应的编码 byte[]
即:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28] 现在要求使用赫夫曼编码,进行解码
又重新得到原来的字符串"i like like like java do you like a java"

针对上述的方法进行封装

具体代码如下:

public class HuffmanCode {
    public static void main(String[] args) {
        String content = "i like like like java do you like a java";
        byte[] contentBytes = content.getBytes();

        byte[] huffmanZipCodeBytes = huffmanZip(contentBytes);
        System.out.println("huffmanZipCodeBytes=" + Arrays.toString(huffmanZipCodeBytes)+ ",长度=" + huffmanZipCodeBytes.length);//17个数,压缩率57.5%


        /*
        封装前:
        System.out.println(contentBytes.length);//40
        List<Node> nodes = getNodes(contentBytes);
        System.out.println("nodes=" + nodes);

        //测试创建的二叉树
        System.out.println("赫夫曼树");
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        System.out.println("前序遍历");
        huffmanTreeRoot.preOrder();

        //测试,是否生成了对应的赫夫曼编码
        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
        System.out.println("生成的赫夫曼编码表=" + huffmanCodes);

        //测试
        byte[] huffmanZipCodeBytes = zip(bytes, huffmanCodes);
        //System.out.println("huffmanZipCodeBytes=" + Arrays.toString(bytes));//17个数,压缩率57.5%

        //发送huffmanCodeBytes 数组
        */

    }

    /**
     *
     * @param bytes 原始的字符串对应的字节数组
     * @return  是经过 赫夫曼编码处理后的字节数组(压缩后的数组)
     */
    //使用一个方法,将前面的方法封装起来,便于我们调用
    private static byte[] huffmanZip(byte[] bytes){
        //1.将数组转成List数组
        List<Node> nodes = getNodes(bytes);
        //2.将nodes数组创建成赫夫曼树
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        //3.创建对应的赫夫曼编码(根据赫夫曼树)
        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
        //4.根据赫夫曼编码对原始的字节进行压缩,压缩后得到的赫夫曼编码数组(二进制)
        byte[] huffmanZipCodeBytes = zip(bytes, huffmanCodes);
        //5.将压缩后的字节数组返回
        return huffmanZipCodeBytes;
    }

    /**
     * @param bytes 接收字节数组
     * @return 返回的就是 List形式,形式 [Node[date=97 ,weight = 5], Node[date=32,weight = 9]…]
     */
    private static List<Node> getNodes(byte[] bytes) {

        //1.创建一个ArrayList
        ArrayList<Node> nodes = new ArrayList<Node>();

        //遍历bytes,统计 每一个byte出现的次数 -> map[key,value]
        //byte数据,integer出现的次数
        Map<Byte, Integer> counts = new HashMap<Byte, Integer>();
        for (byte b : bytes) {
            Integer count = counts.get(b);
            if (count == null) {//Map还没有这个字符数据
                counts.put(b, 1);
            } else {
                counts.put(b, count + 1);
            }
        }

        //把每个键值对转成一个Node对象,并加入到nodes集合中
        for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
            nodes.add(new Node(entry.getKey(), entry.getValue()));
        }
        return nodes;
    }

    //可以通过List 创建对应的赫夫曼树
    private static Node createHuffmanTree(List<Node> nodes) {
        while (nodes.size() > 1) {
            //排序,从小到大
            Collections.sort(nodes);
            //取出第一颗最小的二叉树
            Node leftNode = nodes.get(0);
            //取出第二颗最小的二叉树
            Node rightNode = nodes.get(1);
            //创建一颗新的二叉树,它的根节点没有date
            Node parent = new Node(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;

            //将已经处理的两颗二叉树从nodes移除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //将新的二叉树加入到nodes
            nodes.add(parent);
        }
        //最终剩一个元素,返回的结点就是这棵HuffmanTree的根节点
        return nodes.get(0);
    }

    //前序遍历的方法
    private static void preOrder(Node root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("赫夫曼树为空,不能遍历");
        }
    }


    //编写一个方法,将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码压缩过后的byte[]数组

    /**
     * @param bytes        这是原始的字符串对应的byte[] -> ilikejava
     * @param huffmanCodes 生成的赫夫曼编码map
     * @return 返回赫夫曼编码处理后的byte[]
     * 举例:  String content = "i like like like java do you like a java"; => byte[] contentBytes = content.getBytes();
     * 返回的是 字符串"1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
     * 对应的byte[] huffmanCodeBytes,即8位对应一个byte(由于最大权结点离根越近,最小权结点离根最远,所以都是8位)放入到huffmanCodeBytes中
     * huffmanCodeBytes[0] = 10101000(补码) => byte   [推导 10101000=> 10101000 - 1 => 10100111(反码) ->11011000]
     * huffmanCodeBytes[1] = -88
     */
    private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {

        //1.利用 huffmanCodes 将 bytes 转成赫夫曼编码对应的字符串
        StringBuilder stringBuilder = new StringBuilder();
        //遍历 bytes 数组
        for (byte b : bytes) {
            //把获取到的huffmanCodes的字符串,拼接成一个长字符串
            //这个stringBuilder就是1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
            stringBuilder.append(huffmanCodes.get(b));
        }
        //System.out.println("测试stringBuilder=" + stringBuilder.toString());

        //将"1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
        //转成byte[]

        //统计返回 byte[] huffmanCodeBytes 长度
        //一句话搞定 int len = (stringBuilder.length() + 7) / 8
        int len;
        if (stringBuilder.length() % 8 == 0) {
            len = stringBuilder.length() / 8;
        } else {
            len = stringBuilder.length() / 8 + 1;
        }
        //创建存储压缩后的 byte数组
        byte[] hmCodeBytes = new byte[len];//17
        int index = 0;//记录是第几个byte
        for (int i = 0; i < stringBuilder.length(); i += 8) {//因为每8位对应一个byte,所以步长设为8
            String strByte;
            //索引越界问题
            if (i + 8 > stringBuilder.length()) {//最后一位不够8位
                strByte = stringBuilder.substring(i);
            } else {
                strByte = stringBuilder.substring(i, i + 8);
            }


            //将strByte转成一个byte,放入到by中
            hmCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
            index++;
        }
        //当for循环过后,hmCodeBytes数组长度为8,每个索引(最后一个?)有8个字节
        //也就得到了赫夫曼树编码所对应的字节数组
        return hmCodeBytes;
    }

    //生成赫夫曼树对应的赫夫曼编码
    //思路:
    //1.将赫夫曼编码表存放在 Map<Byte,String>中
    //形式: 32-> =01,97->a=100,100->d=11000...
    static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();
    //2.在生成赫夫曼编码表时,需要去拼接路径,定义一个StringBuilder 存储某个叶子结点的路径
    static StringBuilder stringBuilder = new StringBuilder();


    //为了调用方便,我们重载getCodes方法
    private static Map<Byte, String> getCodes(Node root) {
        if (root == null) {
            return null;
        }
        //处理root的左子树
        getCodes(root.left, "0", stringBuilder);
        //处理root的右子树
        getCodes(root.right, "1", stringBuilder);
        return huffmanCodes;
    }

    /**
     * 功能:将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入huffmanCodes集合中
     *
     * @param node          传入结点
     * @param code          路径的值:左子节点是0,右子结点是1
     * @param stringBuilder 是用于拼接路径(字符串)
     */
    private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //将code加入到stringBuilder2
        stringBuilder2.append(code);
        if (node != null) {//如果node == null 不处理
            //判断当前node是叶子结点还是非叶子结点
            if (node.date == null) {//说明它是非叶子非叶子结点
                //递归处理
                //向左递归
                getCodes(node.left, "0", stringBuilder2);
                //向右递归
                getCodes(node.right, "1", stringBuilder2);
            } else {//说明是一个叶子结点
                //就表示找到了某个叶子结点的最后,放入huffmanCodes中
                huffmanCodes.put(node.date, stringBuilder2.toString());
            }
        }
    }
}

//创建Node,带数据和权值
class Node implements Comparable<Node> {
    Byte date;// 存放数据本身,本身'a' => 97 ' ' => 32
    int weight;//权值,表示该字符出现次数
    Node left;
    Node right;

    public Node(Byte date, int weight) {
        this.date = date;
        this.weight = weight;
    }


    @Override
    public int compareTo(Node o) {
        //代表从小到大排序
        return this.weight - o.weight;
    }


    @Override
    public String toString() {
        return "Node{" +
                "date=" + date +
                ", weight=" + weight +
                '}';
    }

    //前序遍历
    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }
}


数据解压-字节转二进制字符串

使用赫夫曼编码来解码数据,具体要求是

1.前面我们得到了赫夫曼编码和对应的编码 byte[] , 即:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28] 现在要求使用赫夫曼编码, 进行解码,又重新得到原来的字符串"i like like like java do you like a java"

//完成数据的解压
//思路
//1.将hfCoBytes[-88, -65, -56, -65, -56, -65, -55, 77,
-57, 6, -24, -14, -117, -4, -60, -90, 28] // 重写先转成赫夫曼编码对应的二进制的字符串 “1010100010111…” //2.赫夫曼编码对应的二进制的字符串 “1010100010111…” =》 对照 赫夫曼编码 => “i like like like java do you like a java”

数据解压-赫夫曼解码

该两步放在一起
public class HuffmanCode {
    public static void main(String[] args) {
        String content = "i like like like java do you like a java";
        byte[] contentBytes = content.getBytes();

        byte[] huffmanZipCodeBytes = huffmanZip(contentBytes);
        System.out.println("压缩后的结果,huffmanZipCodeBytes=" + Arrays.toString(huffmanZipCodeBytes) + ",长度=" + huffmanZipCodeBytes.length);//17个数,压缩率57.5%


        //测试byteToBitString方法
        //System.out.println(byteToBitString((byte)1));
        byte[] sourceBytes = decode(huffmanCodes, huffmanZipCodeBytes);
        System.out.println("原来的字符串=" + new String(sourceBytes));

        /*
        封装前:
        System.out.println(contentBytes.length);//40
        List<Node> nodes = getNodes(contentBytes);
        System.out.println("nodes=" + nodes);

        //测试创建的二叉树
        System.out.println("赫夫曼树");
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        System.out.println("前序遍历");
        huffmanTreeRoot.preOrder();

        //测试,是否生成了对应的赫夫曼编码
        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
        System.out.println("生成的赫夫曼编码表=" + huffmanCodes);

        //测试
        byte[] huffmanZipCodeBytes = zip(bytes, huffmanCodes);
        //System.out.println("huffmanZipCodeBytes=" + Arrays.toString(bytes));//17个数,压缩率57.5%

        //发送huffmanCodeBytes 数组
        */

    }


    }

    /*完成数据的解压
    思路
    1.将huffmanCodeBytes:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
      重新先转成 赫夫曼编码对应的二进制的字符串"1010100010111111110010001011111111..."
    2.赫夫曼编码对应的二进制字符串"10101000010111..." => 对照 赫夫曼编码 => "i like like like java do you like a java"
    */


    //编写一个方法,完成对压缩数据的解码

    /**
     * @param huffmanCodes 赫夫曼编码表 map
     * @param huffmanBytes 赫夫曼编码得到的字节数组
     * @return 原来的字符串对应的数组
     */
    private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {

        //1.先得到huffmanBytes 对应的二进制的字符串,形式如10101000010111...
        StringBuilder stringBuilder1 = new StringBuilder();
        //2.将byte数组转成二进制的字符串
        for (int i = 0; i < huffmanBytes.length; i++) {
            byte b = huffmanBytes[i];
            //判断是不是最后一个字节
            boolean flag = (i == huffmanBytes.length - 1);
            stringBuilder.append(byteToBitString(!flag, b));
        }
        //System.out.println("赫夫曼字节数组对应的二进制字符串=" + stringBuilder.toString());

        /*
        把字符串按照指定的赫夫曼编码进行解码
        把赫夫曼编码表进行调换,因为反向查询 97 -> 100 100-> a
        */
        Map<String, Byte> map = new HashMap<String, Byte>();

        for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
            map.put(entry.getValue(), entry.getKey());
        }

        //创建要给的集合,存放byte
        List<Byte> list = new ArrayList<>();
        //i可以理解为,就是一个索引,不停的扫描stringBuilder=10101000010111...
        for (int i = 0; i < stringBuilder.length(); ) {
            int count = 1;//小的计数器
            boolean flag = true;
            Byte b = null;

            while (flag) {
                //递增地取出一个'1' '0' 101000010111...
                String key = stringBuilder.substring(i, i + count);//i 不动,让count移动,直到匹配到一个字符
                b = map.get(key);
                if (b == null) {//说明还没匹配到字符
                    count++;
                } else {//说明已经匹配到
                    flag = false;
                }
            }
            list.add(b);
            //i += count-1;//i直接移动到 count 的位置
            i += count;

        }
        //当for循环结束后,我们list中就存放了所有的字符 i like java
        //把list中的数据,放入到byte[] 并返回
        byte[] b = new byte[list.size()];
        for (int i = 0; i < b.length; i++) {
            b[i] = list.get(i);
        }
        return b;
    }

    /**
     * 将一个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
        //如果是正数,我们还存在补高位
        if (flag) {
            temp |= 256;//按为与 256 1 0000 0000  |   0000 00001 => 1 0000 0001
            // temp 1 => 000
        }


        String str = Integer.toBinaryString(temp);//注意!!此处返回的是temp对应的二进制的补码
        if (flag) {
            return str.substring(str.length() - 8);//截取str倒数第8位
        } else {
            return str;
        }
    }


    /**
     * @param bytes 原始的字符串对应的字节数组
     * @return 是经过 赫夫曼编码处理后的字节数组(压缩后的数组)
     */
    //使用一个方法,将前面的方法封装起来,便于我们调用
    private static byte[] huffmanZip(byte[] bytes) {
        //1.将数组转成List数组
        List<Node> nodes = getNodes(bytes);
        //2.将nodes数组创建成赫夫曼树
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        //3.创建对应的赫夫曼编码(根据赫夫曼树)
        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
        //4.根据赫夫曼编码对原始的字节进行压缩,压缩后得到的赫夫曼编码数组(二进制)
        byte[] huffmanZipCodeBytes = zip(bytes, huffmanCodes);
        //5.将压缩后的字节数组返回
        return huffmanZipCodeBytes;
    }

    /**
     * @param bytes 接收字节数组
     * @return 返回的就是 List形式,形式 [Node[date=97 ,weight = 5], Node[date=32,weight = 9]…]
     */
    private static List<Node> getNodes(byte[] bytes) {

        //1.创建一个ArrayList
        ArrayList<Node> nodes = new ArrayList<Node>();

        //遍历bytes,统计 每一个byte出现的次数 -> map[key,value]
        //byte数据,integer出现的次数
        Map<Byte, Integer> counts = new HashMap<Byte, Integer>();
        for (byte b : bytes) {
            Integer count = counts.get(b);
            if (count == null) {//Map还没有这个字符数据
                counts.put(b, 1);
            } else {
                counts.put(b, count + 1);
            }
        }

        //把每个键值对转成一个Node对象,并加入到nodes集合中
        for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
            nodes.add(new Node(entry.getKey(), entry.getValue()));
        }
        return nodes;
    }

    //可以通过List 创建对应的赫夫曼树
    private static Node createHuffmanTree(List<Node> nodes) {
        while (nodes.size() > 1) {
            //排序,从小到大
            Collections.sort(nodes);
            //取出第一颗最小的二叉树
            Node leftNode = nodes.get(0);
            //取出第二颗最小的二叉树
            Node rightNode = nodes.get(1);
            //创建一颗新的二叉树,它的根节点没有date
            Node parent = new Node(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;

            //将已经处理的两颗二叉树从nodes移除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //将新的二叉树加入到nodes
            nodes.add(parent);
        }
        //最终剩一个元素,返回的结点就是这棵HuffmanTree的根节点
        return nodes.get(0);
    }

    //前序遍历的方法
    private static void preOrder(Node root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("赫夫曼树为空,不能遍历");
        }
    }


    //编写一个方法,将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码压缩过后的byte[]数组

    /**
     * @param bytes        这是原始的字符串对应的byte[] -> ilikejava
     * @param huffmanCodes 生成的赫夫曼编码map
     * @return 返回赫夫曼编码处理后的byte[]
     * 举例:  String content = "i like like like java do you like a java"; => byte[] contentBytes = content.getBytes();
     * 返回的是 字符串"1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
     * 对应的byte[] huffmanCodeBytes,即8位对应一个byte(由于最大权结点离根越近,最小权结点离根最远,所以都是8位)放入到huffmanCodeBytes中
     * huffmanCodeBytes[0] = 10101000(补码) => byte   [推导 10101000=> 10101000 - 1 => 10100111(反码) ->11011000]
     * huffmanCodeBytes[1] = -88
     */
    private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {

        //1.利用 huffmanCodes 将 bytes 转成赫夫曼编码对应的字符串
        StringBuilder stringBuilder = new StringBuilder();
        //遍历 bytes 数组
        for (byte b : bytes) {
            //把获取到的huffmanCodes的字符串,拼接成一个长字符串
            //这个stringBuilder就是1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
            stringBuilder.append(huffmanCodes.get(b));
        }
        //System.out.println("测试stringBuilder=" + stringBuilder.toString());

        /*
        将"1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
        转成byte[]
         */


        //统计返回 byte[] huffmanCodeBytes 长度
        //一句话搞定 int len = (stringBuilder.length() + 7) / 8
        int len;
        if (stringBuilder.length() % 8 == 0) {
            len = stringBuilder.length() / 8;
        } else {
            len = stringBuilder.length() / 8 + 1;
        }
        //创建存储压缩后的 byte数组
        byte[] hmCodeBytes = new byte[len];//17
        int index = 0;//记录是第几个byte
        for (int i = 0; i < stringBuilder.length(); i += 8) {//因为每8位对应一个byte,所以步长设为8
            String strByte;
            //索引越界问题
            if (i + 8 > stringBuilder.length()) {//最后一位不够8位
                strByte = stringBuilder.substring(i);
            } else {
                strByte = stringBuilder.substring(i, i + 8);
            }


            //将strByte转成一个byte,放入到by中
            hmCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
            index++;
        }
        //当for循环过后,hmCodeBytes数组长度为8,每个索引(最后一个?)有8个字节
        //也就得到了赫夫曼树编码所对应的字节数组
        return hmCodeBytes;
    }

    /*
    生成赫夫曼树对应的赫夫曼编码
    思路:
    1.将赫夫曼编码表存放在 Map<Byte,String>中
    形式: 32-> =01,97->a=100,100->d=11000...
     */

    static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();
    //2.在生成赫夫曼编码表时,需要去拼接路径,定义一个StringBuilder 存储某个叶子结点的路径
    static StringBuilder stringBuilder = new StringBuilder();


    //为了调用方便,我们重载getCodes方法
    private static Map<Byte, String> getCodes(Node root) {
        if (root == null) {
            return null;
        }
        //处理root的左子树
        getCodes(root.left, "0", stringBuilder);
        //处理root的右子树
        getCodes(root.right, "1", stringBuilder);
        return huffmanCodes;
    }

    /**
     * 功能:将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入huffmanCodes集合中
     *
     * @param node          传入结点
     * @param code          路径的值:左子节点是0,右子结点是1
     * @param stringBuilder 是用于拼接路径(字符串)
     */
    private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //将code加入到stringBuilder2
        stringBuilder2.append(code);
        if (node != null) {//如果node == null 不处理
            //判断当前node是叶子结点还是非叶子结点
            if (node.date == null) {//说明它是非叶子非叶子结点
                //递归处理
                //向左递归
                getCodes(node.left, "0", stringBuilder2);
                //向右递归
                getCodes(node.right, "1", stringBuilder2);
            } else {//说明是一个叶子结点
                //就表示找到了某个叶子结点的最后,放入huffmanCodes中
                huffmanCodes.put(node.date, stringBuilder2.toString());
            }
        }
    }
}

//创建Node,带数据和权值
class Node implements Comparable<Node> {
    Byte date;// 存放数据本身,本身'a' => 97 ' ' => 32
    int weight;//权值,表示该字符出现次数
    Node left;
    Node right;

    public Node(Byte date, int weight) {
        this.date = date;
        this.weight = weight;
    }


    @Override
    public int compareTo(Node o) {
        //代表从小到大排序
        return this.weight - o.weight;
    }


    @Override
    public String toString() {
        return "Node{" +
                "date=" + date +
                ", weight=" + weight +
                '}';
    }

    //前序遍历
    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }
}

使用赫夫曼编码压缩文件

通过赫夫曼编码对一个字符串进行编码和解码, 下面我们来完成对文件的压缩和解压, 具体要求:给你一个图片文件,要求对其进行无损压缩,
看看压缩效果如何。

  • 思路:读取文件-> 得到赫夫曼编码表 -> 完成压缩
代码实现:

在这里插入图片描述

public class HuffmanCode {

    public static void main(String[] args) {

        //测试压缩文件
        String srcFile = "C:\\Users\\86182\\Desktop\\ZipFile\\zzz.txt";
        String dstFile = "C:\\Users\\86182\\Desktop\\dstZipFile\\zzz.zip";

        zipFile(srcFile, dstFile);
        System.out.println("压缩文件成功~");

        /*
        String content = "i like like like java do you like a java";
        byte[] contentBytes = content.getBytes();

        byte[] huffmanZipCodeBytes = huffmanZip(contentBytes);
        System.out.println("压缩后的结果,huffmanZipCodeBytes=" + Arrays.toString(huffmanZipCodeBytes) + ",长度=" + huffmanZipCodeBytes.length);//17个数,压缩率57.5%


        //测试byteToBitString方法
        //System.out.println(byteToBitString((byte)1));
        byte[] sourceBytes = decode(huffmanCodes, huffmanZipCodeBytes);
        System.out.println("原来的字符串=" + new String(sourceBytes));
    */
        /*
        封装前:
        System.out.println(contentBytes.length);//40
        List<Node> nodes = getNodes(contentBytes);
        System.out.println("nodes=" + nodes);

        //测试创建的二叉树
        System.out.println("赫夫曼树");
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        System.out.println("前序遍历");
        huffmanTreeRoot.preOrder();

        //测试,是否生成了对应的赫夫曼编码
        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
        System.out.println("生成的赫夫曼编码表=" + huffmanCodes);

        //测试
        byte[] huffmanZipCodeBytes = zip(bytes, huffmanCodes);
        //System.out.println("huffmanZipCodeBytes=" + Arrays.toString(bytes));//17个数,压缩率57.5%

        //发送huffmanCodeBytes 数组
        */

    }


    //编写方法,将一个文件进行压缩

    /**
     *
     * @param srcFile   传入的压缩文件完整路径
     * @param dstFile   压缩文件放到哪个目录下
     */
    public static void zipFile(String srcFile,String dstFile){

        //创建输出流 [此处需要用到IO流]
        OutputStream os = null;
        ObjectOutputStream oos = null;
        //创建文件输入流
        FileInputStream is = null;
        try {
            //1.创建文件输入流
            is = new FileInputStream(srcFile);
            //2.创建一个和源文件大小一样的byte数组->读取数据
            byte[] b = new byte[is.available()];//available返回源文件大小
            //3.拿到源文件->读取文件
            is.read(b);//读到了内容
            //4.直接对 b(源文件一样) 进行压缩
            //拿到赫夫曼压缩后的字节
            byte[] huffmanBytes = huffmanZip(b);//huffmanBytes 压缩后的数组
            //创建文件的输出流,存放压缩文件
            os = new FileOutputStream(dstFile);
            //创建一个和文件输出流关联的ObjectOutputStream
            oos = new ObjectOutputStream(os);//接收一个对象输出流

            //把赫夫曼编码后的字节数组写入压缩文件
            oos.writeObject(huffmanBytes);

            //这里我们以对象流的方式,写入赫夫曼编码->以后我们恢复源文件时使用
            //注意!一定要把赫夫曼编码写入压缩文件
            oos.writeObject(huffmanCodes);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }finally {
            try {
                is.close();
                oos.close();
                os.close();
            }catch (Exception e){
                System.out.println(e.getMessage());
            }

        }

    }

    /*完成数据的解压
    思路
    1.将huffmanCodeBytes:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
      重新先转成 赫夫曼编码对应的二进制的字符串"1010100010111111110010001011111111..."
    2.赫夫曼编码对应的二进制字符串"10101000010111..." => 对照 赫夫曼编码 => "i like like like java do you like a java"
    */


    //编写一个方法,完成对压缩数据的解码

    /**
     * @param huffmanCodes 赫夫曼编码表 map
     * @param huffmanBytes 赫夫曼编码得到的字节数组
     * @return 原来的字符串对应的数组
     */
    private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {

        //1.先得到huffmanBytes 对应的二进制的字符串,形式如10101000010111...
        StringBuilder stringBuilder1 = new StringBuilder();
        //2.将byte数组转成二进制的字符串
        for (int i = 0; i < huffmanBytes.length; i++) {
            byte b = huffmanBytes[i];
            //判断是不是最后一个字节
            boolean flag = (i == huffmanBytes.length - 1);
            stringBuilder.append(byteToBitString(!flag, b));
        }
        //System.out.println("赫夫曼字节数组对应的二进制字符串=" + stringBuilder.toString());

        /*
        把字符串按照指定的赫夫曼编码进行解码
        把赫夫曼编码表进行调换,因为反向查询 97 -> 100 100-> a
        */
        Map<String, Byte> map = new HashMap<String, Byte>();

        for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
            map.put(entry.getValue(), entry.getKey());
        }

        //创建要给的集合,存放byte
        List<Byte> list = new ArrayList<>();
        //i可以理解为,就是一个索引,不停的扫描stringBuilder=10101000010111...
        for (int i = 0; i < stringBuilder.length(); ) {
            int count = 1;//小的计数器
            boolean flag = true;
            Byte b = null;

            while (flag) {
                //递增地取出一个'1' '0' 101000010111...
                String key = stringBuilder.substring(i, i + count);//i 不动,让count移动,直到匹配到一个字符
                b = map.get(key);
                if (b == null) {//说明还没匹配到字符
                    count++;
                } else {//说明已经匹配到
                    flag = false;
                }
            }
            list.add(b);
            //i += count-1;//i直接移动到 count 的位置
            i += count;

        }
        //当for循环结束后,我们list中就存放了所有的字符 i like java
        //把list中的数据,放入到byte[] 并返回
        byte[] b = new byte[list.size()];
        for (int i = 0; i < b.length; i++) {
            b[i] = list.get(i);
        }
        return b;
    }

    /**
     * 将一个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
        //如果是正数,我们还存在补高位
        if (flag) {
            temp |= 256;//按为与 256 1 0000 0000  |   0000 00001 => 1 0000 0001
            // temp 1 => 000
        }


        String str = Integer.toBinaryString(temp);//注意!!此处返回的是temp对应的二进制的补码
        if (flag) {
            return str.substring(str.length() - 8);//截取str倒数第8位
        } else {
            return str;
        }
    }


    /**
     * @param bytes 原始的字符串对应的字节数组
     * @return 是经过 赫夫曼编码处理后的字节数组(压缩后的数组)
     */
    //使用一个方法,将前面的方法封装起来,便于我们调用
    private static byte[] huffmanZip(byte[] bytes) {
        //1.将数组转成List数组
        List<Node> nodes = getNodes(bytes);
        //2.将nodes数组创建成赫夫曼树
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        //3.创建对应的赫夫曼编码(根据赫夫曼树)
        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
        //4.根据赫夫曼编码对原始的字节进行压缩,压缩后得到的赫夫曼编码数组(二进制)
        byte[] huffmanZipCodeBytes = zip(bytes, huffmanCodes);
        //5.将压缩后的字节数组返回
        return huffmanZipCodeBytes;
    }

    /**
     * @param bytes 接收字节数组
     * @return 返回的就是 List形式,形式 [Node[date=97 ,weight = 5], Node[date=32,weight = 9]…]
     */
    private static List<Node> getNodes(byte[] bytes) {

        //1.创建一个ArrayList
        ArrayList<Node> nodes = new ArrayList<Node>();

        //遍历bytes,统计 每一个byte出现的次数 -> map[key,value]
        //byte数据,integer出现的次数
        Map<Byte, Integer> counts = new HashMap<Byte, Integer>();
        for (byte b : bytes) {
            Integer count = counts.get(b);
            if (count == null) {//Map还没有这个字符数据
                counts.put(b, 1);
            } else {
                counts.put(b, count + 1);
            }
        }

        //把每个键值对转成一个Node对象,并加入到nodes集合中
        for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
            nodes.add(new Node(entry.getKey(), entry.getValue()));
        }
        return nodes;
    }

    //可以通过List 创建对应的赫夫曼树
    private static Node createHuffmanTree(List<Node> nodes) {
        while (nodes.size() > 1) {
            //排序,从小到大
            Collections.sort(nodes);
            //取出第一颗最小的二叉树
            Node leftNode = nodes.get(0);
            //取出第二颗最小的二叉树
            Node rightNode = nodes.get(1);
            //创建一颗新的二叉树,它的根节点没有date
            Node parent = new Node(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;

            //将已经处理的两颗二叉树从nodes移除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //将新的二叉树加入到nodes
            nodes.add(parent);
        }
        //最终剩一个元素,返回的结点就是这棵HuffmanTree的根节点
        return nodes.get(0);
    }

    //前序遍历的方法
    private static void preOrder(Node root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("赫夫曼树为空,不能遍历");
        }
    }


    //编写一个方法,将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码压缩过后的byte[]数组

    /**
     * @param bytes        这是原始的字符串对应的byte[] -> ilikejava
     * @param huffmanCodes 生成的赫夫曼编码map
     * @return 返回赫夫曼编码处理后的byte[]
     * 举例:  String content = "i like like like java do you like a java"; => byte[] contentBytes = content.getBytes();
     * 返回的是 字符串"1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
     * 对应的byte[] huffmanCodeBytes,即8位对应一个byte(由于最大权结点离根越近,最小权结点离根最远,所以都是8位)放入到huffmanCodeBytes中
     * huffmanCodeBytes[0] = 10101000(补码) => byte   [推导 10101000=> 10101000 - 1 => 10100111(反码) ->11011000]
     * huffmanCodeBytes[1] = -88
     */
    private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {

        //1.利用 huffmanCodes 将 bytes 转成赫夫曼编码对应的字符串
        StringBuilder stringBuilder = new StringBuilder();
        //遍历 bytes 数组
        for (byte b : bytes) {
            //把获取到的huffmanCodes的字符串,拼接成一个长字符串
            //这个stringBuilder就是1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
            stringBuilder.append(huffmanCodes.get(b));
        }
        //System.out.println("测试stringBuilder=" + stringBuilder.toString());

        /*
        将"1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
        转成byte[]
         */


        //统计返回 byte[] huffmanCodeBytes 长度
        //一句话搞定 int len = (stringBuilder.length() + 7) / 8
        int len;
        if (stringBuilder.length() % 8 == 0) {
            len = stringBuilder.length() / 8;
        } else {
            len = stringBuilder.length() / 8 + 1;
        }
        //创建存储压缩后的 byte数组
        byte[] hmCodeBytes = new byte[len];//17
        int index = 0;//记录是第几个byte
        for (int i = 0; i < stringBuilder.length(); i += 8) {//因为每8位对应一个byte,所以步长设为8
            String strByte;
            //索引越界问题
            if (i + 8 > stringBuilder.length()) {//最后一位不够8位
                strByte = stringBuilder.substring(i);
            } else {
                strByte = stringBuilder.substring(i, i + 8);
            }


            //将strByte转成一个byte,放入到by中
            hmCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
            index++;
        }
        //当for循环过后,hmCodeBytes数组长度为8,每个索引(最后一个?)有8个字节
        //也就得到了赫夫曼树编码所对应的字节数组
        return hmCodeBytes;
    }

    /*
    生成赫夫曼树对应的赫夫曼编码
    思路:
    1.将赫夫曼编码表存放在 Map<Byte,String>中
    形式: 32-> =01,97->a=100,100->d=11000...
     */

    static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();
    //2.在生成赫夫曼编码表时,需要去拼接路径,定义一个StringBuilder 存储某个叶子结点的路径
    static StringBuilder stringBuilder = new StringBuilder();


    //为了调用方便,我们重载getCodes方法
    private static Map<Byte, String> getCodes(Node root) {
        if (root == null) {
            return null;
        }
        //处理root的左子树
        getCodes(root.left, "0", stringBuilder);
        //处理root的右子树
        getCodes(root.right, "1", stringBuilder);
        return huffmanCodes;
    }

    /**
     * 功能:将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入huffmanCodes集合中
     *
     * @param node          传入结点
     * @param code          路径的值:左子节点是0,右子结点是1
     * @param stringBuilder 是用于拼接路径(字符串)
     */
    private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //将code加入到stringBuilder2
        stringBuilder2.append(code);
        if (node != null) {//如果node == null 不处理
            //判断当前node是叶子结点还是非叶子结点
            if (node.date == null) {//说明它是非叶子非叶子结点
                //递归处理
                //向左递归
                getCodes(node.left, "0", stringBuilder2);
                //向右递归
                getCodes(node.right, "1", stringBuilder2);
            } else {//说明是一个叶子结点
                //就表示找到了某个叶子结点的最后,放入huffmanCodes中
                huffmanCodes.put(node.date, stringBuilder2.toString());
            }
        }
    }
}

//创建Node,带数据和权值
class Node implements Comparable<Node> {
    Byte date;// 存放数据本身,本身'a' => 97 ' ' => 32
    int weight;//权值,表示该字符出现次数
    Node left;
    Node right;

    public Node(Byte date, int weight) {
        this.date = date;
        this.weight = weight;
    }


    @Override
    public int compareTo(Node o) {
        //代表从小到大排序
        return this.weight - o.weight;
    }


    @Override
    public String toString() {
        return "Node{" +
                "date=" + date +
                ", weight=" + weight +
                '}';
    }

    //前序遍历
    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }
}

使用赫夫曼编码解压文件

  • 具体要求:将前面压缩的文件,重新恢复成原来的文件。
  • 思路:读取压缩文件(数据和赫夫曼编码表) -> 完成解压(文件恢复)
代码实现:
public class HuffmanCode {

    public static void main(String[] args) {

        //测试压缩文件
        String srcFile = "C:\\Users\\86182\\Desktop\\ZipFile\\zzz.txt";
        String dstFile = "C:\\Users\\86182\\Desktop\\dstZipFile\\zzz.zip";

        //zipFile(srcFile, dstFile);
        //System.out.println("压缩文件成功~");

        //测试解压文件
        String zipFile = "C:\\Users\\86182\\Desktop\\dstZipFile\\zzz.zip";
        String deFile = "C:\\Users\\86182\\Desktop\\deZipFile\\dezzz.txt";
        deZipFile(zipFile, deFile);
        System.out.println("解压成功~");

        /*
        String content = "i like like like java do you like a java";
        byte[] contentBytes = content.getBytes();

        byte[] huffmanZipCodeBytes = huffmanZip(contentBytes);
        System.out.println("压缩后的结果,huffmanZipCodeBytes=" + Arrays.toString(huffmanZipCodeBytes) + ",长度=" + huffmanZipCodeBytes.length);//17个数,压缩率57.5%


        //测试byteToBitString方法
        //System.out.println(byteToBitString((byte)1));
        byte[] sourceBytes = decode(huffmanCodes, huffmanZipCodeBytes);
        System.out.println("原来的字符串=" + new String(sourceBytes));
    */
        /*
        封装前:
        System.out.println(contentBytes.length);//40
        List<Node> nodes = getNodes(contentBytes);
        System.out.println("nodes=" + nodes);

        //测试创建的二叉树
        System.out.println("赫夫曼树");
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        System.out.println("前序遍历");
        huffmanTreeRoot.preOrder();

        //测试,是否生成了对应的赫夫曼编码
        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
        System.out.println("生成的赫夫曼编码表=" + huffmanCodes);

        //测试
        byte[] huffmanZipCodeBytes = zip(bytes, huffmanCodes);
        //System.out.println("huffmanZipCodeBytes=" + Arrays.toString(bytes));//17个数,压缩率57.5%

        //发送huffmanCodeBytes 数组
        */

    }


    //编写方法,将一个文件进行压缩

    /**
     *
     * @param srcFile   传入的压缩文件完整路径
     * @param dstFile   压缩文件放到哪个目录下
     */
    public static void zipFile(String srcFile,String dstFile){

        //创建输出流 [此处需要用到IO流]
        OutputStream os = null;
        ObjectOutputStream oos = null;
        //创建文件输入流
        FileInputStream is = null;
        try {
            //1.创建文件输入流
            is = new FileInputStream(srcFile);
            //2.创建一个和源文件大小一样的byte数组->读取数据
            byte[] b = new byte[is.available()];//available返回源文件大小
            //3.拿到源文件->读取文件
            is.read(b);//读到了内容
            //4.直接对 b(源文件一样) 进行压缩
            //拿到赫夫曼压缩后的字节
            byte[] huffmanBytes = huffmanZip(b);//huffmanBytes 压缩后的数组
            //创建文件的输出流,存放压缩文件
            os = new FileOutputStream(dstFile);
            //创建一个和文件输出流关联的ObjectOutputStream
            oos = new ObjectOutputStream(os);//接收一个对象输出流

            //把赫夫曼编码后的字节数组写入压缩文件
            oos.writeObject(huffmanBytes);

            //这里我们以对象流的方式,写入赫夫曼编码->以后我们恢复源文件时使用
            //注意!一定要把赫夫曼编码写入压缩文件
            oos.writeObject(huffmanCodes);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }finally {
            try {
                is.close();
                oos.close();
                os.close();
            }catch (Exception e){
                System.out.println(e.getMessage());
            }

        }

    }

    //编写一个方法,完成对压缩文件的解压

    /**
     *
     * @param zipFile   准备解压的文件
     * @param dstFile   将解压的文件发到哪个路径
     */
    public static void deZipFile(String zipFile,String dstFile){

        //定义文件输入流
        InputStream is = null;
        //定义一个对象输入流
        ObjectInputStream ois = null;
        //定义文件的输出流
        OutputStream os = null;
        try {
            //创建文件输入流
            is = new FileInputStream(zipFile);
            //创建一个和 is关联的对象输入流
            ois = new ObjectInputStream(is);
            //读取byte数组 huffmanBytes
            byte[] huffmanBytes = (byte[]) ois.readObject();
            //读取保存的赫夫曼编码表
            Map<Byte,String> huffmanCodes = (Map<Byte,String>)ois.readObject();

            //有了数组,有了编码表,可以解码了
            //原始bytes
            byte[] bytes = decode(huffmanCodes, huffmanBytes);
            //将bytes数组写入到目标文件
              os = new FileOutputStream(dstFile);
            //写数据到 dstFile 文件中
            os.write(bytes);

        }catch (Exception e){
            System.out.println(e.getMessage());
        }finally {
            try {
                os.close();
                ois.close();
                is.close();
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }
    }



    /*完成数据的解压
    思路
    1.将huffmanCodeBytes:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
      重新先转成 赫夫曼编码对应的二进制的字符串"1010100010111111110010001011111111..."
    2.赫夫曼编码对应的二进制字符串"10101000010111..." => 对照 赫夫曼编码 => "i like like like java do you like a java"
    */


    //编写一个方法,完成对压缩数据的解码

    /**
     * @param huffmanCodes 赫夫曼编码表 map
     * @param huffmanBytes 赫夫曼编码得到的字节数组
     * @return 原来的字符串对应的数组
     */
    private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {

        //1.先得到huffmanBytes 对应的二进制的字符串,形式如10101000010111...
        StringBuilder stringBuilder1 = new StringBuilder();
        //2.将byte数组转成二进制的字符串
        for (int i = 0; i < huffmanBytes.length; i++) {
            byte b = huffmanBytes[i];
            //判断是不是最后一个字节
            boolean flag = (i == huffmanBytes.length - 1);
            stringBuilder.append(byteToBitString(!flag, b));
        }
        //System.out.println("赫夫曼字节数组对应的二进制字符串=" + stringBuilder.toString());

        /*
        把字符串按照指定的赫夫曼编码进行解码
        把赫夫曼编码表进行调换,因为反向查询 97 -> 100 100-> a
        */
        Map<String, Byte> map = new HashMap<String, Byte>();

        for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
            map.put(entry.getValue(), entry.getKey());
        }

        //创建要给的集合,存放byte
        List<Byte> list = new ArrayList<>();
        //i可以理解为,就是一个索引,不停的扫描stringBuilder=10101000010111...
        for (int i = 0; i < stringBuilder.length(); ) {
            int count = 1;//小的计数器
            boolean flag = true;
            Byte b = null;

            while (flag) {
                //递增地取出一个'1' '0' 101000010111...
                String key = stringBuilder.substring(i, i + count);//i 不动,让count移动,直到匹配到一个字符
                b = map.get(key);
                if (b == null) {//说明还没匹配到字符
                    count++;
                } else {//说明已经匹配到
                    flag = false;
                }
            }
            list.add(b);
            //i += count-1;//i直接移动到 count 的位置
            i += count;

        }
        //当for循环结束后,我们list中就存放了所有的字符 i like java
        //把list中的数据,放入到byte[] 并返回
        byte[] b = new byte[list.size()];
        for (int i = 0; i < b.length; i++) {
            b[i] = list.get(i);
        }
        return b;
    }

    /**
     * 将一个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
        //如果是正数,我们还存在补高位
        if (flag) {
            temp |= 256;//按为与 256 1 0000 0000  |   0000 00001 => 1 0000 0001
            // temp 1 => 000
        }


        String str = Integer.toBinaryString(temp);//注意!!此处返回的是temp对应的二进制的补码
        if (flag) {
            return str.substring(str.length() - 8);//截取str倒数第8位
        } else {
            return str;
        }
    }


    /**
     * @param bytes 原始的字符串对应的字节数组
     * @return 是经过 赫夫曼编码处理后的字节数组(压缩后的数组)
     */
    //使用一个方法,将前面的方法封装起来,便于我们调用
    private static byte[] huffmanZip(byte[] bytes) {
        //1.将数组转成List数组
        List<Node> nodes = getNodes(bytes);
        //2.将nodes数组创建成赫夫曼树
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        //3.创建对应的赫夫曼编码(根据赫夫曼树)
        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
        //4.根据赫夫曼编码对原始的字节进行压缩,压缩后得到的赫夫曼编码数组(二进制)
        byte[] huffmanZipCodeBytes = zip(bytes, huffmanCodes);
        //5.将压缩后的字节数组返回
        return huffmanZipCodeBytes;
    }

    /**
     * @param bytes 接收字节数组
     * @return 返回的就是 List形式,形式 [Node[date=97 ,weight = 5], Node[date=32,weight = 9]…]
     */
    private static List<Node> getNodes(byte[] bytes) {

        //1.创建一个ArrayList
        ArrayList<Node> nodes = new ArrayList<Node>();

        //遍历bytes,统计 每一个byte出现的次数 -> map[key,value]
        //byte数据,integer出现的次数
        Map<Byte, Integer> counts = new HashMap<Byte, Integer>();
        for (byte b : bytes) {
            Integer count = counts.get(b);
            if (count == null) {//Map还没有这个字符数据
                counts.put(b, 1);
            } else {
                counts.put(b, count + 1);
            }
        }

        //把每个键值对转成一个Node对象,并加入到nodes集合中
        for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
            nodes.add(new Node(entry.getKey(), entry.getValue()));
        }
        return nodes;
    }

    //可以通过List 创建对应的赫夫曼树
    private static Node createHuffmanTree(List<Node> nodes) {
        while (nodes.size() > 1) {
            //排序,从小到大
            Collections.sort(nodes);
            //取出第一颗最小的二叉树
            Node leftNode = nodes.get(0);
            //取出第二颗最小的二叉树
            Node rightNode = nodes.get(1);
            //创建一颗新的二叉树,它的根节点没有date
            Node parent = new Node(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;

            //将已经处理的两颗二叉树从nodes移除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //将新的二叉树加入到nodes
            nodes.add(parent);
        }
        //最终剩一个元素,返回的结点就是这棵HuffmanTree的根节点
        return nodes.get(0);
    }

    //前序遍历的方法
    private static void preOrder(Node root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("赫夫曼树为空,不能遍历");
        }
    }


    //编写一个方法,将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码压缩过后的byte[]数组

    /**
     * @param bytes        这是原始的字符串对应的byte[] -> ilikejava
     * @param huffmanCodes 生成的赫夫曼编码map
     * @return 返回赫夫曼编码处理后的byte[]
     * 举例:  String content = "i like like like java do you like a java"; => byte[] contentBytes = content.getBytes();
     * 返回的是 字符串"1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
     * 对应的byte[] huffmanCodeBytes,即8位对应一个byte(由于最大权结点离根越近,最小权结点离根最远,所以都是8位)放入到huffmanCodeBytes中
     * huffmanCodeBytes[0] = 10101000(补码) => byte   [推导 10101000=> 10101000 - 1 => 10100111(反码) ->11011000]
     * huffmanCodeBytes[1] = -88
     */
    private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {

        //1.利用 huffmanCodes 将 bytes 转成赫夫曼编码对应的字符串
        StringBuilder stringBuilder = new StringBuilder();
        //遍历 bytes 数组
        for (byte b : bytes) {
            //把获取到的huffmanCodes的字符串,拼接成一个长字符串
            //这个stringBuilder就是1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
            stringBuilder.append(huffmanCodes.get(b));
        }
        //System.out.println("测试stringBuilder=" + stringBuilder.toString());

        /*
        将"1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
        转成byte[]
         */


        //统计返回 byte[] huffmanCodeBytes 长度
        //一句话搞定 int len = (stringBuilder.length() + 7) / 8
        int len;
        if (stringBuilder.length() % 8 == 0) {
            len = stringBuilder.length() / 8;
        } else {
            len = stringBuilder.length() / 8 + 1;
        }
        //创建存储压缩后的 byte数组
        byte[] hmCodeBytes = new byte[len];//17
        int index = 0;//记录是第几个byte
        for (int i = 0; i < stringBuilder.length(); i += 8) {//因为每8位对应一个byte,所以步长设为8
            String strByte;
            //索引越界问题
            if (i + 8 > stringBuilder.length()) {//最后一位不够8位
                strByte = stringBuilder.substring(i);
            } else {
                strByte = stringBuilder.substring(i, i + 8);
            }


            //将strByte转成一个byte,放入到by中
            hmCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
            index++;
        }
        //当for循环过后,hmCodeBytes数组长度为8,每个索引(最后一个?)有8个字节
        //也就得到了赫夫曼树编码所对应的字节数组
        return hmCodeBytes;
    }

    /*
    生成赫夫曼树对应的赫夫曼编码
    思路:
    1.将赫夫曼编码表存放在 Map<Byte,String>中
    形式: 32-> =01,97->a=100,100->d=11000...
     */

    static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();
    //2.在生成赫夫曼编码表时,需要去拼接路径,定义一个StringBuilder 存储某个叶子结点的路径
    static StringBuilder stringBuilder = new StringBuilder();


    //为了调用方便,我们重载getCodes方法
    private static Map<Byte, String> getCodes(Node root) {
        if (root == null) {
            return null;
        }
        //处理root的左子树
        getCodes(root.left, "0", stringBuilder);
        //处理root的右子树
        getCodes(root.right, "1", stringBuilder);
        return huffmanCodes;
    }

    /**
     * 功能:将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入huffmanCodes集合中
     *
     * @param node          传入结点
     * @param code          路径的值:左子节点是0,右子结点是1
     * @param stringBuilder 是用于拼接路径(字符串)
     */
    private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //将code加入到stringBuilder2
        stringBuilder2.append(code);
        if (node != null) {//如果node == null 不处理
            //判断当前node是叶子结点还是非叶子结点
            if (node.date == null) {//说明它是非叶子非叶子结点
                //递归处理
                //向左递归
                getCodes(node.left, "0", stringBuilder2);
                //向右递归
                getCodes(node.right, "1", stringBuilder2);
            } else {//说明是一个叶子结点
                //就表示找到了某个叶子结点的最后,放入huffmanCodes中
                huffmanCodes.put(node.date, stringBuilder2.toString());
            }
        }
    }
}

//创建Node,带数据和权值
class Node implements Comparable<Node> {
    Byte date;// 存放数据本身,本身'a' => 97 ' ' => 32
    int weight;//权值,表示该字符出现次数
    Node left;
    Node right;

    public Node(Byte date, int weight) {
        this.date = date;
        this.weight = weight;
    }


    @Override
    public int compareTo(Node o) {
        //代表从小到大排序
        return this.weight - o.weight;
    }


    @Override
    public String toString() {
        return "Node{" +
                "date=" + date +
                ", weight=" + weight +
                '}';
    }

    //前序遍历
    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }
}

赫夫曼编码的注意事项

  • 如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化, 比如视频、pptx等等文件 [举例压一个.pptx]。
  • 赫夫曼编码是按字节来处理的,因此可以处理所有的文件(二进制文件、文本文件) [举例压一个.xml文件]。
  • 如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显。

在这里插入图片描述

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值