哈夫曼编码压缩与解压思路分析与Java代码实现

Posted Spring-_-Bear

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了哈夫曼编码压缩与解压思路分析与Java代码实现相关的知识,希望对你有一定的参考价值。

一、哈夫曼编码介绍

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

赫夫曼编码的具体方法:先按出现的概率大小排队,把两个最小的概率相加,作为新的概率 和剩余的概率重新排队,再把最小的两个概率相加,再重新排队,直到最后变成1。每次相 加时都将“0”和“1”赋与相加的两个概率,读出时由该符号开始一直走到最后的“1”, 将路线上所遇到的“0”和“1”按最低位到最高位的顺序排好,就是该符号的赫夫曼编码。

二、哈夫曼压缩思路分析及代码实现

  1. 将需要传输的原始字符串数据转换为对应的字节数组 i like like like java do you like a java
  2. 根据原始字符串数据的字节数组数据依次构建哈夫曼树节点
    /**
     * 根据原始数据字节数组创建对应的树节点
     *
     * @param contentBytes 字节数据数组
     * @return 树节点集合
     */
    public List<TreeNode> createTreeNodes(byte[] contentBytes) 
        List<TreeNode> nodeList = new ArrayList<>();
        Map<Byte, Integer> dataCounts = new HashMap<>(10);

        // 遍历数据,统计每个字符出现的次数,获得对应权重
        for (byte contentByte : contentBytes) 
            // 从 Map 中获取对应的数据出现的次数,未出现则添加,出现了则次数 +1
            dataCounts.merge(contentByte, 1, Integer::sum);
        

        // 遍历 data-weight Map 集合,依次创建 TreeNode
        Set<Map.Entry<Byte, Integer>> entries = dataCounts.entrySet();
        for (Map.Entry<Byte, Integer> entry : entries) 
            nodeList.add(new TreeNode(entry.getKey(), entry.getValue()));
        
        return nodeList;
    
  1. 根据树节点构建哈夫曼树
    /**
     * 根据传入的节点集合创建一颗赫夫曼树
     *
     * @param nodeList 树节点集合
     * @return 哈夫曼树根节点
     */
    public TreeNode createHuffmanTree(List<TreeNode> nodeList) 
        if (nodeList == null || nodeList.size() == 0) 
            return null;
        

        while (nodeList.size() > 1) 
            // 使用 Collections 工具类对集合中的节点根据 val 值进行排序
            Collections.sort(nodeList);
            // 每次取出排序后的两个节点生成新的父节点
            TreeNode leftChild = nodeList.get(0);
            TreeNode rightChild = nodeList.get(1);
            TreeNode parent = new TreeNode(null, leftChild.weight + rightChild.weight);
            parent.left = leftChild;
            parent.right = rightChild;
            nodeList.remove(leftChild);
            nodeList.remove(rightChild);
            // 将 parent 加入 List 中继续排序,依次构建
            nodeList.add(parent);
        
        return nodeList.get(0);
    
  1. 遍历哈夫曼树得到每个数据对应的哈夫曼编码即哈夫曼表
    /**
     * 根据赫夫曼树生成对应的赫夫曼编码,左孩子为 0,右孩子为 1
     * 例如:32 -> 01;97 -> 100
     *
     * @param root 根节点
     * @return data - huffmanCode
     * @code 左孩子为 "0",有孩子为 "1",初始化值为 ""
     */
    public Map<Byte, String> generateHuffmanCode(TreeNode root, String code, StringBuilder stringBuilder) 
        StringBuilder temp = new StringBuilder(stringBuilder);
        // 将当前节点对于对应的编码 0 或 1 追加到 StringBuilder 中
        temp.append(code);
        if (root != null) 
            // 判断当前节点是不是赫夫曼树的叶子节点,不是则递归查找
            if (root.data == null) 
                // 左递归
                generateHuffmanCode(root.left, "0", temp);
                // 右递归
                generateHuffmanCode(root.right, "1", temp);
             else 
                huffmanCodeMap.put(root.data, temp.toString());
            
        
        return huffmanCodeMap;
    
  1. 拼接哈夫曼表中的每个原始数据对应的哈夫曼编码,得到原始数据对应哈夫曼编码 1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
  2. 将哈夫曼编码(二进制字符串)按 8 位字长依次解析并存入字节数据,此字节数组即原始数据经过哈夫曼压缩后的字节数据 [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
    /**
     * 将对应数据的哈夫曼编码转换为字节数组数据,8 位一个字节
     *
     * @param srcDataBytes   原始数据字节数组
     * @param huffmanCodeMap srcData 对应的哈夫曼编码 Map 即 srcByteData - huffmanCode
     * @return 将哈夫曼变换转换后的字节数组
     */
    public byte[] huffmanCodeZip(byte[] srcDataBytes, Map<Byte, String> huffmanCodeMap) 
        StringBuilder stringBuilder = new StringBuilder();
        // 按序取出 Map 中的编码数据,连接成字符串即最短路径
        for (byte srcDataByte : srcDataBytes) 
            stringBuilder.append(huffmanCodeMap.get(srcDataByte));
        

        // 统计哈夫曼编码字符串的长度以将其存入字节数组,8 位数据一个字节
        int len = stringBuilder.length() % 8 == 0 ? stringBuilder.length() / 8 : stringBuilder.length() / 8 + 1;
        byte[] huffmanCodeBytes = new byte[len];
        for (int i = 0; i < len; i++) 
            String str;
            int start = i * 8;
            int end = i * 8 + 8;
            // 从哈夫曼编码字符串中每 8 位截取一个子串
            if (end > stringBuilder.length()) 
                str = stringBuilder.substring(start);
             else 
                str = stringBuilder.substring(start, end);
            
            // 将子串数据(二进制)转换为对应的字节数据并存入字节数组中
            huffmanCodeBytes[i] = (byte) Integer.parseInt(str, 2);
        
        return huffmanCodeBytes;
    

三、哈夫曼压缩代码演示

package com.bear.datastructure.tree;


import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * @author Spring-_-Bear
 * @datetime 2022/3/18 20:16
 */
public class HuffmanCode 
    public static class TreeNode implements Comparable<TreeNode> 
        /**
         * 存放数据本身;'a' <=> 19
         */
        Byte data;
        /**
         * date 对应的权重,出现的次数
         */
        int weight;
        public TreeNode left;
        public TreeNode right;

        public TreeNode(Byte data, int weight) 
            this.data = data;
            this.weight = weight;
        

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

        @Override
        public int compareTo(TreeNode o) 
            return this.weight - o.weight;
        
    

    private final Map<Byte, String> huffmanCodeMap = new HashMap<>();

    /**
     * 哈夫曼压缩,将需要传输的字符串压缩为字节数组
     * 示例:"i like like like java do you like a java" -> [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
     * @param str 字符串
     * @return 字符数组
     */
    public byte[] huffmanZip(String str) 
        if (str == null || str.length() == 0) 
            return null;
        

        // 1. 将原始数据字符串转换为字节数组
        byte[] srcBytes = str.getBytes(StandardCharsets.UTF_8);
        System.out.println("原始数据所占字节数:" + srcBytes.length);
        // 2. 根据字节数组中的数据值依次创建哈夫曼树节点
        List<TreeNode> treeNodes = createTreeNodes(srcBytes);
        // 3. 根据创建好的节点构建哈夫曼树
        TreeNode huffmanTree = createHuffmanTree(treeNodes);
        // 4. 遍历创建好的哈夫曼树,得到原始数据对应的哈夫曼编码,向左路径编码为 0,右为 1
        Map<Byte, String> byteStringMap = generateHuffmanCode(huffmanTree, "", new StringBuilder());
        // 5. 拼接每个数据的哈夫曼编码,得到哈夫曼树最短路径,将最短路径处理为字节数组
        byte[] zipBytes = huffmanCodeZip(srcBytes, byteStringMap);
        System.out.println("Huffman 压缩后所占字节数:" + zipBytes.length);
        System.out.println("压缩比:" + 1.0 * (srcBytes.length - zipBytes.length) / srcBytes.length);
        return zipBytes;
    

    /**
     * 将对应数据的哈夫曼编码转换为字节数组数据,8 位一个字节
     * 示例:1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100 -> [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
     * @param srcDataBytes   原始数据字节数组
     * @param huffmanCodeMap srcData 对应的哈夫曼编码 Map 即 srcByteData - huffmanCode
     * @return 将哈夫曼变换转换后的字节数组
     */
    public byte[] huffmanCodeZip(byte[] srcDataBytes, Map<Byte, String> huffmanCodeMap) 
        StringBuilder stringBuilder = new StringBuilder();
        // 按序取出 Map 中的编码数据,连接成字符串即最短路径
        for (byte srcDataByte : srcDataBytes) 
            stringBuilder.append(huffmanCodeMap.get(srcDataByte));
        
        System.out.println("哈夫曼压缩最短路径:" + stringBuilder);
        // 统计哈夫曼编码字符串的长度以将其存入字节数组,8 位数据一个字节
        int len = stringBuilder.length() % 8 == 0 ? stringBuilder.length() / 8 : stringBuilder.length() / 8 + 1;
        byte[] huffmanCodeBytes = new byte[len];
        for (int i = 0; i < len; i++) 
            String str;
            int start = i * 8;
            int end = i * 8 + 8;
            // 从哈夫曼编码字符串中每 8 位截取一个子串
            if (end > stringBuilder.length()) 
                str = stringBuilder.substring(start);
             else 
                str = stringBuilder.substring(start, end);
            
            // 将子串数据(二进制)转换为对应的字节数据并存入字节数组中
            huffmanCodeBytes[i] = (byte) Integer.parseInt(str, 2);
        
        return huffmanCodeBytes;
    

    /**
     * 根据赫夫曼树生成对应的赫夫曼编码,左孩子为 0,右孩子为 1
     * 例如:32 -> 01;97 -> 100
     * 
     * @param root 根节点
     * @return data - huffmanCode
     * @code 左孩子为 "0",有孩子为 "1",初始化值为 ""
     */
    public Map<Byte, String> generateHuffmanCode(TreeNode root, String code, StringBuilder stringBuilder) 
        StringBuilder temp = new StringBuilder(stringBuilder);
        // 将当前节点对于对应的编码 0 或 1 追加到 StringBuilder 中
        temp.append(code);
        if (root != null) 
            // 判断当前节点是不是赫夫曼树的叶子节点,不是则递归查找
            if (root.data == null) 
                // 左递归
                generateHuffmanCode(root.left, "0", temp);
                // 右递归
                generateHuffmanCode(root.right, "1", temp);
             else 
                huffmanCodeMap.put(root.data, temp.toString());
            
        
        return huffmanCodeMap;
    

    /**
     * 根据传入的节点集合创建一颗赫夫曼树
     *
     * @param nodeList 树节点集合
     * @return 哈夫曼树根节点
     */
    public TreeNode createHuffmanTree(List<TreeNode> nodeList) 
        if (nodeList == null || nodeList.size() == 0) 
            return null;
        

        while (nodeList.size() > 1) 
            // 使用 Collections 工具类对集合中的节点根据 val 值进行排序
            Collections.sort(nodeList);
            // 每次取出排序后的两个节点生成新的父节点
            TreeNode leftChild = nodeList.get(0);
            TreeNode rightChild = nodeList.get(1);
            TreeNode parent = new TreeNode(null, leftChild.weight + rightChild.weight);
            parent.left = leftChild;
            parent.right = rightChild;
            nodeList.remove(leftChild);
            nodeList.remove(rightChild);
            // 将 parent 加入 List 中继续排序,依次构建
            nodeList.add(parent);
        
        return nodeList.get(0);
    

    /**
     * 根据原始数据字节数组创建对应的树节点
     *
     * @param contentBytes 字节数据数组
     * @return 树节点集合
     */
    public List<TreeNode> createTreeNodes(byte[] contentBytes) 
        List<TreeNode> nodeList = new ArrayList<>();
        Map<Byte, Integer> dataCounts = new HashMap<>(10);

        // 遍历数据,统计每个字符出现的次数,获得对应权重
        for (byte contentByte : contentBytes) 
            // 从 Map 中获取对应的数据出现的次数,未出现则添加,出现了则次数 +1
            dataCounts.merge(contentByte, 1, Integer::sum);
        

        // 遍历 data-weight Map 集合,依次创建 TreeNode
        Set<Map.Entry<Byte, Integer>> entries = dataCounts.entrySet();
        for (Map.Entry<Byte, Integer> entry : entries) 
            nodeList.add(new TreeNode(entry.getKey(), entry.getValue()));
        
        return nodeList;
    

四、哈夫曼解压思路分析及代码实现

  1. 将哈夫曼压缩得到的编码字节数组重新转换为哈夫曼编码最短路径(二进制字符串)
    /**
     * 将哈夫曼压缩后得到的字节数组转换为哈夫曼树的最短路径(二进制字符串)
     * 示例:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28] - >
     * 10101000 10111111 11001000 10111111 11001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
     * @param huffmanBytes 哈夫曼编码字节数组
     * @return 二进制字符串(补码) 或 null
     */
    public String huffmanBytesToBitString(byte[] huffmanBytes) 
        int len = huffmanBytes.length;
        StringBuilder bitString = new StringBuilder();
        /*
         * 依次将每一个字节数据转换为对应的二进制串并拼接
         * huffmanBytes[0] ~ huffmanBytes[len - 2] 之间的所有字节数据转换为均对应 8 位二进制串
         * 最后一位 huffmanBytes[len - 1] 对应的二进制串可能不是 8 位二进制串(与哈夫曼压缩时的处理有关)
         */

        // 处理前 n - 1 个字节数据,处理后每个字节对应 8 位二进制串
        for (int i = 0; i < len - 1; i++) 
            byte digit = huffmanBytes[i];
            // 使用 Integer.toBinaryString(digit) 进行转换时,若 digit >= 0,则对应二进制串高位为空,需要进行高位补 0
            // 若为负数,得到的二进制串长度为 32 位二进制补码,需要进行截取后 8 位
            String str = Integer.toBinaryString(digit);
            if (digit >= 0) 
                for (int j = 1; j <= 8 - str.length(); j++) 
                    bitString.append("0");
                
                bitString.append(str);
             else 
                bitString.append(str.substring(str.length() - 8));
            
        

        // 处理最后一位字节数据
        byte lastByte = huffmanBytes[len - 1];
        String bitStr = Integer.toBinaryString(lastByte);
        // 大于等于 0 时高位无前导 0,直接拼接并返回
        if (lastByte >= 0) 
            bitString.append(bitStr);
         else
            // 小于 0 时高位存在前导 0,需要特殊处理(可能刚好 8 位,可能小于 8 位)
            // TODO 暂未找到较好解决方案,有幸看到这的码友可提供好思路
        
        return bitString.toString();
    
  1. 遍历第一步逆向解析字节数组得到的原哈夫曼最短路径二进制字符串,从哈夫曼编码表中依次匹配,获得字节数组,最终得到原始字符串
    /**
     * 将哈夫曼树最短路径解析为原始字符串数据
     *
     * @param huffmanCodeTable     哈夫曼编码表
     * @param huffmanPathBitString 哈夫曼树最短路径
     * @return 原始数据字符串
     */
    public String huffmanBitStringToSrcString(Map<Byte, String> huffmanCodeTable, String huffmanPathBitString) 
        // 1. 将哈夫曼编码表(key - value [a - 010121])逆向转换以方便查找
        Map<String, Byte> map = new HashMap<>以上是关于哈夫曼编码压缩与解压思路分析与Java代码实现的主要内容,如果未能解决你的问题,请参考以下文章

文件压缩与解压-霍夫曼编码

基于哈夫曼树的任意文件解压缩实现

文件压缩与解压

项目:文件的压缩与解压

高级数据结构---赫(哈)夫曼树及java代码实现

哈夫曼编码译码器 java