赫夫曼树分析

什么是赫夫曼树
就是wpl: 就是树的带权路径长度
什么是路劲长度
什么是结点的权, 结点的带权路径长度
在这里插入图片描述
int[] arr = {13,7,8,3,29,6,1};
将这个arr变成一个赫夫曼树
就应该是这样的
在这里插入图片描述

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class HefumanshuTest {
    
    
    public static void main(String[] args) {
    
    
        int[] arr  = {
    
    13,7,8,3,29,6,1};
        Nodes anHefumanshuByarr = getAnHefumanshuByarr(arr);
        anHefumanshuByarr.qianxu();

    }
    //创建算法, 根据arr创建一个赫夫曼树
    public static Nodes getAnHefumanshuByarr(int[] arr){
    
    
        //先将int的数组变成树节点的ArrayList
        List<Nodes> nodes = new ArrayList<Nodes>();
        for (int value: arr){
    
    
            nodes.add(new Nodes(value));
        }


        /*//现在将节点的ArrayList进行排序
        Collections.sort(nodes);

        //先取出权值最小的两颗二叉树
        Nodes left = nodes.get(0);
        Nodes right = nodes.get(1);
        //合并成一颗新的二叉树, 并且父节点的权值就是他们的权值的和
        Nodes parent = new Nodes(left.value+right.value);
        parent.left = left;
        parent.right = right;


        //从树节点的ArrayList中删除已经处理的节点
        nodes.remove(left);
        nodes.remove(right);
        //再将新的树的父节点存放进ArrayList, 并从新排序
        nodes.add(parent);
        Collections.sort(nodes);*/

        //将以上步骤整合就是
        while(nodes.size() > 1){
    
    
            Collections.sort(nodes);

            //先取出权值最小的两颗二叉树
            Nodes left = nodes.get(0);
            Nodes right = nodes.get(1);
            //合并成一颗新的二叉树, 并且父节点的权值就是他们的权值的和
            Nodes parent = new Nodes(left.value+right.value);
            parent.left = left;
            parent.right = right;


            //从树节点的ArrayList中删除已经处理的节点
            nodes.remove(left);
            nodes.remove(right);
            //再将新的树的父节点存放进ArrayList, 并从新排序
            nodes.add(parent);
        }

        //最后返回ArrayList的最后一个节点, 及是赫夫曼树的root
        return nodes.get(0);

    }

}

//创建树节点
class Nodes implements Comparable<Nodes>{
    
    
    int value ; //权值
    Nodes left;
    Nodes right;

    public Nodes(int value) {
    
    
        this.value = value;
    }

    @Override
    public String toString() {
    
    
        return "Nodes{" +
                "value=" + value +
                '}';
    }

