数据结构——赫夫曼编码(算法)

基本介绍:
  • 1)、赫夫曼编码也译为哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,属于一种程序算法。
  • 2)、赫夫曼编码是赫夫曼树在电讯通信中经典的应用之一。
  • 3)、赫夫曼编码广泛地用于数据文件压缩,其压缩率通常在20%~90%之间。
  • 4)、赫夫曼码是可变字长编码(VLC) 的一种。Huffman于1952年提出一种编码方法,称之为最佳编码。
原理剖析:
  • 通信领域中信息的处理方式1-定长编码
  1. eg:content = “i like like like java do you like a java”,共40个字符(包括空格)
  2. 对应的二进制编码:01101001 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101010 01100001 01110110 01100001 00100000 01100100 01101111 00100000 01111001 01101111 01110101 00100000 01101100 01101001 01101011 01100101 00100000 01100001 00100000 01101010 01100001 01110110 01100001
  3. 对应的十进制编码:105 32 108 105 107 101 32 108 105 107 101 32 108 105 107 101 32 106 97 118 97 32 100 111 32 121 111 117 32 108 105 107 101 32 97 32 106 97 118 97
  4. 如果按照二进制来传递信息,信息长度为359(包括空格)
  • 通信领域中信息的处理方式2-变长编码
  1. eg:content = “i like like like java do you like a java”,共40个字符(包括空格)
  2. 各个字符对应的出现的次数:‘d’:1,‘y’:1,‘u’:1,‘j’:2,‘v’:2,‘o’:2,‘l’:4,‘k’:4,‘e’:4,‘i’:5,‘a’:5,’ ':9。
  3. 按各个字符出现的次数进行编码,原则是出现的次数越多的,则编码越小,比如space出现了9次,编码为0,其它依次类推:0=’’,1=‘a’,10=‘i’,11=‘e’,100=‘k’,101=‘l’,110=‘o’,111=‘v’,1000=‘j’,1001=‘u’,1010=‘y’,1011=‘d’。
  4. 按照上面给各个字符规定的编码,则我们在传输“i like like like java do you like a java”数据时,编码就是
    (i)10( )0(l)101(i)10(k)100…(以此类推下去)
  5. 字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码,即不能匹配到重复的编码,实际上在这个变长编码方式中是不符合前缀编码要求的,如10作为前缀,即可能是’k’(100=‘k’),也可能是’i’(101=‘l’)。
  • 通信领域中信息的处理方式3-赫夫曼编码
  1. eg:content = “i like like like java do you like a java”,共40个字符(包括空格)。
  2. 各个字符对应的出现的次数:‘d’:1,‘y’:1,‘u’:1,‘j’:2,‘v’:2,‘o’:2,‘l’:4,‘k’:4,‘e’:4,‘i’:5,‘a’:5,’ ':9。
  3. 按照上面字符出现的次数构建成一棵赫夫曼树次数作为权值赫夫曼树的构建原理,请移驾至此处
    在这里插入图片描述
  4. 根据赫夫曼树给各个字符规定编码,始终遵循一个原则:向左的路径为0,向右的路径为1
    ‘o’:1000 ‘u’:10010 ‘d’:100110 ‘y’:100111 ‘i’:101
    ‘a’:110 ‘k’:1110 ‘e’:1111 ‘j’:0000 ‘v’:0001
    ‘l’:001 ’ ':01
  5. 按照上面的赫夫曼编码,无损压缩对应的编码为:
    101 01 00110111101111 01 00110111101111 01 00110111101111 01 00001100001110 01
    1001101000 01 100111100010010 01 00110111101111 01 00001100001110
    长度:133
    说明
    1). 原来的长度359,压缩率为(359-133)/359=62.9%
    2). 此编码满足前缀编码,即字符的编码都不能是其他字符编码的前缀,不会造成匹配的多义性。
    注意
    赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是WPL是一样的,都是最小的。
思路解析:
  1. 传输的字符串转换成Byte[];
  2. 获取每个字符出现的次数;
  3. 构建赫夫曼树;
  4. 生成赫夫曼树对应的赫夫曼编码;
    4.1. 将赫夫曼编码存放在Map<Byte,String>中,形式32->01 97->100 100->11000等等
    4.2. 在生成赫夫曼编码表时,需要去拼接路径,定义一个StringBuilder,存储某个叶子节点的路径
    4.3. 使用赫夫曼编码对数据进行编码;
  5. 解码赫夫曼编码
    5.1. 将压缩完成的byte数组转换成对应的二进制字符串
    5.2. 赫夫曼编码对应的二进制字符串转换成原始数据
代码示例
public class HuffmanCodeDemo {
    /**
     * 用于编码,将赫夫曼编码存放在Map<Byte,String>中,形式 32->01 97->100 100->11000等等
     */
    private static Map<Byte, String> huffmanCodeMap = Maps.newHashMap();
    /**
     * 用于解码,将赫夫曼编码存放在Map<String,Byte>中,形式 01->32 100->97 11000->100等等
     */
    private static Map<String, Byte> huffmanDecodeMap = Maps.newHashMap();
    /**
     * 在生成赫夫曼编码表时,需要去拼接路径,定义一个StringBuilder,存储某个叶子节点的路径
     */
    private static StringBuilder stringBuilder = new StringBuilder();

