[062] Java “数据压缩及解压--哈夫曼编码”

1、哈夫曼编码

  • Huffman Coding:是哈夫曼树在电讯通信领域的经典应用,被用于数据文件压缩,其压缩率在20%-90%之间,是可变字长编码(VLC)的一种,属于无损压缩
    • 固定长度编码:使用固定长度的符号编码,如使用8位二进制数进行编码的ASCII码就是最常用的定长编码;定长编码后的数据文件较大;
    • 可变字长编码:根据字符在文件中的出现率大小分配不同长度的编码,出现几率高的字符使用较短的编码,反之亦然。这能使编码后的文件得到一定压缩,但可能使解码产生二义性
    • 哈夫曼编码是变长编码,但通过巧妙的哈夫曼树构建规则,同一文件中每个字符的编码都不可能是其他任何字符编码的前缀,这消除了编码的二义性;
    • 哈夫曼树建立过程不唯一(当某些字符在文件中出现的次数相同时,它们的排序不同,构建的哈夫曼树也不同),对应生成的哈夫曼编码也不唯一。但编码后的编码长度都一样(WPL相同),即压缩率相同。
  • 哈夫曼编码压缩的步骤:
    • 输入需要编码的字符串,统计不同字符对应出现的次数
    • 次数作为权值,构建一棵哈夫曼树;
    • 根据二叉树向左路径为0,向右路径为1的规则,可对各个字符完成编码,进而形成一个编码表
    • 根据编码表,将原字符串按顺序转换为二进制编码字符串
    • 8位为1个字节,将二进制编码字符串换算为十进制数(第一位为符号位),如 (bit)(1000 1000) => (byte)(-8)。依次执行后,形成一个字节数组,这就是压缩后的数据;
  • 哈夫曼编码解压的步骤:
    • 即压缩步骤的反向操作。

2、Java代码 -- 字符串的压缩与解压

// 用哈夫曼编码压缩一串字符串后,再解压显示

package DataStructures.Tree;

import java.util.*;

/**
 * @author yhx
 * @date 2020/10/17
 */
public class HuffmanCode {
    /**
     * 生成哈夫曼树对应的哈夫曼编码
     * 1、将哈夫曼编码存放在 Map<Byte,String> 中
     * 2、生成哈夫曼编码时,需要拼接路径,定义一个StringBuilder存储某个叶子节点的路径
     */
    static Map<Byte, String> huffmanCodes = new HashMap<>();
    static StringBuilder stringBuilder = new StringBuilder();

    public static void main(String[] args) {
        String str = "I like java very very like java, do you like java?";
        // 压缩
        byte[] huffmanCodeBytes = huffmanZip(str);
        // 解压
        byte[] sourceBytes = decode(huffmanCodes, huffmanCodeBytes);
        System.out.println("9、原来的字符串为:\n" + new String(sourceBytes));
    }

    /**
     * 接收字符串数组,转换为哈夫曼节点集合,节点统计了每个字符出现的次数
     *
     * @param bytes 字符串数组
     * @return 返回哈夫曼节点集合
     */
    private static List<HuffmanNode> getNodes(byte[] bytes) {
        // 1、创建一个ArrayList
        List<HuffmanNode> nodes = new ArrayList<>();

        /*
           2、统计每一个字符出现的次数
              键Key:字符,byte整型
              值Value:次数,Integer整型
         */
        HashMap<Byte, Integer> counts = new HashMap<>();
        for (byte ch : bytes) {
            // 获取每个字符在哈希表中值(次数)
            Integer count = counts.get(ch);
            // 如果值为空,说明该字符第一次被统计到,将该字符ch(Key)的次数count(Value)改为1
            if (count == null) {
                counts.put(ch, 1);
            }
            // 值不为空,说明该字符不是第一次出现,只需将出现次数加1
            else {
                counts.put(ch, count + 1);
            }
        }

        // 3、遍历哈希表,将每个“键值对”转成一个HuffmanNode对象,加入到Nodes集合中
        for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
            nodes.add(new HuffmanNode(entry.getKey(), entry.getValue()));
        }

