数据结构(九)---赫夫曼编码

数据结构(九)---赫夫曼编码

创建节点类

//创建Node节点类,带权值
//它可以使继承他的类进行比较大小,只需要调用实现类的compareTo方法即可
class Node implements Comparable<Node>{
    public Byte data;//存放数据本身,字符a转化成ASCII码97,空格就是32
    public int weight;//代表权值,表示字符出现的次数
    Node left;//指向左子节点
    Node right;//指向右子节点

    //构造器,用来初始化数据
    public Node(Byte data, int weight) {
        this.data = data;
        this.weight = weight;
    }

    //通过调用Comparable接口中的compareTo方法,可以比较Node节点中weight权值的大小
    @Override
    public int compareTo(Node node) {
        //要比较的顺序是“从小到大”
        //如果返回值=0,则this.weight==node.weight
        //如果返回值>0,则this.weight>node.weight
        return this.weight-node.weight;
    }

    //用来输出
    @Override
    public String toString() {
        return "Node{" +
                "data=" + data +
                ", 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) {
        //1-给个字符串
        String str="i like like like java do you like a java";
        //2-把字符串string的字符取出来,放在byte数组里面(这些字符还要变成ASCII码,然后放进Node节点,Node节点放进List)
        byte[] bytes = str.getBytes();
        //在压缩前,看看长度,长度为40
        System.out.println("压缩前byte数组的长度为:"+bytes.length);

        /*//3-把byte数组里的字符和出现次数,放进map存储,然后用map数据创建Node节点,再把Node加到List
        List<Node> nodesList=getNode(bytes);
        System.out.println("处理后的Node节点的List:"+nodesList);

        //4-根据List来创建一个赫夫曼树,返回值是树的根节点root,有了根节点,顺着遍历就得到整个树了
        System.out.println("赫夫曼树为:");
        Node huffmanTreeRoot=createHuffmanTree(nodesList);
        //树的根节点有了,要想得到整个树,用一个前序遍历的方法就行了,把root根节点传进去即可
        System.out.println("赫夫曼树的前序遍历:");
        huffmanTreeRoot.preOrder();

        //5-遍历赫夫曼树,生成赫夫曼的编码
        //把根节点传进去,最后得到的是string类型的路径编码huffmanCode
        // getCodes(huffmanTreeRoot,"",stringBuilder);
        //因为下面写了getCodes方法的重载,只需要给方法传一个根节点root,其他不用管,这样更方便
        Map<Byte,String> huffmanCodes=getCodes(huffmanTreeRoot);
        System.out.println("生成的赫夫曼编码表:"+huffmanCodes);

        //6-得到赫夫曼编码之后的数据,也就是把所有字符的编码路径拼接到一起,进行压缩
        byte[] huffmanCodeBytes=zip(bytes,huffmanCodes);
        //字符串Array相关的调用方法也要深入学习掌握
        System.out.println("huffmanCodeBytes="+ Arrays.toString(huffmanCodeBytes));*/

        byte[] huffmanCodesBytes=huffmanZip(bytes);
        System.out.println("压缩后的结果:"+Arrays.toString(huffmanCodesBytes));
        System.out.println("压缩后byte数组的长度为:"+huffmanCodesBytes.length);

        //测试byteToBitString方法
        // System.out.println(byteToBitString((byte)1));
        decode(huffmanCode, huffmanCodesBytes);

    }

    //完成数据的解压
    //思路:
    //1-把压缩后的字节huffmanCodesBytes[-88,-65,...]先转换成赫夫曼编码对应的二进制的字符串“1010100...”
    //2-赫夫曼编码对应的二进制的字符串“1010100...”转成字符串“i like like...”
    //

    //编写一个方法,完成对压缩数据的解码
    /**huffmanCodes 赫夫曼编码
     * huffmanBytes 赫夫曼编码得到的字节数组
     * 返回的是原来字符串对应的数组
     * @MethodName: decode
     * @Author: AllenSun
     * @Date: 2019/11/11 22:40
     */
    private static byte[] decode(Map<Byte,String> huffmanCodes,byte[] huffmanBytes){
        //1-先得到huffmanBytes对应的二进制的字符串“1010100...”,需要StringBuilder拼接
        StringBuilder stringBuilder1 = new StringBuilder();
        //把byte数组转成二进制的字符串
        for (int i = 0; i < huffmanBytes.length; i++) {
            byte b=huffmanBytes[i];
            //调用前面写过的方法,先判断是不是最后一个字节,是的话就不用补高位
            boolean flag=(i==huffmanBytes.length-1);
            stringBuilder1.append(byteToBitString(!flag,b));
        }
        System.out.println("赫夫曼字节数组对应的二进制字符串为:"+stringBuilder1.toString());
        return null;
    }