    //让树节点去继承comparable接口, 继承compareTo方法, 实现从小到大或者从大到小的排序
    @Override
    public int compareTo(Nodes o) {
    
    
        //表示从小到大排序 , 如是从大到小就是负的形式
        return this.value-o.value;
    }

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

前序遍历结果

Nodes{
    
    value=67}
Nodes{
    
    value=29}
Nodes{
    
    value=38}
Nodes{
    
    value=15}
Nodes{
    
    value=7}
Nodes{
    
    value=8}
Nodes{
    
    value=23}
Nodes{
    
    value=10}
Nodes{
    
    value=4}
Nodes{
    
    value=1}
Nodes{
    
    value=3}
Nodes{
    
    value=6}
Nodes{
    
    value=13}

什么是赫夫曼编码
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
但是这种编码方式有二义性
有没有一种编码方式, 既是变长编码(可以减短传输过程中的字长), 又是前缀编码(没有二义性).
就是赫夫曼编码
思路:
首先将字符对应的个数变成一个数列, 这个数列在变成一个赫夫曼树, 根据这个赫夫曼树给字符规定编码, 从root开始左为0, 右为1; 这样每一个字符(叶子结点)根据这个赫夫曼树, 都神奇的有自己的编码, 并且是前缀编码
需要注意的是, 树有可能不一样, 编码也就不完全一样, 但是结果是一样的, 因为只要满足wpl最小, 最后的字长还是一样的

import java.util.*;

public class HeFuManBianMaTest {
    
    
    public static void main(String[] args) {
    
    
        //要传输的字符串
        String str = "i like you like you you";
        //将字符串转化为字节数组
        byte[] strarr = str.getBytes();
        System.out.println("原数据的字节数组是"+Arrays.toString(strarr));
        //打印一次这个字节数组的长度
        System.out.println("原数据的字节数组长度是"+strarr.length);  //23

       /* //1
        //得到树节点的ArrayList
        List<CodeNode> shujiedianArrayList = getShujiedianArrayList(strarr);
        System.out.println(shujiedianArrayList);

        //2
        //得到赫夫曼树的root并遍历
        CodeNode heFuManShuroot = getHeFuManShuByList(shujiedianArrayList);
        heFuManShuroot.qianxu();

        //3
        //得到编码表
        getbianmabiao(heFuManShuroot, "", stringBuilder);
        System.out.println("生成的赫夫曼编码表"+bianmabiao);

        //4
        //得到新的由编码表转换的并且压缩后的字节数组
        byte[] zipstrarr = zip(strarr, bianmabiao);
        System.out.println(zipstrarr.length);  //9
        //看看新的字节数组
        System.out.println(Arrays.toString(zipstrarr));
        //[-126, -97, -59, -52, 83, -8, -71, -105, 3]解释一下为什么是这样子, 当时先是String变成二进制的int, 再强转为byte,我们看到的是十进制的源码形式, 但是底层放的依然是二进制的形式
*/

        byte[] zipByte = getZipByte(strarr);
        System.out.println("用于传输的字节数组为"+Arrays.toString(zipByte)+"长度是"+zipByte.length);


        //解码操作
        /*System.out.println(byteToBitString(false,(byte)3));    //这个代码是有问题的呀*/
        byte[] decode = decode(bianmabiao, zipByte);
        System.out.println("解压后的字节数组是"+Arrays.toString(decode));
        System.out.println("解压后的字节数组的长度是"+decode.length);

    }

    //1
    //写一个方法, 传入一个字节数组得到一个树节点的ArrayList
    public static List<CodeNode> getShujiedianArrayList(byte[] strarr){
    
    
        //
        ArrayList<CodeNode> codeNodeList = new ArrayList<CodeNode>();
        //统计传进来的字节数组中的每一个节点的出现次数
        Map<Byte, Integer> tongji = new HashMap<>();
        for (byte data : strarr){
    
    
            //先判断map中有没有这个data
            Integer cishu = tongji.get(data);
            if (cishu == null){
    
     //没有这个data说明是头一次
                tongji.put(data, 1);  //设置quan为一
            }else{
    
    
                tongji.put(data, cishu+1);
            }
        }

        //接下来就是将每一个键值对变成树节点, 并加入ArrayList
        //遍历map
        for (Map.Entry<Byte, Integer> entry : tongji.entrySet()){
    
     //这个写法要记住
            codeNodeList.add(new CodeNode(entry.getValue(),entry.getKey()));
        }

        return codeNodeList;
    }

    //2
    //根据树节点的ArrayList得到赫夫曼树
    public static CodeNode getHeFuManShuByList(List<CodeNode> shujiedianArrayList){
    
    
        while(shujiedianArrayList.size() > 1){
    
    
            Collections.sort(shujiedianArrayList);

            //先取出权值最小的两颗二叉树
            CodeNode left = shujiedianArrayList.get(0);
            CodeNode right = shujiedianArrayList.get(1);
            //合并成一颗新的二叉树, 并且父节点的权值就是他们的权值的和
            CodeNode parent = new CodeNode(left.quan+right.quan, null);
            parent.left = left;
            parent.right = right;


            //从树节点的ArrayList中删除已经处理的节点
            shujiedianArrayList.remove(left);
            shujiedianArrayList.remove(right);
            //再将新的树的父节点存放进ArrayList, 并从新排序
            shujiedianArrayList.add(parent);
        }

        //最后返回ArrayList的最后一个节点, 及是赫夫曼树的root
        return shujiedianArrayList.get(0);
    }


