一:基本介绍

        哈夫曼编码也翻译为    赫夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法 赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。 赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间 赫夫曼码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,称之为最佳编码

1.1:原理剖析

        1:通信领域中信息的处理方式1-定长编码

将 i like like like java do you like a java 定长编码       // 共40个字符(包括空格)  

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  //对应Ascii码

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 //对应的二进制 按照二进制来传递信息,总的长度是  359   (包括空格)

        2:  通信领域中信息的处理方式2-变长编码

i like like like java do you like a java       // 共40个字符(包括空格)

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=a, 10=i, 11=e, 100=k, 101=l, 110=o, 111=v, 1000=j, 1001=u, 1010=y, 1011=d  

说明:按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小,比如 空格出现了9 次, 编码为0 ,其它依次类推.

按照上面给各个字符规定的编码,则我们在传输  "i like like like java do you like a java" 数据时,编码就是 10010110100...  

字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码,例如 1=a,110=o,那么a是o的前缀所以该编码不是前缀编码

        3:通信领域中信息的处理方式3-赫夫曼编码

i like like like java do you like a java       // 共40个字符(包括空格)

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 

编码如下: o: 1000   u: 10010  d: 100110  y: 100111  i: 101 a : 110     k: 1110    e: 1111       j: 0000       v: 0001 l: 001          : 01

按照上面的赫夫曼编码,我们的"i like like like java do you like a java"   字符串对应的编码为 (注意这里我们使用的无损压缩) 1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110

长度为 : 133

说明: 原来长度是  359 , 压缩了  (359-133) / 359 = 62.9% 此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性

注意, 这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl 是一样的,都是最小的,字符总长度相同。 比如: 我们让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个和最前面

二:案例——哈夫曼压缩

功能: 根据赫夫曼编码压缩数据的原理,需要创建 "i like like like java do you like a java" 对应的赫夫曼树

思路: (1) CodeNode{ data (存放数据), weight (权值), left  和 right }                                               (2) 得到  "i like like like java do you like a java"   对应的 byte[] 数组                                             (3)  编写一个方法,将准备构建赫夫曼树的Node 节点放到 List  , 形式 [CodeNode[date=97 ,weight = 5], CodeNode[date=32,weight = 9]......],  体现 d:1 y:1 u:1 j:2  v:2  o:2  l:4  k:4  e:4 i:5  a:5   :9                                                                                                                                           (4) 可以通过List 创建对应的赫夫曼树                                                                                          (5)根据哈夫曼树生成哈夫曼编码表,左路径为0,右路径为1                                                      (6)将转换后的字符串转换为字节数组

package com.atgguigu.huffmanTree;

import java.util.*;

public class HuffmanCode {
    static Map<Byte,String> hCode = new HashMap<>();  //存储字符对应编码
    static StringBuilder stringBuilder = new StringBuilder();   //拼接路径生成编码

    public static void main(String[] args) {
        String s = "i like like like java do you like a java";
        byte[] bytes = s.getBytes();
        System.out.println(Arrays.toString(bytes));
        System.out.println("没有压缩的原长度:"+bytes.length);

        byte[] zip = zip(s);
        System.out.println(Arrays.toString(zip));
        System.out.println("哈夫曼编码压缩后的长度:"+zip.length);


    }
    /**
     * 封装方法
     */
    private static byte[] zip(String s){
        byte[] bytes = s.getBytes(); //获取字节数组
        List<CodeNode> nodes = getNodes(bytes);  //构建集合存储字符和权值
        CodeNode huffmanTree = createHuffmanTree(nodes);  //构建哈夫曼树
        getCode(huffmanTree);  //根据哈夫曼树获取对应编码表

        byte[] zip = zip(bytes, hCode);  //将字节数组转化为哈夫曼编码字节数组
        return zip;
    }