        return nodes;
    }

    /**
     * 创建哈夫曼树
     *
     * @param nodes 节点集合
     * @return 返回树根节点
     */
    private static HuffmanNode createHuffmanTree(List<HuffmanNode> nodes) {
        while (nodes.size() > 1) {
            // 排序
            Collections.sort(nodes);
            // 取出最小的二叉树
            HuffmanNode leftNode = nodes.get(0);
            // 取出第二小的二叉树
            HuffmanNode rightNode = nodes.get(1);
            // 创建一棵新的二叉树,没有data,只有权值
            HuffmanNode parent = new HuffmanNode(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;
            // 移除两棵处理过的二叉树
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            // 将新的二叉树加入到nodes集合
            nodes.add(parent);
        }
        // 最后的节点就是哈夫曼树的根节点
        return nodes.get(0);
    }

    /**
     * 得到 node节点的所有叶子节点的哈夫曼编码,并存放到容器huffmanCodes中
     *
     * @param node          传入的节点(根节点)
     * @param code          叶子节点的路径。左子节点是0,右子节点是1
     * @param stringBuilder 拼接路径
     */
    private static void getCodes(HuffmanNode node, String code, StringBuilder stringBuilder) {
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        // 将code拼接到stringBuilder2后
        stringBuilder2.append(code);
        if (node != null) {
            // 判断当前node节点是否是叶子节点
            if (node.data == null) {
                // 左右递归处理
                getCodes(node.left, "0", stringBuilder2);
                getCodes(node.right, "1", stringBuilder2);
            }
            // 叶子节点
            else {
                // 说明找到了某个叶子节点,将对应的路径(哈夫曼编码)存入到容器huffmanCodes中
                huffmanCodes.put(node.data, stringBuilder2.toString());
            }
        }

    }

    /**
     * 为了调用方便,重载getCodes方法
     *
     * @param root 根节点
     * @return 返回哈夫曼编码表
     */
    private static Map<Byte, String> getCodes(HuffmanNode root) {
        if (root == null) {
            return null;
        }
        // 处理根节点的左右子树
        getCodes(root.left, "0", stringBuilder);
        getCodes(root.right, "1", stringBuilder);
        return huffmanCodes;
    }

    /**
     * 输入一个字符串数组,返回哈夫曼编码压缩后的 byte[]
     *
     * @param bytes        待压缩字符串数组
     * @param huffmanCodes 生成的哈夫曼编码表
     * @return 返回压缩结果
     */
    private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
        // 先利用huffmanCodes将字符数组bytes转为哈夫曼编码对应的字符串
        StringBuilder stringBuilder = new StringBuilder();
        for (byte b : bytes) {
            // 按顺序得到每个字符对应的哈夫曼编码,并将其拼接到一起
            stringBuilder.append(huffmanCodes.get(b));
        }
        System.out.println("5、根据哈夫曼编码表,将字符串转为二进制编码字符串:\n" + stringBuilder.toString() + "\n");


        // 统计返回byte[]的长度
        // 以下6行代码等价于:int len = (stringBuilder.length() + 7) / 8;
        int len;
        // 因为每8位对应一个byte,所以步长为8
        int stride = 8;
        if (stringBuilder.length() % stride == 0) {
            len = stringBuilder.length() / stride;
        } else {
            len = stringBuilder.length() / stride + 1;
        }

        // 存储压缩后的byte数组
        byte[] huffmanCodeBytes = new byte[len];
        // 记录是第几个byte
        int index = 0;
        for (int i = 0; i < stringBuilder.length(); i += stride) {
            String strByte;

            // 不足8位
            if (i + stride > stringBuilder.length()) {
                strByte = stringBuilder.substring(i);
            }
            // 位数足够
            else {
                strByte = stringBuilder.substring(i, i + stride);
            }

            // 将strByte转成一个byte,放入到huffmanCodeBytes数组中
            // parseInt(String s, int radix)函数:表示将s字符串(radix表示其进制)转为10进制整型
            huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
            index++;
        }
        return huffmanCodeBytes;
    }

    /**
     * 将以上方法封装,传入待压缩字符串,返回压缩后的哈夫曼编码字节数组
     *
     * @param str 待压缩字符串
     * @return 哈夫曼编码
     */
    private static byte[] huffmanZip(String str) {
        System.out.println("1、待压缩的字符串为:");
        System.out.println(str);
        System.out.println();

        // 将字符串中的字符存到byte整型数组中
        System.out.println("2、待压缩字符串转为ASCII码后为:");
        byte[] contentBytes = str.getBytes();
        System.out.println(Arrays.toString(contentBytes));
        System.out.println();

        // 将每个字符转为node节点
        List<HuffmanNode> nodes = getNodes(contentBytes);

        // 创建哈夫曼树
        HuffmanNode huffmanTreeRoot = createHuffmanTree(nodes);
        System.out.println("3、哈夫曼树前序遍历的结果:");
        huffmanTreeRoot.preOrder();
        System.out.println();

        // 根据哈夫曼树创建哈夫曼编码表
        System.out.println("4、生成哈夫曼编码表:");
        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
        System.out.println(huffmanCodes);
        System.out.println();

        // 根据哈夫曼编码表将字符串压缩为哈夫曼编码字节数组
        byte[] huffmanCodeBytes = zip(contentBytes, huffmanCodes);
        System.out.println("6、字符串转为哈夫曼编码字节数组:\n" + Arrays.toString(huffmanCodeBytes) + "\n");

        // 返回
        return huffmanCodeBytes;
    }

    /**
     * 将一个byte转成一个二进制字符串,如 8 --> 0000 1000
     *
     * @param b    字节
     * @param flag 标识是否需要补高位,true表示需要。
     *             如果是最后一个字节,不需要补高位。
     *             因为最后一个字节在压缩时不一定有8位,解码时如果强行补位就改变其值了。
     * @return 二进制字符串,按照补码返回
     */
    private static String byteToBitString(boolean flag, byte b) {
        int temp = b;
        if (flag) {
            /*
             按位或,用于将正数补高位
             256 二进制 => 1 0000 0000
             */
            temp = temp | 256;
        }
        String str = Integer.toBinaryString(temp);
        if (flag) {
            // 取后八位
            return str.substring(str.length() - 8);
        } else {
            return str;
        }
    }

    /**
     * 解码
     *
     * @param huffmanCodes 哈夫曼编码表
     * @param huffmanBytes 哈夫曼编码得到的字节数组
     * @return 原来字符串对应的数组
     */
    private 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]));
        }
        System.out.println("7、(哈夫曼字节数组 -> 二进制编码字符串)解码后:\n" + stringBuilder.toString() + "\n");

        // 解码
        Map<String, Byte> map = new HashMap<>();
        // 将哈夫曼编码表的键K值V反转
        for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
            map.put(entry.getValue(), entry.getKey());
        }
        System.out.println("8、哈夫曼编码表的键K值V反转后为:\n" + map + "\n");

        // 将二进制编码字符串转换为字符数组
        List<Byte> list = new ArrayList<>();
        for (int i = 0; i < stringBuilder.length(); ) {
            // 递增计数器
            int count = 1;
            boolean flag = true;
            Byte b = null;
            while (flag) {
                // 递增取字符串中的0或1
                String key = stringBuilder.substring(i, i + count);
                // 从编码表中找,是否有字符串对应的数值(ASCII)
                b = map.get(key);
                // 没有匹配到
                if (b == null) {
                    count++;
                } else {
                    flag = false;
                }
            }
            // 将匹配到的数值(ASCII)添加到集合
            list.add(b);
            i += count;
        }

        // 将集合中的数据放入数组,返回结果
        byte[] bs = new byte[list.size()];
        for (int i = 0; i < bs.length; i++) {
            bs[i] = list.get(i);
        }
        return bs;
    }

    /**
     * 前序遍历的方法
     *
     * @param root 根节点
     */
    private static void preOrder(HuffmanNode root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("哈夫曼树为空");
        }
    }
}


