赫夫曼树的经典应用——赫夫曼编码解码

之前利用赫夫曼树来对字符串进行压缩,相当于对字符串加密。现在需要利用赫夫曼编码来进行解码,也就是解密。

如果还不了解赫夫曼编码的小伙伴可以先看上篇文章——利用哈夫曼树实现哈夫曼编码进行字符串压缩

之前利用赫夫曼编码得到了字符串的byte数组,现在首先需要将byte数组中的值转为二进制并且还要转成补码(因为计算机中存的都是二进制补码)

转为二进制的代码如下(有注释):

    /**
     * 将十进制数转为至少八位的二进制数
     *
     * @param i    需要转换的十进制数
     * @param flag 作为是否要补到8位的标志,true表示要补全
     * @return      将二进制按字符串形式返回
     */
    public static StringBuffer tenToBinary(int i, boolean flag) {
        //拼接二进制
        StringBuffer stringBuffer1 = new StringBuffer();
        StringBuffer stringBuffer2 = new StringBuffer();
        int temp;
        //先存储绝对值
        int cur = Math.abs(i);
        //得到二进制的反序,之后直接翻转字符串就可以了
        while (cur != 0) {
            temp = cur % 2;
            cur = cur / 2;
            stringBuffer1.append(temp);
        }
        //如果不足八位而且不是数组中的最后一个值则补0
        while (stringBuffer1.length() < 8 && flag) {
            stringBuffer1.append(0);
        }
        //如果是负数,需要把最高位改成符号位
        if (i < 0) {
            String substring = stringBuffer1.substring(0, stringBuffer1.length() - 1);
            stringBuffer2.append(substring).append(1);
        } else {
            stringBuffer2 = stringBuffer1;
        }
        //翻转字符串,之前得到是二进制的反序
        stringBuffer2 = stringBuffer2.reverse();
        //返回得到的二进制字符串
        return stringBuffer2;
    }

 

再得到二进制的补码: 

    /**
     * 得到十进制数对应的二进数的补码
     *
     * @param b    十进制数
     * @param flag 作为是否要补到8位的标志,true表示要补全
     * @return 字符串形式的二进制补码
     */
    public static String byteToBitString(byte b, boolean flag) {
        int temp = b;
        //将temp转为二进制
        StringBuffer strBinary = tenToBinary(temp, flag);
        //测试
        //System.out.println(strBinary);
        // 如果是负数,得到temp对应的二进制补码,正数三码合一
        if (temp < 0) {
            //先转为反码
            for (int i = 1; i < strBinary.length(); i++) {
                if (strBinary.charAt(i) == '0') {
                    strBinary = strBinary.replace(i, i + 1, "1");
                } else {
                    strBinary = strBinary.replace(i, i + 1, "0");
                }
            }
            //再加一变成补码
            int i = strBinary.length() - 1;
            //如果是1就需要进位,再置为0
            while (strBinary.charAt(i) == '1') {
                strBinary = strBinary.replace(i, i + 1, "0");
                i--;
            }
            //最后别忘了将0改为1
            strBinary = strBinary.replace(i, i + 1, "1");
        }
        return strBinary.toString();
    }

 

 然后就需要循环将byte数组中的所有数的二进制补码都拿到并且存在可拼接的字符串中,

得到的就是之前用赫夫曼编码压缩成的二进制数

