数据结构与算法-哈夫曼树

引言

        在计算机科学领域,数据结构与算法是构建高效软件系统的重要基石。其中,哈夫曼树(Huffman Tree)作为一种特殊的二叉树,以其在数据压缩领域的卓越表现而闻名。本文将深入探讨哈夫曼树的原理、构造过程以及实际应用场景。

一、什么是哈夫曼树?

        哈夫曼树,又称最优二叉树或最小带权路径长度树,是一种带权重的二叉树,由美国计算机科学家戴维·A·哈夫曼于1952年提出。它的特点是通过自底向上、结合的方式构建,使得树中每个叶子节点代表一个字符,且整棵树的加权路径长度(所有叶子节点到根节点的路径长度之和)最小。

二、哈夫曼树的构造过程

  1. 频率统计:首先统计给定字符集中的每个字符出现的频率,形成字符-频率对。

  2. 构建最小堆:将每个字符-频率对视为一个节点,根据频率大小构建一个最小堆,保证父节点的频率总是小于或等于其子节点。

  3. 节点合并:从最小堆中取出两个频率最小的节点,创建一个新的内部节点作为它们的父节点,其频率为两个子节点频率之和。然后将新节点重新插入到最小堆中。

  4. 重复操作:不断重复第三步,直到最小堆中只剩下一个节点,这个节点即为哈夫曼树的根节点。

三、哈夫曼编码与数据压缩

哈夫曼编码 是基于哈夫曼树的一种前缀编码方式。对于哈夫曼树中的每一个叶子节点(对应一个字符),从根节点到该叶子节点的路径定义了该字符的编码。由于高频字符对应的路径较短,低频字符对应的路径较长,因此哈夫曼编码具有“短码长优先”的特点,能够实现数据的高效压缩。

四、哈夫曼树的应用场景

  1. 数据压缩:哈夫曼编码广泛应用于文件压缩工具,如文本压缩、图片压缩、视频压缩等领域。通过使用哈夫曼编码进行无损压缩,可以显著减少存储空间需求。

  2. 网络传输:在网络通信中,为了节省带宽资源,也会采用哈夫曼编码对传输数据进行压缩。

  3. 操作系统内核:在某些操作系统内核中,用于进程调度的数据结构也借鉴了哈夫曼树的思想,优化调度效率。

  4. 搜索引擎:在信息检索领域,哈夫曼树可用于构建倒排索引,提高查询速度。

五、哈夫曼树的代码实践 

1.节点类

class Node1 implements Comparable<Node1> {
    Byte data;      //存放数据字符本身 a -> 97
    int weight;     //权值,字符的次数
    Node1 lift;
    Node1 right;

    public Node1(Byte data, int weight) {
        this.data = data;
        this.weight = weight;
    }

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

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

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

2.构建哈夫曼树的集合

    /**
     * @param bytes 接收字节的数组
     * @return 返回构建哈夫曼树的集合
     */
    public static List<Node1> getNodes(byte[] bytes) {
        List<Node1> node1s = new ArrayList<>();
        HashMap<Byte, Integer> counts = new HashMap<>();
        for (byte data : bytes) {
            Integer count = counts.get(data);
            if (count == null) {
                counts.put(data, 1);
            } else {
                counts.put(data, count + 1);
            }
        }
//        构建node对象
        counts.entrySet().forEach((item) -> {
            node1s.add(new Node1(item.getKey(), item.getValue()));
        });
        return node1s;
    }

3.构建哈夫曼树

    //根据构建哈夫曼的集合创建哈夫曼树
    public static Node1 createHuffmanTree(List<Node1> list) {
        while (list.size() > 1) {
//        先进行排序
            Collections.sort(list);
//        取出两个权值最小的节点
            Node1 liftNode = list.get(0);
            Node1 rightNode = list.get(1);
//        重新构建一个二叉树 根节点没有data数据只有权值
            Node1 parent = new Node1(null, liftNode.weight + rightNode.weight);
            parent.lift = liftNode;
            parent.right = rightNode;
//            将父节点加入到集合
            list.add(parent);
//            删除两个小节点
            list.remove(liftNode);
            list.remove(rightNode);
        }
//        返回的就是根节点
        return list.get(0);
    }

4.获取哈夫曼编码

    //    生成哈夫曼树对应的哈夫曼编码
//    思路
//    1.将哈弗曼编码存放在Map<Byte,String>形式
    static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();
    //    2.在生成哈夫曼编码表示,需要去拼接路径,定义一个StringBuilder,存储某个叶子节点的路径
    static StringBuilder stringBuilder = new StringBuilder();