/**
 * 创建节点Node,带数据和权值
 */
class HuffmanNode implements Comparable<HuffmanNode> {
    Byte data;
    int weight;
    HuffmanNode left;
    HuffmanNode right;

    /**
     * 构造器
     *
     * @param data   以ASCII码存放数据,如'a' = 97
     * @param weight 权值,表示字符出现的次数
     */
    public HuffmanNode(Byte data, int weight) {
        this.data = data;
        this.weight = weight;
    }

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

    /**
     * 比较器,用于排序
     */
    @Override
    public int compareTo(HuffmanNode o) {
        // 升序排序
        return this.weight - o.weight;
    }

    /**
     * 重写方法:打印节点信息
     *
     * @return 打印形式
     */
    @Override
    public String toString() {
        return "Huffman Node [ data = " + data + " weight = " + weight + " ]";
    }
}

3、运行结果

1、待压缩的字符串为:
I like java very very like java, do you like java?

2、待压缩字符串转为ASCII码后为:
[73, 32, 108, 105, 107, 101, 32, 106, 97, 118, 97, 32, 118, 101, 114, 121, 32, 118, 101, 114, 121, 32, 108, 105, 107, 101, 32, 106, 97, 118, 97, 44, 32, 100, 111, 32, 121, 111, 117, 32, 108, 105, 107, 101, 32, 106, 97, 118, 97, 63]