代码如下:

    /**
     * 将压缩后的编码解开,也就是解码
     *
     * @param huffmanCodes 赫夫曼编码表 map
     * @param huffmanBytes 赫夫曼编码得到的字节数组
     * @param strSize 字符串经过赫夫曼压缩后的大小
     * @return 就是原来的字符串对应的数组
     */
    private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes,int strSize) {

        StringBuffer stringBuffer1 = new StringBuffer();
        //解码
        for (int i = 0; i < huffmanBytes.length; i++) {
            //如果是最后一个值并且二进制个数不能被8整除则不需要补全
            if (i == huffmanBytes.length - 1 && strSize % 8 != 0)
                stringBuffer1.append(byteToBitString( huffmanBytes[i],false));
            else
                stringBuffer1.append(byteToBitString(huffmanBytes[i],true));
        }
        //测试
        System.out.println("输出字符串解压成赫夫曼编码后对应的二进制编码:" + stringBuffer1 + "长度为:" + stringBuffer1.length());
        Map<String ,Byte> map = new HashMap<>();
        for (Map.Entry<Byte, String> stringByteEntry:huffmanCodes.entrySet()) {
            map.put(stringByteEntry.getValue(),stringByteEntry.getKey());
        }
        //测试
        //System.out.println(map);
        List<Byte> list = new ArrayList<>();
        for (int i = 0; i < stringBuffer1.length();) {
            int count = 0;
            Byte b = null;
            //扫描字符串
            while (true){
                //递增的取出key
                String key = "";
                key = stringBuffer1.substring(i,i+count);
                // 如果map集合中没有对应的二进制编码就count递增
                b = map.get(key);
                if (b == null){
                    //说明没取到
                    count++;
                }else {
                    break;
                }
            }
            list.add(b);
            i += count;
        }

        //循环结束后将list集合中的字符串存储到byte数组中,并返回
        byte b[] = new byte[list.size()];
        for (int i = 0; i < b.length; i++) {
            b[i] = list.get(i);
        }
        return b;
    }

将需要的所有方法封装在一个方法中,便于main方法调用

    /**
     * 将之前写的方法封装在一个方法之中,便于调用
     *
     * @param str 原始的字符串
     * @return 经过赫夫曼编码压缩后对应的字节数组
     */
    public static void huffmanAll(String str) {

        // 把输入的字符串转为byte数组,在byte数组中存储的是字符对应的ASCII码值
        byte[] strBytes = str.getBytes();
        System.out.println(str + ",压缩成赫夫曼编码前对应的byte数组:" + Arrays.toString(strBytes));
        //计算压缩前的字符串有多少位二进制数
        int compressionBeforeCodeSize = str.length() * 8 + str.length() - 1;
        System.out.println(str + ",压缩前的字符串大小:" + compressionBeforeCodeSize);
        //统计字符串中每个字符出现的次数和空格出现次数并存入Node节点中
        List<Node> nodeList = totalCharCounts(str);
        //创建huffman树
        Node root = createHuffmanTree(nodeList);
        //得到压缩后的编码
        getHuffmanCompressionCode(root, "", stringBuffer);
        //输出赫夫曼编码表
        System.out.println(str + ",对应的赫夫曼编码表:");
        System.out.println(huffmanCodes);
        //得到压缩后的字符串大小
        int compressionAfterCodeSize = getStrCodeSize();
        System.out.println(str + ",压缩后的字符串大小:" + compressionAfterCodeSize);
        //可以算出压缩率是多少
        double compressionRadio = (compressionBeforeCodeSize - compressionAfterCodeSize) * 1.0 / compressionBeforeCodeSize;
        System.out.println(str + ",压缩成赫夫曼编码的压缩率为:" + compressionRadio);
        byte[] bytes = zip(strBytes, huffmanCodes);

        //解码
        byte[] decodeByte = decode(huffmanCodes, bytes,compressionAfterCodeSize);
        System.out.println("解码后的字符串为:" + new String(decodeByte));

        return ;
    }

 以下是编码与解码的全部方法:

import java.util.*;

/**
 * 实现huffman编码
 */
public class HuffmanCode {