    //3
    //生成赫夫曼树对应的编码表
    //思考: 应该用什么数据结构来存放编码表呢
    //Map<Byte, String>
    //String就是每个叶子结点(字符)对应的赫夫曼编码, 显然的是String不能一次性得到, 是需要拼接的
    //StringBuilder: 用来存储某个叶子结点的路径
    static Map<Byte, String> bianmabiao = new HashMap<Byte, String>();
    static StringBuilder stringBuilder = new StringBuilder();
    /**
     * 用于得到编码表
     * @param node 传入的节点
     * @param code 路径代码, 左就是0, 右就是1
     * @param stringBuilder 用于拼接路径
     */
    public static void getbianmabiao(CodeNode node, String code, StringBuilder stringBuilder){
    
    
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);  //临时容器
        stringBuilder2.append(code);  //用临时容器拼接code
        if (node != null){
    
      //如果code==null不处理
            //判断当前节点是不是叶子结点
            if (node.data == null){
    
     //非叶子结点
                //递归处理
                //向左
                getbianmabiao(node.left, "0", stringBuilder2);
                //向右
                getbianmabiao(node.right, "1", stringBuilder2);
            }else{
    
      //说明是叶子结点
                //就表示找到一个叶子结点
                bianmabiao.put(node.data, stringBuilder2.toString());
            }
        }
    }

    //4
    //将原本的字符串使用编码表转换成赫夫曼编码表对应的字符串, 并压缩成一个新的字节数组
    public static byte[] zip(byte[] bytes, Map<Byte, String> bianmabiao){
    
    
        //用于拼接的容器
        StringBuilder stringBuilder = new StringBuilder();
        for (byte b : bytes){
    
    
            stringBuilder.append(bianmabiao.get(b));
        }
        System.out.println("原来编码表的编码串是"+stringBuilder);
        //这样就得到了未压缩的根据编码表形成的源字符串的编码串
        //接下来就要压缩成一个新的字节数组, 就是八位一个字节
        int len;
        if (stringBuilder.length() % 8 == 0){
    
    
            //说明刚好是8的整数倍
            len = stringBuilder.length() / 8;
        }else{
    
    
            len = stringBuilder.length() /8 +1;
        }
        //这样一来这个len就是压缩后的字节数组的长度
        byte[] zipbytes = new byte[len];
        int index = 0;
        for (int i = 0; i < stringBuilder.length(); i+=8){
    
     //步长为8
            String str ;  //临时存放每一个字节
            if (i+8 > stringBuilder.length()){
    
    
                str = stringBuilder.substring(i); //直接取到最后
            }else{
    
    
                str = stringBuilder.substring(i, i+8); //例: 其实就是0~7的字长的一段, 再来就是8~15......
            }
            //然后将这个临时容器里的编码转成一个字节, 并放入新的字节数组中
            zipbytes[index] = (byte)Integer.parseInt(str, 2);  //二进制形式的int, 在强转为byte
            index ++;
        }
        return zipbytes;
    }

    //将以上四个步骤合成一个方法
    public static byte[] getZipByte(byte[] strarr){
    
    
        List<CodeNode> shujiedianArrayList = getShujiedianArrayList(strarr);
        CodeNode heFuManShuroot = getHeFuManShuByList(shujiedianArrayList);
        getbianmabiao(heFuManShuroot, "", stringBuilder);
        byte[] zipstrarr = zip(strarr, bianmabiao);
        return zipstrarr;
    }



    //解码操作
    /**
     * 传进一个字节, 输出它对应的二进制的字符串形式
     * @param flag 判断该字节是否需要补高位
     * @param b 要处理的字节
     * @return b对应的二进制的字符串形式
     */
    public static String byteToBitString(boolean flag , byte b){
    
    
        //使用int变量保存byte
        int temp = b;  //把字节数据转成int数据
        //如果不是最后一个字节, 我们要补高位    -------------这里解释不是最后一个字节为什么要补高位, 因为我们压缩的时候使用的是每八位编码变成一个字节数据来存储, 而最后一个字节有可能其实不满八位, 那也即是说
        //不是最有一个字节的话, 一定要补满八位, 最后一个字节就直接变成二进制就可以,
        if (flag){
    
    
            temp |= 256;  //按位与(相同0, 不同1), 256就是1 0000 0000;
        }
        String str = Integer.toBinaryString(temp); //把int变成二进制, 并且以字符串形式保存
        if (flag){
    
    
            //因为有与操作取后八位
            return str.substring(str.length()-8);
        }else{
    
    
            //直接返回
            return str;
        }
    }

