赫夫曼树

本文详细介绍了赫夫曼树的概念、创建过程和编码原理,包括如何通过权值构建最优二叉树,并提供了Java代码示例展示如何创建赫夫曼树以及利用赫夫曼编码进行数据压缩和解压。同时,讨论了文件的压缩与解压操作,强调了赫夫曼编码在数据压缩中的应用及其局限性。
摘要由CSDN通过智能技术生成

一、赫夫曼树

1.1 基本介绍

  • 给定 n 个权值作为 n 个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树/霍夫曼树(Huffman Tree)。
  • 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

1.2 重要概念

  • 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为 1,则从根结点到第 L 层结点的路径长度为 L-1。
  • 节点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积
  • 的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为 WPL(weighted pathlength) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。
  • WPL 最小的就是赫夫曼树。
  • 示意图

1.3 创建思路

需求:将数列 {13, 7, 8, 3, 29, 6, 1},要求转成一颗赫夫曼树。

  1. 从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树;
  2. 取出根节点权值最小的两颗二叉树;
  3. 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
  4. 再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树。

1.4 代码示例

  • 节点及属性
public class Node implements Comparable<Node> {

    /**
     * 存放数据(字符)本身,比如'a' => 97 ; ' ' => 32 ...
     */
    private Byte data;

    /**
     * 节点权值。
     */
    private final int weight;

    /**
     * 左子节点。
     */
    private Node left;

    /**
     * 右子节点。
     */
    private Node right;

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

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

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

    @Override
    public int compareTo(Node node) {
        return this.weight - node.weight;
    }


    public Byte getData() {
        return data;
    }

    public int getWeight() {
        return weight;
    }

    public Node getLeft() {
        return left;
    }

    public void setLeft(Node left) {
        this.left = left;
    }

    public Node getRight() {
        return right;
    }

    public void setRight(Node right) {
        this.right = right;
    }

    @Override
    public String toString() {
        return "Node{" +
                "data=" + data +
                ", weight=" + weight +
                '}';
    }
}
  • 创建赫夫曼树
public class HuffmanTree {

    public static void main(String[] args) {
        int[] arr = {13, 7, 8, 3, 29, 6, 1};
        List<Node> nodes = getNodes(arr);
        Node root = createHuffmanTree(nodes);
        System.out.println("rootWeight=" + root.getWeight());
        // rootWeight=67

        System.out.println();
        System.out.println("前序遍历结果如下:");
        preOrder(root);
        // 前序遍历结果如下:
        // weight=67
        // weight=29
        // weight=38
        // weight=15
        // weight=7
        // weight=8
        // weight=23
        // weight=10
        // weight=4
        // weight=1
        // weight=3
        // weight=6
        // weight=13
    }

    /**
     * 前序遍历。
     *
     * @param node 节点
     */
    public static void preOrder(Node node) {
        if (null == node) {
            throw new NullPointerException();
        }
        node.preOrder();
    }

    /**
     * 将整型数组的元素创建为新节点。
     *
     * @param array 需要创建成哈夫曼树的数组
     * @return {@link List}<{@link Node}>
     */
    private static List<Node> getNodes(int[] array) {
        // 将数组元素构建成新的Node并存入集合(方便进行操作)。
        List<Node> nodes = new ArrayList<>();

        for (int i : array) {
            nodes.add(new Node(i));
        }

        return nodes;
    }

    /**
     * 创建哈夫曼树。
     *
     * @return {@link Node} 创建好后的赫夫曼树的 root 节点
     */
    public static Node createHuffmanTree(List<Node> nodes) {
        // 循环处理。
        while (nodes.size() > 1) {

            // 从小到大排序。
            Collections.sort(nodes);

            // 取出权值最小的节点。
            Node leftNode = nodes.get(0);
            // 取出权值倒数第二的节点。
            Node rightNode = nodes.get(1);

            // 构建新树。
            // 新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和。
            int parentWeight = leftNode.getWeight() + rightNode.getWeight();
            Node parent = new Node(null, parentWeight);
            parent.setLeft(leftNode);
            parent.setRight(rightNode);

            // 移除已经处理过的二叉树。
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            // 将节点加入集合。
            nodes.add(parent);
        }
        // 返回 root 节点。
        return nodes.get(0);
    }
}