3、哈夫曼树前序遍历的结果:
Huffman Node [ data = null weight = 50 ]
Huffman Node [ data = null weight = 21 ]
Huffman Node [ data = null weight = 10 ]
Huffman Node [ data = 101 weight = 5 ]
Huffman Node [ data = 118 weight = 5 ]
Huffman Node [ data = null weight = 11 ]
Huffman Node [ data = null weight = 5 ]
Huffman Node [ data = null weight = 2 ]
Huffman Node [ data = 44 weight = 1 ]
Huffman Node [ data = 117 weight = 1 ]
Huffman Node [ data = 105 weight = 3 ]
Huffman Node [ data = 97 weight = 6 ]
Huffman Node [ data = null weight = 29 ]
Huffman Node [ data = null weight = 12 ]
Huffman Node [ data = null weight = 6 ]
Huffman Node [ data = 106 weight = 3 ]
Huffman Node [ data = 107 weight = 3 ]
Huffman Node [ data = null weight = 6 ]
Huffman Node [ data = 108 weight = 3 ]
Huffman Node [ data = 121 weight = 3 ]
Huffman Node [ data = null weight = 17 ]
Huffman Node [ data = null weight = 7 ]
Huffman Node [ data = null weight = 3 ]
Huffman Node [ data = 63 weight = 1 ]
Huffman Node [ data = 111 weight = 2 ]
Huffman Node [ data = null weight = 4 ]
Huffman Node [ data = 114 weight = 2 ]
Huffman Node [ data = null weight = 2 ]
Huffman Node [ data = 100 weight = 1 ]
Huffman Node [ data = 73 weight = 1 ]
Huffman Node [ data = 32 weight = 10 ]

4、生成哈夫曼编码表:
{32=111, 97=011, 100=110110, 101=000, 105=0101, 73=110111, 106=1000, 107=1001, 44=01000, 108=1010, 111=11001, 114=11010, 117=01001, 118=001, 121=1011, 63=11000}

5、根据哈夫曼编码表,将字符串转为二进制编码字符串:
11011111110100101100100011110000110010111110010001101010111110010001101010111111010010110010001111000011001011010001111101101100111110111100101001111101001011001000111100001100101111000

6、字符串转为哈夫曼编码字节数组:
[-33, -46, -56, -16, -53, -28, 106, -7, 26, -65, 75, 35, -61, 45, 31, 108, -5, -54, 125, 44, -113, 12, -68, 0]

7、(哈夫曼字节数组 -> 二进制编码字符串)解码后:
11011111110100101100100011110000110010111110010001101010111110010001101010111111010010110010001111000011001011010001111101101100111110111100101001111101001011001000111100001100101111000

8、哈夫曼编码表的键K值V反转后为:
{110110=100, 11010=114, 110111=73, 1000=106, 1011=121, 1010=108, 01001=117, 011=97, 000=101, 111=32, 001=118, 0101=105, 01000=44, 1001=107, 11001=111, 11000=63}

9、原来的字符串为:
I like java very very like java, do you like java?

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值