    /**把一个byte转成一个二进制的字符串,涉及到java基础里的原码、反码、补码
     * @MethodName: byteToBitString
     * @Author: AllenSun
     * @Date: 2019/11/11 22:38
     */
    private static String byteToBitString(boolean flag, byte b){
        //使用变量保存b
        int temp=b;//把b转换成int
        //如果是正数,我们还存在补高位,flag是true就补高位,是false就不用补高位。如果是最后一个字节,也不需要补高位
        if(flag){
            temp |=256;//按位与256
        }
        //toBinaryString此方法返回int变量的二进制表示的字符串
        String str=Integer.toBinaryString(temp);//返回的是temp对应的二进制的补码
        if(flag){
            return str.substring(str.length()-8);
        } else {
            return str;
        }
    }


    /**使用一个方法,把前面的方法封装起来,便于我们的调用
     * 传入一个原始的字符串对应的字节数组
     * 返回一个经过赫夫曼编码处理后的字节数组(也就是返回一个“压缩后的”数组)
     * @MethodName: huffmanZip
     * @Author: AllenSun
     * @Date: 2019/11/9 18:06
     */
    private static byte[] huffmanZip(byte[] bytes){
        List<Node> nodesList=getNode(bytes);
        //4-根据List来创建一个赫夫曼树,返回值是树的根节点root,有了根节点,顺着遍历就得到整个树了
        Node huffmanTreeRoot=createHuffmanTree(nodesList);
        //5-遍历赫夫曼树,生成赫夫曼的编码
        //把根节点传进去,最后得到的是string类型的路径编码huffmanCode
        // getCodes(huffmanTreeRoot,"",stringBuilder);
        //因为下面写了getCodes方法的重载,只需要给方法传一个根节点root,其他不用管,这样更方便
        Map<Byte,String> huffmanCodes=getCodes(huffmanTreeRoot);
        //6-得到赫夫曼编码之后的数据,也就是把所有字符的编码路径拼接到一起,进行压缩
        byte[] huffmanCodeBytes=zip(bytes,huffmanCodes);
        return huffmanCodeBytes;
    }


    /**写一个“创建一个Node节点的链表List”方法
     * 1-接收一个字节数组,统计每个字节出现的次数
     * 2-然后把字符的值和出现的次数存到map里去
     * 3-再遍历map,把遍历出来的键值对用来初始化创建Node节点
     * 4-最后把Node节点加到List里去
     * @MethodName: getNode
     * @Author: AllenSun
     * @Date: 2019/11/9 15:00
     */
    //从byte数组里把Node节点取出来,然后放到List里(为什么要放到List里去:因为数组取出插入数据不如链表方便)
    private static List<Node> getNode(byte[] bytes){
        //1-创建一个ArrayList
        ArrayList<Node> nodesList=new ArrayList<Node>();
        //2-遍历bytes,统计存储每个byte字符出现的次数--存放在map里,Byte就是数据,integer就是出现的次数
        //所有说,当你要一个数据时,要先想好,用什么数据结构来存放它,并且对数据结构可以调用的方法了如指掌
        Map<Byte,Integer> counts=new HashMap<>();
        for (byte b : bytes) {//遍历所有的bytes
            Integer count=counts.get(b);//把byte的字符取出来放进Map
            //如果count一开始就是空的,说明是第一次取,计数为1
            if(count==null){
                counts.put(b,1);//第一次,把data值和weight值放进map
            } else {
                counts.put(b,count+1);//若不是第一次,
            }
        }
        //3-把字符的ASCII码放进Node的data,遍历出来的次数map放进权值weight,这样就创建了一个节点,把节点Node对象,放到List里去
        //上面已经有了Map的键值对,接下来就是把键值对(byte,count)放到对应的Node(data,value)里去
        //怎么把字符转换成ASCII码???
        //首先遍历一下Map,entrySet就是用来遍历Map的方法,直接调用
        for (Map.Entry<Byte,Integer> entry:counts.entrySet()){
            //把遍历出来的键值对放到Node里,在把Node加到节点的List里去
            nodesList.add(new Node(entry.getKey(),entry.getValue()));
        }

        //4-最后返回一个我们需要的nodesList
        return nodesList;
    }