二、赫夫曼编码

2.1 基本介绍

  • 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,哈夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码(有时也称为霍夫曼编码)。

  • 赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。

  • 赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在 20%~90% 之间。

2.2 原理说明

  • 假设传输的字符串为:“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
  • 需求:按照上面字符出现的次数构建一颗赫夫曼树,次数作为权值。
  • 示意图

  • 根据赫夫曼树,给各个字符规定编码(前缀编码),向左的路径为 0 向右的路径为 1,编码如下:
j:0000、v:0001、l:001、" ":01
o:1000、u:10010、d:100110、y:100111
i:101、a:110、k:1110、e:1111
  • 原字符"i like like like java do you like a java",完整编码后得到1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
  • 长度由原来的359变为133,压缩率为62.9%。
  • 说明:此编码满足前缀编码,即字符的编码都不能是其它字符编码的前缀,不会造成匹配的多义性赫夫曼编码是无损处理方案。
  • 注意:赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是 wpl 是一样的都是最小的,最后生成的赫夫曼编码的长度是一样

2.3 数据压缩与解压

需求:使用赫夫曼编码来对字符串进行编码及逆向解码操作。

  • 代码示例
    /**
     * 生成赫夫曼树对应的赫夫曼编码。
     */
    static Map<Byte, String> huffmanCodes = new HashMap<>();

    /**
     * 存储某个叶子节点的路径。
     */
    static StringBuilder pathBuilder = new StringBuilder();

    @Test
    public void test01HuffmanCodesAndDecode() {
        String content = "i like like like java do you like a java";
        List<Node> nodes = getNodes(content.getBytes());
        Node root = HuffmanTree.createHuffmanTree(nodes);
        System.out.println("编码操作, 生成的赫夫曼编码表: " + getCodes(root));
        // 编码操作, 生成的赫夫曼编码表: {32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}


        byte[] huffmanCodesBytes = huffmanZip(content.getBytes());
        byte[] sourceBytes = decode(huffmanCodes, huffmanCodesBytes);
        System.out.println("解码操作, 原来的字符串: " + new String(sourceBytes));
        // 解码操作, 原来的字符串: i like like like java do you like a java
    }

    /**
     * 重载 getCodes()。
     *
     * @param root 根节点
     * @return {@link Map}<{@link Byte}, {@link String}>
     */
    private static Map<Byte, String> getCodes(Node root) {
        if (null == root) {
            return Collections.emptyMap();
        }
        // 处理root的左子树。
        getCodes(root.getLeft(), "0", pathBuilder);
        // 处理root的右子树。
        getCodes(root.getRight(), "1", pathBuilder);
        return huffmanCodes;
    }

    /**
     * 将传入的 node 节点的所有叶子节点的赫夫曼编码得到,并放入到huffmanCodes集合。
     *
     * @param node        节点
     * @param pathCode    路径: 左子节点是 0, 右子节点 1
     * @param pathBuilder 用于拼接路径
     */
    private static void getCodes(Node node, String pathCode, StringBuilder pathBuilder) {
        StringBuilder builder = new StringBuilder(pathBuilder);
        builder.append(pathCode);
        if (null != node) {
            // 非叶子节点时,进行递归处理。
            if (null == node.getData()) {
                // 向左递归。
                getCodes(node.getLeft(), "0", builder);
                // 向右递归。
                getCodes(node.getRight(), "1", builder);
            } else {
                // 就表示找到某个叶子节点的最后。
                huffmanCodes.put(node.getData(), builder.toString());
            }
        }
    }

    /**
     * 将字节数组的元素创建为新节点。
     *
     * @param bytes 字节
     * @return {@link List}<{@link Node}>
     */
    private static List<Node> getNodes(byte[] bytes) {

        List<Node> nodes = new ArrayList<>();
        Map<Byte, Integer> counts = new HashMap<>(8);

        // 遍历 bytes , 统计每一个byte出现的次数->map[key,value]
        for (byte data : bytes) {
            // 不断对字符出现次数,进行累加计数。
            counts.merge(data, 1, Integer::sum);
        }

        Set<Map.Entry<Byte, Integer>> entries = counts.entrySet();
        for (Map.Entry<Byte, Integer> entry : entries) {
            // [Node[date=97 ,weight=5], Node[date=32 , weight=9]......]
            Byte data = entry.getKey();
            Integer weight = entry.getValue();
            // 创建新节点并添加至集合。
            nodes.add(new Node(data, weight));
        }

        return nodes;
    }

    /**
     * 将一个 byte 转成一个二进制的字符串。
     *
     * @param b    传入的 byte
     * @param flag 标志是否需要补高位。 1.true: 表示需要补高位;2.false:表示不补;3.如果是最后一个字节,无需补高位;
     * @return {@link String} 该 byte 补码对应的二进制的字符串
     */
    private static String byteToBitString(byte b, boolean flag) {
        // 将 byte 转为 int。
        int temp = b;

        if (flag) {
            // 按位与 256。
            temp |= 256;
        }
        
        // 返回的是temp对应的二进制的补码。
        String str = Integer.toBinaryString(temp);
        if (flag) {
            return str.substring(str.length() - 8);
        } else {
            return str;
        }
    }

    /**
     * 对压缩数据的解码。
     *
     * @param huffmanCodes 霍夫曼编码表
     * @param huffmanBytes 赫夫曼编码得到的字节数组
     * @return {@link byte[]} 解码后字符串对应的数组
     */
    private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {

        StringBuilder builder = new StringBuilder();

        // 将byte数组转成二进制的字符串。
        for (int i = 0; i < huffmanBytes.length; i++) {
            byte b = huffmanBytes[i];
            // 判断是不是最后一个字节。
            boolean flag = (i == (huffmanBytes.length - 1));
            String str = byteToBitString(b, !flag);
            builder.append(str);
        }

        // 把赫夫曼编码表进行调换,因为反向查询。
        Map<String, Byte> map = new HashMap<>(8);
        Set<Map.Entry<Byte, String>> entries = huffmanCodes.entrySet();
        for (Map.Entry<Byte, String> entry : entries) {
            map.put(entry.getValue(), entry.getKey());
        }


        List<Byte> list = new ArrayList<>();
        // 指定索引对可变字符串进行扫描。
        for (int i = 0; i < builder.length(); ) {
            int count = 1;
            Byte b = null;
            boolean flag = true;

            while (flag) {
                // 递增的取出 key。
                // i 不动,让 count 移动,指定匹配到一个字符。
                String key = builder.substring(i, i + count);
                b = map.get(key);
                if (null == b) {
                    count++;
                } else {
                    // 说明匹配到了。
                    flag = false;
                }
            }
            list.add(b);
            // i 直接移动到 count。
            i += count;
        }

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

        return bytes;
    }

    /**
     * 将字符串对应的字节数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码[压缩后]的字节数组。
     *
     * @param bytes        原始的字符串对应的字节数组
     * @param huffmanCodes 生成的赫夫曼编码表
     * @return 压缩后的字节数组
     */
    private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {

        // 将字节数组转成 赫夫曼编码对应的字符串。
        StringBuilder builder = new StringBuilder();
        for (byte b : bytes) {
            builder.append(huffmanCodes.get(b));
        }

        // 统计返回字节数组 huffmanCodeBytes 长度。
        int len;
        if (builder.length() % 8 == 0) {
            len = builder.length() / 8;
        } else {
            len = builder.length() / 8 + 1;
        }
        // 创建存储压缩后的字节数组。
        byte[] huffmanCodeBytes = new byte[len];
        // 记录是第几个字节。
        int index = 0;
        // 因为是每8位对应一个字节,所以步长 +8。
        for (int i = 0; i < builder.length(); i += 8) {
            String strByte;
            // 不够8位。
            if (i + 8 > builder.length()) {
                strByte = builder.substring(i);
            } else {
                strByte = builder.substring(i, i + 8);
            }
            // 存入压缩字节数组。
            huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
            index++;
        }
        return huffmanCodeBytes;
    }

    /**
     * 过程封装:1.创建赫夫曼树 >> 2.得到赫夫曼编码 >> 3.生成压缩赫夫曼编码字节数组。
     * 目的:便于调用。
     *
     * @param bytes 原始的字符串对应的字节数组
     * @return 是经过 赫夫曼编码处理后的字节数组(压缩后的数组)
     */
    private static byte[] huffmanZip(byte[] bytes) {
        List<Node> nodes = getNodes(bytes);
        // 根据 nodes 创建的赫夫曼树。
        Node huffmanTreeRoot = HuffmanTree.createHuffmanTree(nodes);
        // 对应的赫夫曼编码。
        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
        // 根据生成的赫夫曼编码,压缩得到压缩后的赫夫曼编码字节数组。
        return zip(bytes, huffmanCodes);
    }