    //将赫夫曼编码表存放在Map<Byte,String>中
    public static Map<Byte, String> huffmanCodes = new HashMap<>();
    //需要定义一个StringBuffer来存储某个节点的路径对于的编码
    public static StringBuffer stringBuffer = new StringBuffer();
    //创建一个map,来保存每个字符以及他对应出现的次数
    public static Map<Character, Integer> map = new HashMap<>();

    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);
        System.out.println("输入字符串:");
        //scanner.next()方法不能输入空格,例如输入: aaa bbb实际上只能接收到aaa,空格后面的字符串都接收不到
        //所以需要用scanner,nextLine()方法来接收字符串
        String str = scanner.nextLine();
        //调用总的方法,直接输出解码后的值
        huffmanAll(str);

    }

    /**
     * 将压缩后的编码解开,也就是解码
     *
     * @param huffmanCodes 赫夫曼编码表 map
     * @param huffmanBytes 赫夫曼编码得到的字节数组
     * @param strSize 字符串经过赫夫曼压缩后的大小
     * @return 就是原来的字符串对应的数组
     */
    private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes,int strSize) {

        StringBuffer stringBuffer1 = new StringBuffer();
        //解码
        for (int i = 0; i < huffmanBytes.length; i++) {
            //如果二进制位的个数能被8整除也
            if (i == huffmanBytes.length - 1 && strSize % 8 != 0)
                stringBuffer1.append(byteToBitString( huffmanBytes[i],false));
            else
                stringBuffer1.append(byteToBitString(huffmanBytes[i],true));
        }
        //测试
        System.out.println("输出字符串解压成赫夫曼编码后对应的二进制编码:" + stringBuffer1 + "长度为:" + stringBuffer1.length());
        Map<String ,Byte> map = new HashMap<>();
        for (Map.Entry<Byte, String> stringByteEntry:huffmanCodes.entrySet()) {
            map.put(stringByteEntry.getValue(),stringByteEntry.getKey());
        }
        //测试
        System.out.println(map);
        List<Byte> list = new ArrayList<>();
        for (int i = 0; i < stringBuffer1.length();) {
            int count = 0;
            Byte b = null;
            //扫描字符串
            while (true){
                //递增的取出key
                String key = "";
                key = stringBuffer1.substring(i,i+count);
                b = map.get(key);
                if (b == null){
                    //说明没取到
                    count++;
                }else {
                    break;
                }
            }
            list.add(b);
            i += count;
        }

        //循环结束后将list集合中的字符串存储到byte数组中,并返回
        byte b[] = new byte[list.size()];
        for (int i = 0; i < b.length; i++) {
            b[i] = list.get(i);
        }
        return b;
    }

    /**
     * 得到十进制数对应的二进数的补码
     *
     * @param b    十进制数
     * @param flag 作为是否要补到8位的标志,true表示要补全
     * @return 字符串形式的二进制补码
     */
    public static String byteToBitString(byte b, boolean flag) {
        int temp = b;
        //将temp转为二进制
        StringBuffer strBinary = tenToBinary(temp, flag);
        //测试
        //System.out.println(strBinary);
        // 得到temp对应的二进制补码
        if (temp < 0) {
            //先转为反码
            for (int i = 1; i < strBinary.length(); i++) {
                if (strBinary.charAt(i) == '0') {
                    strBinary = strBinary.replace(i, i + 1, "1");
                } else {
                    strBinary = strBinary.replace(i, i + 1, "0");
                }
            }
            //再加一变成补码
            int i = strBinary.length() - 1;
            while (strBinary.charAt(i) == '1') {
                strBinary = strBinary.replace(i, i + 1, "0");
                i--;
            }
            strBinary = strBinary.replace(i, i + 1, "1");
        }
        return strBinary.toString();
    }

    /**
     * 将十进制数转为至少八位的二进制数
     *
     * @param i    需要转换的十进制数
     * @param flag 作为是否要补到8位的标志,true表示要补全
     * @return      将二进制按字符串形式返回
     */
    public static StringBuffer tenToBinary(int i, boolean flag) {
        //拼接二进制
        StringBuffer stringBuffer1 = new StringBuffer();
        StringBuffer stringBuffer2 = new StringBuffer();
        int temp;
        int cur = Math.abs(i);
        while (cur != 0) {
            temp = cur % 2;
            cur = cur / 2;
            stringBuffer1.append(temp);
        }
        //如果不足八位则补0
        while (stringBuffer1.length() < 8 && flag) {
            stringBuffer1.append(0);
        }
        if (i < 0) {
            String substring = stringBuffer1.substring(0, stringBuffer1.length() - 1);
            stringBuffer2.append(substring).append(1);
        } else {
            return stringBuffer1.reverse();
        }
        //翻转字符串,之前得到是二进制的反序
        stringBuffer2 = stringBuffer2.reverse();

        return stringBuffer2;
    }


    /**
     * 将之前写的方法封装在一个方法之中,便于调用
     *
     * @param str 原始的字符串
     * @return 经过赫夫曼编码压缩后对应的字节数组
     */
    public static void huffmanAll(String str) {

        // 把输入的字符串转为byte数组,在byte数组中存储的是字符对应的ASCII码值
        byte[] strBytes = str.getBytes();
        System.out.println(str + ",压缩成赫夫曼编码前对应的byte数组:" + Arrays.toString(strBytes));
        //计算压缩前的字符串有多少位二进制数
        int compressionBeforeCodeSize = str.length() * 8 + str.length() - 1;
        System.out.println(str + ",压缩前的字符串大小:" + compressionBeforeCodeSize);
        //统计字符串中每个字符出现的次数和空格出现次数并存入Node节点中
        List<Node> nodeList = totalCharCounts(str);
        //创建huffman树
        Node root = createHuffmanTree(nodeList);
        //得到压缩后的编码
        getHuffmanCompressionCode(root, "", stringBuffer);
        //输出赫夫曼编码表
        System.out.println(str + ",对应的赫夫曼编码表:");
        System.out.println(huffmanCodes);
        //得到压缩后的字符串大小
        int compressionAfterCodeSize = getStrCodeSize();
        System.out.println(str + ",压缩后的字符串大小:" + compressionAfterCodeSize);
        //可以算出压缩率是多少
        double compressionRadio = (compressionBeforeCodeSize - compressionAfterCodeSize) * 1.0 / compressionBeforeCodeSize;
        System.out.println(str + ",压缩成赫夫曼编码的压缩率为:" + compressionRadio);
        byte[] bytes = zip(strBytes, huffmanCodes);

        //解码
        byte[] decodeByte = decode(huffmanCodes, bytes,compressionAfterCodeSize);
        System.out.println("解码后的字符串为:" + new String(decodeByte));

        return ;
    }

    /**
     * @return 得到压缩后的赫夫曼编码大小
     */
    public static int getStrCodeSize() {
        int size = 0;
        //将两个map集合都转为set集合
        Set<Map.Entry<Character, Integer>> mapSet = map.entrySet();
        Set<Map.Entry<Byte, String>> huffmanMapSet = huffmanCodes.entrySet();
        //循环两个set集合
        for (Map.Entry<Character, Integer> set1 : mapSet) {
            for (Map.Entry<Byte, String> set2 : huffmanMapSet) {
                //如果两个set的key相同就将他们的value相乘,只是需要注意存储huffman编码中的是字符串,需要乘字符串的长度
                if ((byte) set1.getKey().charValue() == set2.getKey()) {
                    size = size + set1.getValue() * (set2.getValue().length());
                    //节约时间,之间退出内循环。因为不可能有一对多的关系。
                    break;
                }
            }
        }
        return size;
    }

    /**
     * 根据huffman树来进行数据编码压缩
     * 思路:
     * 1、只要向左子树走就代表0,向右子树走就代表1
     * 2、从头节点走到对于字符在的节点位置的路径对于的0和1组成的二进制编码就是压缩后该字符对于的编码
     * 3、需要定义一个StringBuffer来存储某个节点的路径对于的编码
     * 4、将赫夫曼编码表存放在Map<Byte,String>中
     *
     * @param node         huffman树的根节点
     * @param stringBuffer 用于拼接路径
     * @param code         路径:左子节点是0,右子节点是1
     * @return
     */
    private static void getHuffmanCompressionCode(Node node, String code, StringBuffer stringBuffer) {
        StringBuffer stringBuffer1 = new StringBuffer(stringBuffer);
        stringBuffer1.append(code);
        //如果为空,不进行处理
        if (node != null) {
            //判断node是叶子节点还是非叶子节点
            if (node.data == null) {
                //非叶子节点
                //向左递归
                getHuffmanCompressionCode(node.left, "0", stringBuffer1);
                //向右递归
                getHuffmanCompressionCode(node.right, "1", stringBuffer1);
            } else {
                //叶子节点
                //说明这条路走到尾了,将路径编码存入map中
                huffmanCodes.put(node.data, stringBuffer1.toString());
            }
        }
    }

    /**
     * //统计字符串中每个字符出现的次数和空格出现次数
     *
     * @param str 字符串
     * @return 返回一个排好序的Node集合
     */
    public static List<Node> totalCharCounts(String str) {

        for (int i = 0; i < str.length(); i++) {
            char ch = str.charAt(i);
            Integer count = map.get(ch);
            if (count == null) {
                count = 0;
            }
            map.put(ch, count + 1);
        }
        //遍历map,将map中的数据存入Node节点中
        //先将map转为set集合
        Set<Map.Entry<Character, Integer>> mapSet = map.entrySet();
        //观察测试输出
        //System.out.println(mapSet);
        List<Node> nodeList = new ArrayList<>();
        //遍历set
        for (Map.Entry<Character, Integer> set : mapSet) {
            // 将map中的数据存入Node节点中
            Node node = new Node((byte) set.getKey().charValue(), set.getValue());
            // 将node存入集合中
            nodeList.add(node);
            //System.out.println(set.getKey() + " = " + set.getValue());
        }
        //排序
        Collections.sort(nodeList);
        //测试
        //System.out.println(nodeList);
        return nodeList;
    }

    /**
     * 创建huffman树
     *
     * @param nodeList 排好序的集合
     * @return 返回huffman树的根节点
     */
    public static Node createHuffmanTree(List<Node> nodeList) {
        //循环创建huffman树
        while (nodeList.size() > 1) {
            //1、每次取出集合中的前两个节点
            Node left = nodeList.get(0);
            Node right = nodeList.get(1);
            //2、将他们的权值相加构成一个新的节点并作为他们的父节点
            Node parent = new Node(null, left.weight + right.weight);
            parent.left = left;
            parent.right = right;
            //3、删除已经处理过的节点
            nodeList.remove(left);
            nodeList.remove(right);
            //4、将新的节点存入集合中
            nodeList.add(parent);
            //5、重新给集合排序,循环这5步即可,直到集合中只有一个节点,这就是huffman树的根节点
            Collections.sort(nodeList);
            //观察测试输出
            //System.out.println(nodeList);
        }
        //返回huffman树的根节点
        return nodeList.get(0);
    }

    /**
     * 编写一个方法,将字符串对应的byte[]数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码压缩后的byte[]
     *
     * @param bytes        这时原始的字符串对应的 byte[]
     * @param huffmanCodes 生成的赫夫曼编码 map
     * @return 返回赫夫曼编码处理后的 byte[]
     * 举例: String content = "i like like like java do you like a java"; =》 byte[] contentBytes = content.getBytes();
     * 返 回 的 是 字 符 串:
     * "1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000
     * 101111111100110001001010011011100"
     * => 对应的 byte[] huffmanCodeBytes ,即 8 位对应一个 byte,放入到 huffmanCodeBytes
     * huffmanCodeBytes[0] = 10101000(补码) => byte [推导 10101000=> 10101000 - 1 => 10100111(反
     * 码)=> 11011000(源码) = -88 ]
     */
    public static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
        //1、先利用赫夫曼编码表将传进来的bytes数组转为压缩后的编码
        StringBuffer stringBuffer1 = new StringBuffer();
        for (byte b : bytes) {
            stringBuffer1.append(huffmanCodes.get(b));
        }
        //输出字符串压缩成赫夫曼编码后对应的二进制编码
        System.out.println("输出字符串压缩成赫夫曼编码后对应的二进制编码:" + stringBuffer1 + "长度为:" + stringBuffer1.length());

        //获取byte数组的长度,Math.ceil()表示向上取整
        int len = (int) Math.ceil(stringBuffer1.length() * 1.0 / 8);
        //也可以用下面的方法获取长度
        /*if(stringBuffer1.length() % 8 == 0) {
            len = stringBuffer1.length() / 8;
        } else {
            len = stringBuffer1.length() / 8 + 1;
        }*/
        //测试
        //System.out.println(stringBuffer1.length());
        //System.out.println(len);
        byte[] huffmanBytes = new byte[len];
        int index = 0;
        for (int i = 0; i < stringBuffer1.length(); i = i + 8) {
            String strByte;
            if (i + 8 > stringBuffer1.length()) {
                //从i取到字符串最后一个字符
                strByte = stringBuffer1.substring(i);
            } else {
                //一次截取8个
                strByte = stringBuffer1.substring(i, i + 8);
            }
            //将 strByte 转成一个 byte,放入到 huffmanBytes中
            //该方法是将strByte对应的01字符串传换为十进制
            //第二个参数表示基数(radix),表示转换为radix进制
            huffmanBytes[index] = (byte) Integer.parseInt(strByte, 2);
            index++;
        }
        return huffmanBytes;
    }

}

以下是我的测试结果,有些情况会出现bug(实在改不出来了),比如字符串中不能有中文

 1、输入的字符串为:i like like like java do you like a java

 2、输入的字符串为: hello world java

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

终有弱水替沧海i

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值