(八)二叉树—赫夫曼编码

1、基本介绍

 如下:

2、应用实例

 创建赫夫曼树

 生成赫夫曼编码和赫夫曼编码后的数据

使用赫夫曼编码解码

package tree;

import java.util.*;
/*
   实现步骤:
        ①  将对应字符串转为字节数组(105, 32, 108, 105, 107, 101, 32, 108, 105, 107......)
        ②  将字节数组转为类型为Node的List集合(Node{data=32, weight=9}, Node{data=97, weight=5}......)
        ③  根据List集合构建哈夫曼树(Node{data=null, weight=40}Node{data=null, weight=17}Node{data=null, weight=8}....)
        ④  通过哈夫曼树产生每个字符对应的哈夫曼编码(97(a) -> 100、100(d) -> 11000......)
        ⑤  根据哈夫曼编码集合,将原字节数组的每个字符拼接成由0、1组成的哈夫曼编码字符串(10101000......)
        ⑥  根据⑥生成的哈夫曼编码字符串,将其按8位进行切割,再转成十进制,放到字节数组中并返回(-88, -65, -56, -65, -56, -65, -55......)
        ⑦  根据⑦返回的经压缩的字节数组,可结合哈夫曼编码表huffmanCodes进行解压,解压后的字节数组即为原字符串(105, 32, 108, 105.....)
 */


public class HuffmanCodeDemo {
    //  下面两个东西用于生成对应的哈夫曼编码时使用(173行):
    public static Map<Byte, String> huffmanCode = new HashMap<>();    // 用于暂存存放哈夫曼编码
    public static StringBuilder stringBuilder = new StringBuilder();  // 用于暂存并拼接路径上的0、1个数

    public static void main(String[] args) {

        String sourceStr = "i like like like java do you like a java";

        //  1、将字符串 转为 字节数组
        byte[] strBytes = sourceStr.getBytes(); // 此时有40个字符
        System.out.println(Arrays.toString(strBytes));

        //  2、将字节数组 转为 类型为Node的List集合
        List<HNode> nodes = getNodes(strBytes);
        System.out.println(nodes);

        //  3、根据List集合构建哈夫曼树
        HuffmanTree huffmantree = createHuffmanTree(nodes);
        huffmantree.preOrderTraverse();

        //  4、根据哈夫曼树,生成对应的哈夫曼编码
        Map<Byte, String> huffmanCodes = getCodes(huffmantree.root, "", stringBuilder);// 根节点没有路径,设置为空
        for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
            System.out.print(entry.getKey() + " -> " + entry.getValue());
        }

        //  5、根据哈夫曼编码表,将原字节数组压缩,并返回压缩后的以补码形式表示的哈夫曼编码字节数组
        byte[] huffmanCodesBytes = encode(strBytes, huffmanCodes); //  此时有17个字符
        System.out.println(Arrays.toString(huffmanCodesBytes));