    public static void main(String[] args) {
        String content = "i like like like java do you like a java";
        byte[] bytes = huffmanCodeZip(content);
        System.out.println(Arrays.toString(bytes));
        bytes = huffmanCodeUnzip(bytes);
        System.out.println(new String(bytes));
    }
    /**
     * 完成数据的解码
     * 1、将压缩完成的byte数组转换成对应的二进制字符串
     *    [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
     *    => 10101000101111111100100010111111110010001011111111001001010011011100011100000110111010001111001
     *       01000101111111100110001001010011011100
     * 2、赫夫曼编码对应的二进制字符串转换成原始数据
     *   10101000101111111100100010111111110010001011111111001001010011011100011100000110111010001111001
     *   01000101111111100110001001010011011100
     *   => i like like like java do you like a java
     */
    /**
     * 赫夫曼编码解码
     *
     * @param huffmanZipCodeData 通过huffman coding压缩完成的二进制编码
     * @return 压缩前字符传对应的数组
     */
    private static byte[] huffmanCodeUnzip(byte[] huffmanZipCodeData) {
        if (huffmanZipCodeData == null || huffmanZipCodeData.length == 0) {
            return null;
        }
        /*1、将压缩完成的byte数组转换成对应的二进制字符串*/
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0, length = huffmanZipCodeData.length; i < length; i++) {
            /*判断是否是最后一个字符*/
            boolean flag = (i == (length - 1));
            stringBuilder.append(byteToString(!flag, huffmanZipCodeData[i]));
        }
        /*2、通过解码表,获取对应的byte[]*/
        return getHuffmanDecodeList(stringBuilder);
    }

    /**
     * 根据字符串获取对应的Byte集合
     *
     * @param value
     * @return
     */
    private static byte[] getHuffmanDecodeList(StringBuilder value) {
        if (value == null) {
            return null;
        }
        List<Byte> byteList = Lists.newArrayList();
        /*扫描value的每一个字符,组成key,通过huffmanDecodeMap中获取对应的key值是否存在value*/
        for (int index = 0; index < value.length(); ) {
            int count = 1;
            boolean flag = true;
            Byte tempByteValue = null;
            while (flag) {
                /*通过移动count,直到获取到key对应的value*/
                String key = value.substring(index, index + count);
                tempByteValue = huffmanDecodeMap.get(key);
                if (tempByteValue == null) {
                    count++;
                } else {
                    flag = false;
                }
            }
            byteList.add(tempByteValue);
            /*索引移动草count,进行下一次匹配*/
            index += count;
        }
        /*将list数据放入数组返回*/
        byte[] bytes = new byte[byteList.size()];
        for (int i = 0, length = byteList.size(); i < length; i++) {
            bytes[i] = byteList.get(i);
        }
        return bytes;
    }

    /**
     * 将一个byte转换成二进制字符串
     *
     * @param flag      标志是否需要补高位,如果是true,需要补高位,false不需要补高位,最后一个字节不需要补高位
     * @param byteValue byte值
     * @return byte值转换完成后对应的字符串(按补码的形式返回)
     */
    private static String byteToString(boolean flag, byte byteValue) {
        int temp = byteValue;
        if (flag) {
            /*按位或256,1 0000 0000 | 0000 0001 = 1 0000 0001 */
            temp |= 256;
        }
        /*返回temp对应的二进制补码*/
        String tempString = Integer.toBinaryString(temp);
        if (flag) {
            return tempString.substring(tempString.length() - 8);
        } else {
            return tempString;
        }
    }

    /**
     * 获取通过huffman编码压缩完成后的数据
     *
     * @param content 原始字符串
     * @return
     */
    private static byte[] huffmanCodeZip(String content) {
        /*1、字符串转换成字符数组*/
        byte[] contentBytes = content.getBytes();
        /*2、创建节点列表*/
        List<CodeNode> codeNodes = getCodeNodes(contentBytes);
        /*3、创建一棵huffman树*/
        CodeNode codeNode = creatHuffmanTree(codeNodes);
        /*4、获取huffman编码表*/
        getCodes(codeNode);
        /*5、获取数据压缩数组*/
        return zip(contentBytes);
    }

    /**
     * 将字符串对应的Byte[]数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码压缩后的Byte[]
     *
     * @param bytes 原始字符串对应的Byte[]
     * @return 返回赫夫曼编码处理后的Byte[]
     * String content = "i like like like java do you like a java"; =>  byte[] contentBytes = content.getBytes();
     * 返回的字符串是:10101000101111111100100010111111110010001011111111001001010011011100011100000110111010001111001
     * 01000101111111100110001001010011011100
     * => 对应的Byte[] huffmanCodeBytes,级8位对应一个Byte,放入到huffmanCodeBytes
     * huffmanCodeBytes[0] = 10101000(补码)
     * => byte 【10101000 => 10101000 - 1 = 10100111(反码) => 11011000】
     * huffmanCodeBytes[0] = -88
     */
    private static byte[] zip(byte[] bytes) {
        if (bytes == null || bytes.length == 0) {
            return null;
        }
        /*1、将字符串转换成对应的赫夫曼编码字符串*/
        StringBuilder stringBuilder = new StringBuilder();
        for (byte item : bytes) {
            stringBuilder.append(huffmanCodeMap.get(item));
        }
        /**
         * 2、求取赫夫曼编码的字符串长度,需要向上取整,类似于下面的步骤
         * int len;
         * if (stringBuilder.length() % 8 == 0) {
         *      len = stringBuilder.length() / 8;
         * } else {
         *      len = stringBuilder.length() / 8 + 1;
         * }
         */
        int len = (stringBuilder.length() + 7) / 8;
        /*3、将赫夫曼编码字符串转换成byte[],8位数据压缩成一个数据放入到数组中*/
        byte[] huffmanCodeBytes = new byte[len];
        int index = 0;
        String tempByte;
        /*因为是8位对应一个Byte,所以步长是8*/
        for (int i = 0; i < stringBuilder.length(); i += 8) {
            if ((i + 8) > stringBuilder.length()) {
                tempByte = stringBuilder.substring(i);
            } else {
                tempByte = stringBuilder.substring(i, i + 8);
            }
            huffmanCodeBytes[index++] = (byte) Integer.parseInt(tempByte, 2);
        }
        return huffmanCodeBytes;
    }

    /**
     * 方法重载
     *
     * @param codeNode
     */
    private static void getCodes(CodeNode codeNode) {
        if (codeNode == null) {
            return;
        }
        getCodes(codeNode.getLeft(), "0", stringBuilder);
        getCodes(codeNode.getRight(), "1", stringBuilder);
    }

    /**
     * 将传入的node节点的所有叶子节点的赫夫曼编码得到,并放入到huffmanCodeMap中
     *
     * @param codeNode      传入的节点
     * @param code          路径,约定左子节点是 0,右子节点是 1
     * @param stringBuilder 用于拼接路径
     */
    private static void getCodes(CodeNode codeNode, String code, StringBuilder stringBuilder) {
        StringBuilder tempCode = new StringBuilder(stringBuilder);
        /*1、将code加入到tempCode中去*/
        tempCode.append(code);
        /*2、如果节点为空,不出力,直接返回*/
        if (codeNode == null) {
            return;
        }
        /*3、判断当前节点是否是叶子节点*/
        if (codeNode.getValue() == null) {
            /*向左、向右递归处理*/
            getCodes(codeNode.getLeft(), "0", tempCode);
            getCodes(codeNode.getRight(), "1", tempCode);
        } else {
            /*表示找到某个叶子节点的最后*/
            huffmanCodeMap.put(codeNode.getValue(), tempCode.toString());
            huffmanDecodeMap.put(tempCode.toString(), codeNode.getValue());
        }
    }

    private static List<CodeNode> getCodeNodes(byte[] contentBytes) {
        if (ObjectUtils.isEmpty(contentBytes)) {
            return Lists.newArrayList();
        }
        Map<Byte, Integer> contentMap = Maps.newHashMap();
        Integer value;
        for (Byte item : contentBytes) {
            value = contentMap.getOrDefault(item, 0);
            contentMap.put(item, value + 1);
        }
        List<CodeNode> codeNodes = Lists.newArrayList();
        for (Byte item : contentMap.keySet()) {
            codeNodes.add(new CodeNode(item, contentMap.get(item)));
        }
        return codeNodes;
    }

    private static CodeNode creatHuffmanTree(List<CodeNode> codeNodes) {
        if (CollectionUtils.isEmpty(codeNodes)) {
            return null;
        }
        while (codeNodes.size() > 1) {
            Collections.sort(codeNodes);
            /*从小到大取第一、二个元素,如果是从大到小的排序,就取第size-1、size-2个元素*/
            CodeNode leftNode = codeNodes.get(0);
            CodeNode rightNode = codeNodes.get(1);
            CodeNode parent = new CodeNode(null, leftNode.getWeight() + rightNode.getWeight());
            parent.setLeft(leftNode);
            parent.setRight(rightNode);
            codeNodes.remove(leftNode);
            codeNodes.remove(rightNode);
            codeNodes.add(parent);
        }
        return codeNodes.get(0);
    }
}

@Data
class CodeNode implements Comparable<CodeNode> {
    private Byte value;// 存放数值本身
    private Integer weight;// 权值,用于存放数据出现的次数
    private CodeNode left;
    private CodeNode right;

    public CodeNode(Byte value, Integer weight) {
        this.value = value;
        this.weight = weight;
    }

    /*从小到大排序*/
    @Override
    public int compareTo(CodeNode o) {
        return this.weight - o.weight;
    }

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值