    /**
     * 统计字符数目构建对象列表
     * @param bytes
     * @return
     */
    private static List<CodeNode> getNodes(byte[] bytes){
        ArrayList<CodeNode> nodes = new ArrayList<>();
        Map<Byte,Integer> map = new HashMap<>();

        //统计每个字符数量
        for (byte b : bytes) {
            Integer count = map.get(b);
            if(count == null){
                map.put(b,1);
            }else {
                map.put(b,count+1);
            }
        }
        //构建CodeNode对象
        for(Map.Entry<Byte,Integer> entry: map.entrySet()){
            nodes.add(new CodeNode(entry.getKey(),entry.getValue()));
        }
        return  nodes;
    }

    //创建哈夫曼树
    private static CodeNode createHuffmanTree(List<CodeNode> codeNodes){

        while (codeNodes.size() > 1){
            Collections.sort(codeNodes);

            CodeNode leftNode = codeNodes.get(0);
            CodeNode rightNode = codeNodes.get(1);

            CodeNode parent = new CodeNode(leftNode.weight+rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;

            codeNodes.remove(leftNode);
            codeNodes.remove(rightNode);

            codeNodes.add(parent);
        }
        return codeNodes.get(0);
    }

    //前序遍历
    public static void preOrder(CodeNode node){
        if(node != null){
            node.preOrder();
        }else {
            System.out.println("你玩我?空树还传过来");
        }
    }

    //哈夫曼编码表

    /**
     *
     * @param node 节点
     * @param code  路径
     * @param s  拼接路径
     */
    private static  void getCode(CodeNode node, String code, StringBuilder s){
        StringBuilder s2 = new StringBuilder(s);
        s2.append(code);
        if(node != null){
            if(node.data == null){ //非叶子结点
                getCode(node.left, "0",s2);  //左递归
                getCode(node.right, "1",s2);  //左递归
            }else { //叶子结点
                hCode.put(node.data,s2.toString());
            }
        }
    }

    private static  Map<Byte,String> getCode(CodeNode node){
        if (node == null){
            return null;
        }
        getCode(node.left,"0",stringBuilder);
        getCode(node.right,"1",stringBuilder);
        return hCode;
    }

    //压缩生成哈夫曼编码
    /**
     *
     * @param bytes 这时原始的字符串对应的 byte[]
     * @param hCode 生成的赫夫曼编码map
     * @return 返回赫夫曼编码处理后的 byte[]
     * 举例: String content = "i like like like java do you like a java"; =》 byte[] contentBytes = content.getBytes();
     * 返回的是 字符串 "1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
     * => 对应的 byte[] huffmanCodeBytes  ,即 8位对应一个 byte,放入到 huffmanCodeBytes
     * huffmanCodeBytes[0] =  10101000(补码) => byte  [推导  10101000=> 10101000 - 1 => 10100111(反码)=> 11011000= -88 ] 第一个1代表符号-后面反码
     * huffmanCodeBytes[1] = -88
     */
    private static byte[] zip(byte[] bytes,Map<Byte,String> hCode){
        StringBuilder stringBuilder = new StringBuilder();
        for (byte b : bytes) {
            stringBuilder.append(hCode.get(b));
        }

        //统计byte[] 的长度
        int len ;
        if(stringBuilder.length() % 8 == 0){
            len =stringBuilder.length() / 8;
        }else {
            len =stringBuilder.length() / 8 + 1;
        }

        byte[] bs = new byte[len];
        int index = 0;
        for (int i =0;i<stringBuilder.length();i+=8){
            String strByte;
            if(i+8 > stringBuilder.length()-1){
                strByte = stringBuilder.substring(i);
            }else {
                strByte = stringBuilder.substring(i,i+8);
            }
            bs[index] = (byte) Integer.parseInt(strByte,2);
            index++;
        }

        return bs;
    }


}


class CodeNode implements Comparable<CodeNode>{
    Byte data; //存放数据(字符)
    int weight; //权值
    CodeNode left;
    CodeNode right;

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

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

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

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

    //前序遍历
    public void preOrder(){
        System.out.print("==>"+this);

        if(this.left != null){
            this.left.preOrder();
        }

        if(this.right != null){
            this.right.preOrder();
        }
    }

}

二:案例——哈夫曼解压

 

package com.atgguigu.huffmanTree;

import java.util.*;

public class HuffmanCode {
    static Map<Byte,String> hCode = new HashMap<>();  //存储字符对应编码
    static StringBuilder stringBuilder = new StringBuilder();   //拼接路径生成编码