    //    为了调用方便将生成哈夫曼编码方法简化
    public static Map<Byte, String> getCodes(Node1 root) {
        if (root == null) {
            throw new RuntimeException("节点为空!");
        }
        getCodes(root.lift, "0", stringBuilder);
        getCodes(root.right, "1", stringBuilder);
        return huffmanCodes;
    }

    /**
     * 目的:将传入的node节点的哈夫曼编码得到并放入huffmanCodes中 生成哈夫曼编码
     *
     * @param node1         传入节点
     * @param code          路径:左子节点为0,右子节点为1
     * @param stringBuilder 用于拼接路径
     */
    public static void getCodes(Node1 node1, String code, StringBuilder stringBuilder) {
        StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);
        stringBuilder1.append(code);
        if (node1 != null) {
//            判断当前节点是叶子节点还是非叶子节点
            if (node1.data == null) {   //非叶子节点
//                向左递归处理
                getCodes(node1.lift, "0", stringBuilder1);
//                向右递归
                getCodes(node1.right, "1", stringBuilder1);
            } else {   //叶子节点
//                表示找到某个叶子节点的最后
                huffmanCodes.put(node1.data, stringBuilder1.toString());
            }
        }
    }

5.哈夫曼数据压缩 

//    根据字符串对应的byte[] 和哈夫曼编码表,返回一个哈夫曼编码压缩后的byte[]

    /**
     * @param bytes        原始字符串对应的数组
     * @param huffmanCodes 生成的哈夫曼编码
     * @return 返回根据哈夫曼编码压缩的只有01的编码
     */
    public static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
        StringBuilder stringBuilder = new StringBuilder();
        for (byte aByte : bytes) {
            stringBuilder.append(huffmanCodes.get(aByte));
        }
        int len;
        if (stringBuilder.length() % 8 == 0) {
            len = stringBuilder.length() / 8;
        } else {
            len = stringBuilder.length() / 8 + 1;
        }
        byte[] huffmanCodeBytes = new byte[len];
        int index = 0;
        for (int i = 0; i < stringBuilder.length(); i += 8) {
            String strByte;
            if (i + 8 > stringBuilder.length()) {
                strByte = stringBuilder.substring(i);
            } else {
                strByte = stringBuilder.substring(i, i + 8);
            }
            huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
            index++;
        }
        return huffmanCodeBytes;
    }

6.哈夫曼数据解码

//    将根据哈夫曼编码压缩后的数组转换为原始数组

    /**
     * 将一个压缩后的byte转化为二进制字符串
     *
     * @param bytes 压缩后的数组
     * @param flag  标识是否需要补高位 TRUE 补 FALSE 不补
     * @return 该bytes对应的二进制按补码返回
     */
    public static String byteToBitString(boolean flag, byte bytes) {
        int temp = bytes;
        if (flag) {
            temp |= 256;
        }
        String binaryString = Integer.toBinaryString(temp);
        if (flag) {
            return binaryString.substring(binaryString.length() - 8);
        } else {
            return binaryString;
        }
    }

    //    对压缩数据的解码方法

    /**
     * @param huffmanCodes 哈夫曼编码表
     * @param huffmanBytes 被哈夫曼编码表编码的字节数组
     * @return 原来字符串对应的数组
     */
    public static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < huffmanBytes.length; i++) {
//            判断是否为最后一个字节
            boolean flag = (i == huffmanBytes.length - 1);
            stringBuilder.append(byteToBitString(!flag, huffmanBytes[i]));
        }
        Map<String, Byte> map = new HashMap<>();
        for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
            map.put(entry.getValue(), entry.getKey());
        }
        List<Byte> list = new ArrayList<>();
        for (int i = 0; i < stringBuilder.length(); ) {
            int count = 1;
            boolean flag = true;
            Byte b = null;
            while (flag) {
                String key = stringBuilder.substring(i, i + count);
                b = map.get(key);
                if (b == null) {
                    count++;
                } else {
                    flag = false;
                }
            }
            list.add(b);
            i += count;
        }
        byte[] b = new byte[list.size()];
        for (int i = 0; i < b.length; i++) {
            b[i] = list.get(i);
        }
        return b;
    }

六、总结

        哈夫曼树凭借其独特的优势,在数据压缩和信息处理等多个领域都发挥着重要作用。理解和掌握哈夫曼树的构造原理及哈夫曼编码方法,不仅有助于我们解决实际工程问题,还能深化对数据结构与算法设计的理解。随着数据量的爆炸性增长,哈夫曼树及其编码技术将在未来继续扮演关键角色,助力我们更高效地管理和利用海量数据资源。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值