2.4 文件压缩与解压

需求:通过赫夫曼编码完成文件的压缩及解压操作。

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

解压思路:读取压缩文件(数据和赫夫曼编码表)-> 完成解压(文件恢复)。

  • 代码示例
    @Test
    public void test02ZipAndUnzip() {
        zipFile("D:\\test.txt", "D:\\test.zip");
        // total number of bytes:111852
        System.out.println("zip success!");
        // zip success!

        unZipFile("D:\\test.zip", "D:\\test1.txt");
        System.out.println("unZip success!");
    }

    /**
     * 文件进行压缩。
     *
     * @param srcFile 源文件的全路径
     * @param dstFile 输出文件的全路径
     */
    private static void zipFile(String srcFile, String dstFile) {

        try (
                // 文件的输入流。
                FileInputStream is = new FileInputStream(srcFile);

                // 文件输出流。
                OutputStream os = new FileOutputStream(dstFile);
                ObjectOutputStream oos = new ObjectOutputStream(os)
        ) {

            byte[] bytes = new byte[is.available()];
            // 读取文件及字节数大小。
            int read = is.read(bytes);
            System.out.println("total number of bytes:" + read);

            // 对源文件压缩后写出。
            byte[] huffmanBytes = huffmanZip(bytes);
            oos.writeObject(huffmanBytes);
            oos.writeObject(huffmanCodes);

        } catch (IOException e) {
            System.out.println("zipFile error:" + e.getMessage());
        }
    }

    /**
     * 对压缩文件的解压。
     *
     * @param zipFile 准备解压文件的全路径
     * @param dstFile 输出文件的全路径
     */
    @SuppressWarnings("unchecked")
    public static void unZipFile(String zipFile, String dstFile) {
        try (
                // 文件的输入流。
                InputStream is = new FileInputStream(zipFile);
                ObjectInputStream ois = new ObjectInputStream(is);

                // 文件输出流。
                OutputStream os = new FileOutputStream(dstFile)

        ) {
            // 读取字节数组。
            byte[] huffmanBytes = (byte[]) ois.readObject();
            // 读取赫夫曼编码表。
            Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();
            // 解码。
            byte[] bytes = decode(huffmanCodes, huffmanBytes);
            // 写出。
            os.write(bytes);

        } catch (IOException | ClassNotFoundException e) {
            System.out.println("zipFile error:" + e.getMessage());
        }
    }

2.5 注意事项

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

三、结束语


“-------怕什么真理无穷,进一寸有一寸的欢喜。”

微信公众号搜索:饺子泡牛奶

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值