        //  6、根据哈夫曼编码表,对哈夫曼编码字节数组进行解压,并返回压解压后的原字符串
        byte[] targetStr = decode(huffmanCodesBytes, huffmanCodes);
        System.out.println(new String(targetStr));
    }

    //  将被压缩字节数组的每一位字符转为0、1字符串
    public static String toBinaryString(byte b, boolean flag) {
        /*
                个人理解:
                    1、遍历数组每一位时,对负数需要截取后8位,同时还需要考虑对字节数组中的正数进行高位补齐
                    2、但当遍历到字节数组最后一位时:
                                2、1.如果是正数,则无需补齐
                                2、2.如果是负数,则需要截取后八位

         */
        int temp = b;       // 将其转为int类型,用于获取对应的二进制字符串
        String str = null;  // 存放二进制字符串
        if (flag) {         // 为true:表示该字符为字节数组的最后一位,如果是正数,表示该字符无需高位补齐。如果是负数,则需要进行截取
            if (temp < 0) {
                str = Integer.toBinaryString(temp);
                return str.substring(str.length() - 8);  // 截取后8位
            } else {
                return Integer.toBinaryString(temp);
            }
        } else {
            temp |= 256;    // 为false,表该字符不是字节数组的最后一位
            str = Integer.toBinaryString(temp);
            return str.substring(str.length() - 8);  // 截取后8位
        }
    }

    private static byte[] decode(byte[] huffmanCodesBytes, Map<Byte, String> huffmanCodes) {
        //  1、将哈夫曼编码字节数组huffmanCodesBytes的每一个字符(-88, -65, -56, -65, -56)拼接成二进制字符串(10101000......)
        StringBuilder stringBuilder = new StringBuilder(); // 用于拼接二进制字符串
        for (int i = 0; i < huffmanCodesBytes.length; i++) {
            boolean flag = (i == huffmanCodesBytes.length - 1); // 字节数组最后一位需特殊处理,要考虑其是否满八位的情况
            stringBuilder.append(toBinaryString(huffmanCodesBytes[i], flag)); // 逐个读取字符,从而获取到对应的二进制字符串
        }

        System.out.println(stringBuilder.toString());

        //  2、按照哈夫曼编码,对二进制字符串进行解码
        //  因为要查字符串多个二进制位 对应 的字符,而原哈夫曼编码集合形式是<Byte,String>,所以需要将哈夫曼编码表进行反转
        Map<String, Byte> temp = new HashMap<>();
        for (Map.Entry entry : huffmanCodes.entrySet()) {
            temp.put((String) entry.getValue(), (Byte)entry.getKey());
        }

       List<Byte> ch = new ArrayList<>();
        //  3、遍历二进制字符串,从而获取到对应的字符
        for (int i = 0; i < stringBuilder.length(); ) {
            //  通过循环不断地截取对应的二进制个数,判断temp集合中是否有该哈夫曼编码对应的字符
            int count = 1;
            boolean flag = true;
            Byte b = null;
            while (flag) {
                String key = stringBuilder.substring(i, i + count); // 一个一个二进制位匹配
                b = temp.get(key);    // 判断key这个哈夫曼编码在temp集合中是否存在
                if (b == null) {
                    count++;          // 如果不存在,则遍历到下一个二进制位
                } else {
                    flag = false;     // 如果存在,则已经获取到一个哈夫曼编码对应的字符,则退出循环
                }
            }
           ch.add(b);
            i += count; //  i移动到count的位置,寻找下一个哈夫曼编码对应的字符
        }

        //  将List集合中的数据放到字节数组,并返回
        byte[] bytes = new byte[ch.size()];
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = ch.get(i);
        }
        return bytes;
    }


    /*
        1、根据哈夫曼编码集合huffmanCodes的哈夫曼编码,将原始的字节数组strBytes每一个字符拼接成一个由0、1组成的字符串
        2、然后将字符串按8位进行分割,再转成十进制,返回byte类型的数组
     */
    private static byte[] encode(byte[] strBytes, Map<Byte, String> huffmanCodes) {
        StringBuilder stringBuilder = new StringBuilder(); // 用于拼接字节数组每一个字符所对应的哈夫曼编码
        int len = 0;    //  统计哈夫曼编码字节数组的长度
        int index = 0;  //  作为哈夫曼编码字节数组的下标,用于存放1个字节对应的数据
        String temp;    //  暂存所截取的二进制字符串

        //  1、将字节数组转换成由0、1组成的哈夫曼编码字符串
        for (byte b : strBytes) {
            //  获取字节数组的每一个字符,然后去哈夫曼编码集合中,获取该字符对应的哈夫曼编码(0、1字符串)
            stringBuilder.append(huffmanCodes.get(b));
        }

        System.out.println(stringBuilder.toString());

        //  2、将哈夫曼编码字符串中的0、1按8位进行分割
        if (stringBuilder.length() % 8 == 0) {  // 判断该哈夫曼编码字符串可以组成多少个十进制数
            len = stringBuilder.length() / 8;
        } else {
            len = stringBuilder.length() / 8 + 1;
        }
        byte[] huffmanCodeBytes = new byte[len];   // 存放哈夫曼编码字符串对应的以补码形式表示的字符
        for (int i = 0; i < stringBuilder.length(); ) {
            if (i + 8 > stringBuilder.length()) {  // 考虑到字符串末尾时,不足八位的情况
                temp = stringBuilder.substring(i); // 则从当前位开始截取,直到字符串末尾
                huffmanCodeBytes[index] = (byte) Integer.parseInt(temp, 2); // 将其转为二进制的形式
                break;
            }
            temp = stringBuilder.substring(i, i + 8);
            huffmanCodeBytes[index] = (byte)Integer.parseInt(temp, 2);      // 将其转为二进制的形式
            i+=8;   //  每8位存放一个字符
            index++;
        }

        return huffmanCodeBytes; //  将对应的哈夫曼编码字节数组返回(全为补码)
    }

    private static Map<Byte, String> getCodes(HNode node, String code, StringBuilder stringBuilder) {
        //  用于拼接每个叶子节点上的0、1
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //  将路径上的0、1拼接到StringBuilder
        stringBuilder2.append(code);
        //  处理不为null的节点
        if (node != null) {
            //  为null时,表明该节点是非叶子节点,则递归统计根节点到该节点路径上0、1的个数
            if (node.data == null) {
                getCodes(node.left, "0", stringBuilder2);
                getCodes(node.right, "1", stringBuilder2);
            } else {
                // 不为null时,则表明该节点是叶子节点,这时将根节点到当前叶子结点上的哈夫曼编码添加到map集合中
                huffmanCode.put(node.data, stringBuilder2.toString());
            }
        }
        return huffmanCode;
    }

    private static HuffmanTree createHuffmanTree(List<HNode> nodes) {
        while (nodes.size() > 1) {
            //  1、对List<Node>集合进行排序
            Collections.sort(nodes);

            //  2、取出两棵权值最小的二叉树
            HNode node1 = nodes.get(0);
            HNode node2 = nodes.get(1);

            //  3、构建一个棵新的二叉树,并重新添加到List集合
            HNode root = new HNode(null, node1.weight + node2.weight);
            root.left = node1;
            root.right = node2;
            nodes.add(root);

            //  4、将原来那两棵二叉树从List集合中移除
            nodes.remove(node1);
            nodes.remove(node2);
        }

        //  5、当List集合仅剩一个元素时,该元素即为哈夫曼树的根节点
        return new HuffmanTree(nodes.get(0));
    }

    private static List<HNode> getNodes(byte[] strBytes) {
        List<HNode> nodes = new ArrayList<>();     // 用于存放Node节点的List集合
        Map<Byte, Integer> map = new HashMap<>();  // 用于暂存每个字符及出现次数的映射关系

        for (byte b : strBytes) {
            Integer count = map.get(b);            // 判断当前字符在map中是否有值
            if (Objects.isNull(count)) {                   // 返回结果为null,表示当前字符还未放入map集合
                map.put(b, 1);                     // 将当前字符放入map集合,key为字符本身,value为字符出现的次数
            } else {
                map.put(b, count + 1);             // 不为null,表明map集合中已有该字符,则将其出现次数加1并再次放到map集合中
            }
        }

        //  将map中每一个[key为字符,value为字符出现个数]的元素 转化为Node对象 并放入到List集合中
        for (Map.Entry entry : map.entrySet()) {
            nodes.add(new HNode((Byte) entry.getKey(), (Integer) entry.getValue()));
        }

        return nodes;
    }

}
class HuffmanTree {
    public HNode root;

    public HuffmanTree(HNode root) {
        this.root = root;
    }

    //  前序遍历
    public void preOrderTraverse() {
        if (this.root == null) {
            return;
        } else {
            System.out.print("前序遍历:");
            this.root.preOrderTraverse();
        }
    }
}

class HNode implements Comparable<HNode> { // 对HNode对象进行集合排序
    public Byte data;  // 字符对应的ASCII
    public Integer weight; // 权值
    public HNode left;
    public HNode right;

    public HNode(Byte data, Integer weight) {
        this.data = data;
        this.weight = weight;
        this.left = null;
        this.right = null;
    }


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

    @Override
    public int compareTo(HNode o) {
        //  升序排序
        return this.weight - o.weight;
    }


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

}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

来得晚一些也行

观众老爷,请赏~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值