    /**根据已有的节点List,把这些节点Node取出来,创建成一个赫夫曼树
     * @MethodName: createHuffmanTree
     * @Author: AllenSun
     * @Date: 2019/11/9 15:14
     */
    //创建赫夫曼的方法
    private static Node createHuffmanTree(List<Node> nodes) {
        //循环创建
        while(nodes.size()>1){
            //1-首先把数组排序,从小到大,直接使用一个集合
            Collections.sort(nodes);
            System.out.println("nodes=" + nodes);

            //2-取出根节点权值最小的两棵二叉树
            //(1)取出权值最小的节点(二叉树)
            Node leftNode = nodes.get(0);
            //(2)取出权值第二小的节点(二叉树)
            Node rightNode = nodes.get(1);
            //(3)创建一棵新的二叉树,根节点只有权值,没有data
            Node parent = new Node(null,leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;
            //(4)从ArrayList删除处理过的二叉树,不然下次没法从List最前面两位取节点
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //(5)把parent加入nodes,加入以后重新排序sort,就可以继续了
            nodes.add(parent);
        }
        //nodesList最后剩下的节点,也就是权值最大的,就是赫夫曼树的根节点root
        return nodes.get(0);


    }

    /**赫夫曼树的前序遍历
     * 前面创建树的时候,已经得到了树的根节点,但是还没有得到赫夫曼树的全部节点
     * 前序遍历根据根节点,对赫夫曼树进行前序遍历
     * @MethodName: preOrder
     * @Author: AllenSun
     * @Date: 2019/11/9 15:31
     */
    //前序遍历赫夫曼树的方法,给我一个根节点,我帮你遍历出来整棵树
    private static void preOrder(Node root){
        if(root!=null){
            root.preOrder();
        } else {
            System.out.println("赫夫曼树为空,没有数据");
        }
    }


    /**已经得到赫夫曼树了,生成赫夫曼编码
     * 思路:
     * 1-先想好用什么数据结构比较合适,这里用Map<Byte,String>的键值对存放赫夫曼编码表,key是ascii码,value为路径编码
     * 例如:32-01,97-100
     * 2-在遍历赫夫曼树的时候,会得到0和1来拼接路径,用字符串String来保存某个叶子节点路径编码
     * 3-
     * @MethodName: getCodes
     * @Author: AllenSun
     * @Date: 2019/11/9 15:58
     */
     //1-map用来存路径编码
     static Map<Byte,String> huffmanCode=new HashMap<>();
     //2-stringBuilder用来把0和1拼接成路径编码,然后交给map
     //StringBuilder类实现字符串的拼接append功能,不需要用“+”来拼接了
     static StringBuilder stringBuilder=new StringBuilder();
     //把传入的node节点的所有叶子结点的黑曼编码得到,并放到huffmanCode这个集合里去
     //node就是先从根节点root开始,code代表路径值,左边就是0,右边就是1,然后把code拼接到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是空的,就不处理
             //判断当前node是不是叶子结点,如果是叶子结点就可以结束回去了,不是叶子节点就继续往下走
             if(node.data==null){
                 //如果data不是空的就是叶子节点,因为创建赫夫曼树时设定parent节点的data为null,只有权值weight
                 //如果data为空,那就是非叶子结点,List里取出来的有data的节点都在叶子结点上
                 //所以,如果data为null,那就要继续递归下去
                 getCodes(node.left,"0",stringBuilder2);//向左走,把0拼接到stringBuilder2
                 getCodes(node.right,"1",stringBuilder2);//向右走,把1拼接到stringBuilder2
                 //这样,结束的时候,每个node都有自己的stringBuilder2
             } else {
                 //说明这是一个叶子节点,到头了,拼接结束,把拼接得到的stringBuilder2编码路径放到huffmanCode里
                 huffmanCode.put(node.data,stringBuilder2.toString());
             }
         }
     }

    /**为了方便,重载getCodes方法,专门用来处理根节点的
     * 上面那个getCodes方法,是用来处理根节点下面的所有节点的
     * @MethodName: getCodes
     * @Author: AllenSun
     * @Date: 2019/11/9 16:53
     */
    private static Map<Byte,String> getCodes(Node root){
        //如果根节点是空的,那就是空树,什么都不返回
        if(root==null){
            return null;
        }
        //如果根节点不是空的,那就递归调用上面的getCodes方法,分别处理左子树和右子树
        getCodes(root.left,"0",stringBuilder);
        getCodes(root.right,"1",stringBuilder);
        return huffmanCode;
    }

    /**编写一个方法,把字符串对应的byte数组,通过生成的赫夫曼编码表huffmanCodes,返回一个赫夫曼编码压缩后的byte
     * bytes 就是原始的字符串对应的byte数组
     * huffmanCodes 生成的赫夫曼编码map
     * 返回的是赫夫曼变啊处理后的byte数组,也就是字符串“”
     * 每8位对应一个byte,放入huffmanCodeBytes,
     * huffmanCodeBytes[0]=10101000(补码)=》一个byte
     * @MethodName: zip
     * @Author: AllenSun
     * @Date: 2019/11/9 17:09
     */
    private static byte[] zip(byte[] bytes, Map<Byte,String> huffmanCodes){
        //1-使用huffmanCodes把bytes转成赫夫曼编码对应的字符串
        StringBuilder stringBuilder=new StringBuilder();
        //遍历bytes数组
        for (byte b : bytes) {
            stringBuilder.append(huffmanCodes.get(b));
        }
        //把“1010100...”转成byte[]
        //统计返回的byte[] huffmanCodeBytes长度length有多大
        // 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[] huffmanCodeBytes=new byte[len];
        int index=0;//记录是第几个byte
        for (int i = 0; i < stringBuilder.length(); i+=8) {//因为每8位对应一个byte
            String strByte;
            if(i+8>stringBuilder.length()){
                //说明到最后剩的编码不够8位了
                strByte=stringBuilder.substring(i);
            }else {
                strByte=stringBuilder.substring(i,i+8);
            }
            //把strByte转成一个byte,放到huffmanCodeBytes数组里去
            huffmanCodeBytes[index]=(byte)Integer.parseInt(strByte,2);
            index++;
        }
        return huffmanCodeBytes;
    }

}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值