    public static void main(String[] args) {
        String s = "i like like like java do you like a java";
        byte[] bytes = s.getBytes();
        System.out.println(Arrays.toString(bytes));
        System.out.println("没有压缩的原长度:"+bytes.length);

        byte[] zip = zip(s);
        System.out.println(Arrays.toString(zip));
        System.out.println("哈夫曼编码压缩后的长度:"+zip.length);

        byte[] decode = decode(hCode, zip);
        System.out.println("解压:"+new String(decode));

    }
 
    /**解压
     * 将一个byte 转成一个二进制的字符串, 如果看不懂,可以参考我讲的Java基础 二进制的原码,反码,补码
     * @param b 传入的 byte
     * @param flag 标志是否需要补高位如果是true ,表示需要补高位,如果是false表示不补, 如果是最后一个字节,无需补高位
     * @return 是该b 对应的二进制的字符串,(注意是按补码返回)
     */
    private static String byteToBitString(boolean flag, byte b) {
        //使用变量保存 b
        int temp = b; //将 b 转成 int
        //如果是正数我们还存在补高位
        if(flag) {
            temp |= 256; //按位与 256  1 0000 0000  | 0000 0001 => 1 0000 0001
        }
        String str = Integer.toBinaryString(temp); //返回的是temp对应的二进制的补码
        if(flag) {
            return str.substring(str.length() - 8);
        } else {
            return str;
        }
    }

    //编写一个方法,完成对压缩数据的解码
    /**
     *解压
     * @param huffmanCodes 赫夫曼编码表 map
     * @param huffmanBytes 赫夫曼编码得到的字节数组
     * @return 就是原来的字符串对应的数组
     */
    private static byte[] decode(Map<Byte,String> huffmanCodes, byte[] huffmanBytes) {

        //1. 先得到 huffmanBytes 对应的 二进制的字符串 , 形式 1010100010111...
        StringBuilder stringBuilder = new StringBuilder();
        //将byte数组转成二进制的字符串
        for(int i = 0; i < huffmanBytes.length; i++) {
            byte b = huffmanBytes[i];
            //判断是不是最后一个字节
            boolean flag = (i == huffmanBytes.length - 1);
            stringBuilder.append(byteToBitString(!flag, b));
        }
        //把字符串安装指定的赫夫曼编码进行解码
        //把赫夫曼编码表进行调换,因为反向查询 a->100 100->a
        Map<String, Byte>  map = new HashMap<String,Byte>();
        for(Map.Entry<Byte, String> entry: huffmanCodes.entrySet()) {
            map.put(entry.getValue(), entry.getKey());
        }

        //创建要给集合,存放byte
        List<Byte> list = new ArrayList<>();
        //i 可以理解成就是索引,扫描 stringBuilder
        for(int  i = 0; i < stringBuilder.length(); ) {
            int count = 1; // 小的计数器
            boolean flag = true;
            Byte b = null;

            while(flag) {
                //1010100010111...
                //递增的取出 key 1
                String key = stringBuilder.substring(i, i+count);//i 不动,让count移动,指定匹配到一个字符
                b = map.get(key);
                if(b == null) {//说明没有匹配到
                    count++;
                }else {
                    //匹配到
                    flag = false;
                }
            }
            list.add(b);
            i += count;//i 直接移动到 count
        }
        //当for循环结束后,我们list中就存放了所有的字符  "i like like like java do you like a java"
        //把list 中的数据放入到byte[] 并返回
        byte b[] = new byte[list.size()];
        for(int i = 0;i < b.length; i++) {
            b[i] = list.get(i);
        }
        return b;
    }

}


class CodeNode implements Comparable<CodeNode>{
    Byte data; //存放数据(字符)
    int weight; //权值
    CodeNode left;
    CodeNode right;

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

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

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

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

    //前序遍历
    public void preOrder(){
        System.out.print("==>"+this);

        if(this.left != null){
            this.left.preOrder();
        }

        if(this.right != null){
            this.right.preOrder();
        }
    }

}
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