    /**
     * //完成对哈夫曼树的解码
     *
     * @param huffmanCodes 哈夫曼编码表  map
     * @param huffmanBytes 经过哈夫曼编码得到后的字节数组[-88,....]
     * @return 就是原来字符串对应的数组
     */
    public static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
    
    
        //1。先得到huffmanByte对应的二进制字符串,形式1010100011.。。
        StringBuilder stringBuilder = new StringBuilder();
        //将byte数组转成二进制的字符串
        for (int i = 0; i < huffmanBytes.length; i++) {
    
    
            byte b = huffmanBytes[i];
            //判断是不是最后一个字节。这里是与前面的bytetostring里面的方法对应,我们判断如果是最后一个字符,我们只需将其拼接,无需将其进行补全。
            boolean flag = (i == huffmanBytes.length - 1);//为真,即不需要进行补位
            stringBuilder.append(byteToBitString(!flag, b));//!flag取反
        }
        System.out.println("解压后的编码表的编码串是"+stringBuilder.toString());
        //把字符串按照指定的哈夫曼编码进行解码
        //把哈弗曼编码进行调换,因为反向查询  a->97 = 100 -> a
        Map<String, Byte> map = new HashMap<>();
        //哈夫曼编码进行调换
        for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
    
    
            map.put(entry.getValue(), entry.getKey());
        }
        //System.out.println(map + "map");
        //创建要给集合.存放byte
        ArrayList<Byte> list = new ArrayList<>();
        //扫描stringbuilder,每一位一位的进行扫描,这里不能进行自加1,原因:i+count,
        //遍历的时候,每次走count,直到匹配到哈夫曼对应的字母时才i+count,此时,i移动i+count,不是i++
        for (int i = 0; i < stringBuilder.length(); ) {
    
    
            int count = 1;//小的计数器
            boolean flag = true;
            Byte b = null;

            while (flag) {
    
    
                //取出一个1或者0,因为是二进制吗,啊
                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;//i直接移动到count
        }
        //for循环结束后,list中就放入了所有的字符,,将list中的数据放入byte[]并返回‘
        byte[] b = new byte[list.size()];
        for (int i = 0; i < list.size(); i++) {
    
    
            b[i] = list.get(i);
        }
        return b;
    }

}





//树节点带权值和具体是什么字符
class CodeNode implements Comparable<CodeNode>{
    
    
    int quan; //出现的次数
    Byte data; //具体是什么字符
    CodeNode left;
    CodeNode right;

    public CodeNode(int quan, Byte data) {
    
    
        this.quan = quan;
        this.data = data;
    }

    //让树节点去继承comparable接口, 继承compareTo方法, 实现从小到大或者从大到小的排序
    @Override
    public int compareTo(CodeNode o) {
    
    
        //表示从小到大排序 , 如是从大到小就是负的形式
        return this.quan-o.quan;
    }

    @Override
    public String toString() {
    
    
        return "CodeNode{" +
                "权值=" + quan +
                ", 字符=" + data +
                '}';
    }

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

原数据的字节数组是[105, 32, 108, 105, 107, 101, 32, 121, 111, 117, 32, 108, 105, 107, 101, 32, 121, 111, 117, 32, 121, 111, 117]
原数据的字节数组长度是23
原来编码表的编码串是10000010100111111100010111001100010100111111100010111001100101110011
用于传输的字节数组为[-126, -97, -59, -52, 83, -8, -71, -105, 3]长度是9
解压后的编码表的编码串是100000101001111111000101110011000101001111111000101110011001011111
解压后的字节数组是[105, 32, 108, 105, 107, 101, 32, 121, 111, 117, 32, 108, 105, 107, 101, 32, 121, 111, 117, 32, 121, 107]
解压后的字节数组的长度是22

很明显这样的代码是有bug的呀

猜你喜欢

转载自blog.csdn.net/weixin_45032905/article/details/121431136