zoukankan      html  css  js  c++  java
  • 赫夫曼树和赫夫曼编码

    赫夫曼树

    定义

    1. 给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree), 还有的书翻译为霍夫曼树

    2. 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

    赫夫曼树几个重要概念和举例说明

    1)路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1

    2)结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积

    3)树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL(weighted path length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。

    4)WPL最小的就是赫夫曼树

    image-20210826145247926

    赫夫曼树创建思路

    给你一个数列 {13, 7, 8, 3, 29, 6, 1},要求转成一颗赫夫曼树.

    构成赫夫曼树的步骤:

    1)从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树

    2)取出根节点权值最小的两颗二叉树

    3)组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和

    4)再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树

    图解:

    image-20210826151837475 image-20210826151852707 image-20210826151907427 image-20210826151933522

    最后形成的树就是赫夫曼树。

    代码实现

    /**
     * @author wen.jie
     * @date 2021/8/26 15:21
     * 赫夫曼树
     */
    public class HuffmanTree {
    
        private Node root;
    
        public HuffmanTree(int[] arr){
            List<Node> nodes = new ArrayList<>();
            for (int value : arr) {
                nodes.add(new Node(value));
            }
    
            while (nodes.size()>1) {
                //排序所有元素
                Collections.sort(nodes);
                Node leftNode = nodes.get(0);
                Node rightNode = nodes.get(1);
                Node parent = new Node(leftNode.value + rightNode.value);
                parent.left = leftNode;
                parent.right = rightNode;
                nodes.remove(leftNode);
                nodes.remove(rightNode);
                nodes.add(parent);
            }
            root = nodes.get(0);
        }
    
        /**
         * @author wen.jie
         * @date 2021/8/26 15:39
         * 前序遍历
         */
        public void preOrder() {
            if(root == null) return;
            root.preOrder();
        }
    
        private class Node implements Comparable<Node>{
            //节点权值
            int value;
            Node left;
            Node right;
    
            public Node(int value){
                this.value = value;
            }
    
            @Override
            public int compareTo(Node o) {
                //表示从小到大排序
                return this.value - o.value;
            }
    
            void preOrder() {
                System.out.println(toString());
                if(this.left != null){
                    this.left.preOrder();
                }
                if(this.right != null){
                    this.right.preOrder();
                }
            }
    
            @Override
            public String toString() {
                return "Node{" +
                        "value=" + value +
                        '}';
            }
        }
    }
    

    测试:

            int[] arr = {13, 7, 8, 3, 29, 6, 1};
            HuffmanTree tree = new HuffmanTree(arr);
            tree.preOrder();
    

    前序遍历的结果:

    image-20210826154346448

    赫夫曼编码

    简介

    1)赫夫曼编码也翻译为哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法。

    2)赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。

    3)赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间

    4)赫夫曼码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,称之为最佳编码

    定长编码

    •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 (包括空格)

    在线转码 工具 :https://www.mokuge.com/tool/asciito16/

    变长编码

    •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...

    字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码

    赫夫曼编码

    •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 // 各个字符对应的个数

    •按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值.

    image-20210826160247386

    根据赫夫曼树,给各个字符规定编码, 向左的路径为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

    说明:

    1)原来长度是 359 , 压缩了 (359-133) / 359 = 62.9%

    2)此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性

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

    image-20210826162216199

    赫夫曼编码实践

    数据压缩

    创建赫夫曼树

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

    这里不难,代码如下:

    
    /**
     * @author wen.jie
     * @date 2021/8/26 16:33
     */
    public class HuffmanCode {
    
        private Node root = null;
    
        public HuffmanCode(String content){
            byte[] contentBytes = content.getBytes();
            //创建节点
            List<Node> nodes = createNodes(contentBytes);
            //创建赫夫曼树
            root = createHuffmanTree(nodes);
            preOrder();
        }
    
        private void preOrder(){
            if(root == null) return;
            //前序遍历
            root.preOrder();
        }
    
        private Node createHuffmanTree(List<Node> nodes) {
            while (nodes.size() > 1) {
                Collections.sort(nodes);
                Node leftNode = nodes.get(0);
                Node rightNode = nodes.get(1);
                Node parent = new Node(null, leftNode.weight + rightNode.weight);
                parent.left = leftNode;
                parent.right = rightNode;
                nodes.remove(leftNode);
                nodes.remove(rightNode);
                nodes.add(parent);
            }
            return nodes.get(0);
        }
    
        private List<Node> createNodes(byte[] contentBytes){
            Byte[] bytes = new Byte[contentBytes.length];
            for (int i = 0; i < contentBytes.length; i++) bytes[i] = contentBytes[i];
            ArrayList<Node> nodes = new ArrayList<>();
            Map<Byte, Integer> collect = Arrays.stream(bytes)
                    .collect(Collectors.groupingBy(Function.identity(), Collectors.reducing(0, e -> 1, Integer::sum)));
            collect.forEach((k, v) -> nodes.add(new Node(k, v)));
            return nodes;
        }
    
        private class Node implements Comparable<Node>{
            //存放数据本身
            Byte data;
            //权值,表示字符出现的次数
            int weight;
            Node left;
            Node right;
    
            public Node(Byte data, int weight) {
                this.data = data;
                this.weight = weight;
            }
    
            //前序遍历
            void preOrder() {
                System.out.println(toString());
                if(this.left != null){
                    this.left.preOrder();
                }
                if(this.right != null){
                    this.right.preOrder();
                }
            }
    
            @Override
            public int compareTo(Node o) {
                return this.weight - o.weight;
            }
    
            @Override
            public String toString() {
                return "Node{" +
                        "data=" + data +
                        ", weight=" + weight +
                        '}';
            }
        }
    }
    
    

    测试:

            String str = "i like like like java do you like a java";
            HuffmanCode huffmanCode = new HuffmanCode(str);
    

    image-20210826170821037

    生成赫夫曼编码表

    思路:

    1.将赫夫曼编码表放到Map中:32->01 97->100 等等

    代码实现:

        //赫夫曼编码表
        private Map<Byte, String> huffmanCodes = new HashMap<>();
        private StringBuilder sb = new StringBuilder();
        private final String LEFT = "0";
        private final String RIGHT = "1";
    
        public HuffmanCode(String content){
            byte[] contentBytes = content.getBytes();
            //创建节点
            List<Node> nodes = createNodes(contentBytes);
            //创建赫夫曼树
            root = createHuffmanTree(nodes);
            getCodes(root);
            System.out.println(huffmanCodes);
        }
    
        private void getCodes(Node root){
            if (root == null) return;
            //如果只有一个根节点,就直接放进huffmanCodes中
            if (root.left == null && root.right == null)
                huffmanCodes.put(root.data, LEFT);
            getCodes(root.left, LEFT, sb);
            getCodes(root.right, RIGHT, sb);
        }
    
        /**
         * @author wen.jie
         * @date 2021/8/26 17:13
         * @param code 左子节点为0,右子节点为1
         */
        private void getCodes(Node node, String code, StringBuilder stringBuilder){
            StringBuilder builder = new StringBuilder(stringBuilder);
            builder.append(code);
            if(node != null ) {
                if(node.data == null){
                    //非叶子节点
                    getCodes(node.left, LEFT, builder);
                    getCodes(node.right, RIGHT, builder);
                }else {
                    //叶子节点
                    huffmanCodes.put(node.data, builder.toString());
                }
            }
        }
    

    重新执行测试方法:

    image-20210826171936639

    压缩生成赫夫曼编码

    修改代码:

    /**
     * @author wen.jie
     * @date 2021/8/26 16:33
     */
    public class HuffmanCode {
    
        private Node root = null;
    
        //赫夫曼编码表
        private Map<Byte, String> huffmanCodes = new HashMap<>();
        private StringBuilder sb = new StringBuilder();
        private String content;
        private final String LEFT = "0";
        private final String RIGHT = "1";
    
        public HuffmanCode(String content){
            this.content = content;
            //创建节点
            List<Node> nodes = createNodes(content.getBytes());
            //创建赫夫曼树
            root = createHuffmanTree(nodes);
            getCodes(root);
        }
    
        public byte[] huffmanZip(){
            return huffmanZip(content.getBytes(), huffmanCodes);
        }
    
        private static byte[] huffmanZip(byte[] bytes, Map<Byte, String> huffmanCodes) {
            StringBuilder stringBuilder = new StringBuilder();
            for (byte b : bytes)
                stringBuilder.append(huffmanCodes.get(b));
            int len = (stringBuilder.length() + 7) / 8;
            byte[] huffmanCodeBytes = new byte[len];
            //每8位对应一个byte,所以步长为8
            int index = 0;
            for (int i = 0; i < stringBuilder.length(); i+= 8) {
                String strByte;
                if(i+8 > stringBuilder.length()){
                    strByte = stringBuilder.substring(i);
                }else {
                    strByte = stringBuilder.substring(i, i +8);
                }
                //将strByte转成一个byte
                huffmanCodeBytes[index++] = (byte)Integer.parseInt(strByte, 2);
            }
            return huffmanCodeBytes;
        }
    	//Node、createNodes、createHuffmanTree、preOrder、getCodes代码省略
    }
    

    测试:

            String str = "i like like like java do you like a java";
            byte[] zip = HuffmanCode.huffmanZip(str);
            System.out.println(Arrays.toString(zip));
    

    image-20210826174621375

    原本长度为40,现在长度为17,实现了压缩。

    数据的解压

    要求:将[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]数组转成i like like like java do you like a java

    代码实现:

        public Map<Byte, String> getHuffmanCodes() {
            return huffmanCodes;
        }
    
        /**
         * @author wen.jie
         * @date 2021/8/26 20:15
         * @param huffmanCodes 赫夫曼编码表
         * @param huffmanBytes 赫夫曼编码得到的字节数组
         */
        public String decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
            //先得到huffmanBytes对应的二进制的字符串
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < huffmanBytes.length; i++) {
                boolean flag = (i == huffmanBytes.length -1);
                sb.append(byteToBitString(huffmanBytes[i], !flag));
            }
            HashMap<String, Byte> map = new HashMap<>();
            for (Byte bt : huffmanCodes.keySet()){
                map.put(huffmanCodes.get(bt), bt);
            }
            ArrayList<Byte> list = new ArrayList<>();
            //这里利用栈数据结构对字符串进行匹配
            Stack<String> stack = new Stack<>();
            for (int i = 0; i < sb.length(); i++) {
                String str = sb.substring(i, i + 1);
                stack.push(str);
                String key = jointStack(stack);
                if(map.containsKey(key)){
                    list.add(map.get(key));
                    stack.removeAll();
                }
            }
            byte[] b = new byte[list.size()];
            for (int i = 0; i < b.length; i++)
                b[i] = list.get(i);
            return new String(b);
        }
    
        /**
         * @author wen.jie
         * @date 2021/8/26 21:02
         * 拼接栈中所有元素
         */
        private static String jointStack(Stack<String> stack){
            StringBuilder builder = new StringBuilder();
            for (String str : stack) {
                builder.append(str);
            }
            return builder.reverse().toString();
        }
    
        /**
         * @author wen.jie
         * @date 2021/8/26 19:51
         * 将一个byte转成一个二进制字符串
         * @param flag 标志是否需要补高位,最后一位不需要补高位
         */
        private static String byteToBitString(byte b, boolean flag) {
            return flag
                    ? Integer.toBinaryString((b & 0xFF) + 0x100).substring(1)
                    : Integer.toBinaryString(b);
        }
    

    测试:

        @Test
        public void test2(){
            String str = "i like like like java do you like a java";
            // 1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
            HuffmanCode huffmanCode = new HuffmanCode(str);
            byte[] bytes = huffmanCode.huffmanZip();
            System.out.println("加密:" + Arrays.toString(bytes));
            System.out.println("解密:" + huffmanCode.decode(huffmanCode.getHuffmanCodes(), bytes));
        }
    

    image-20210826212732721

    文件压缩

    这里文件压缩与上面的数据压缩思路一致,文件压缩主要多了一个文件读取和写入的操作:

        public void zipFile(String src, String dest) {
            try (FileInputStream fis = new FileInputStream(src);
                 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(dest))) {
                byte[] b = new byte[fis.available()];
                fis.read(b);
                byte[] huffmanBytes = huffmanZip(b);
                oos.writeObject(huffmanBytes);
                oos.writeObject(huffmanCodes);
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    
        public byte[] huffmanZip(byte[] bytes) {
            List<Node> nodes = createNodes(bytes);
            //创建赫夫曼树
            root = createHuffmanTree(nodes);
            getCodes(root);
            return huffmanZip(bytes, huffmanCodes);
        }
    

    测试压缩:

            HuffmanCode huffmanCode = new HuffmanCode();
            huffmanCode.zipFile("D:\红楼梦.txt", "D:\红楼梦.zip");
    

    image-20210826215510084

    文件解压

        public void unzipFile(String zipFile, String destFile) {
            try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(zipFile));
                 FileOutputStream fos = new FileOutputStream(destFile)){
                byte[] huffmanBytes = (byte[])ois.readObject();
                Map<Byte, String> codes = (Map<Byte, String>)ois.readObject();
                byte[] bytes = decode(codes, huffmanBytes).getBytes();
                fos.write(bytes);
            } catch (IOException | ClassNotFoundException e){
                e.printStackTrace();
            }
        }
    

    测试:

        HuffmanCode huffmanCode = new HuffmanCode();
        huffmanCode.unzipFile("D:\红楼梦.zip", "D:\红楼梦2.txt");
    

    image-20210826215615271

    文件打开来,也是可读无误的:

    image-20210826215641001

    赫夫曼编码压缩文件注意事项

    1)如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化, 比如视频,ppt 等等文件 [举例压一个 .ppt]

    2)赫夫曼编码是按字节来处理的,因此可以处理所有的文件(二进制文件、文本文件) [举例压一个.xml文件]

    3)如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显.

    本文所有代码均已上传:https://gitee.com/wj204811/algorithm

  • 相关阅读:
    动态规划>0/1背包问题 小强斋
    【C/C++和指针】auto_ptr智能指针
    WEBSHELL
    sql反模式分析1
    SQLServer之Compute/ComputeBy实现数据汇总
    链表算法大全
    【C++ Primer】自己动手编写函数 atoi(char *str)
    MySQL 5.5 的COMPRESSED INNODB 表
    【设计模式】加薪非要老总批 职责链模式
    【C和指针】const指针
  • 原文地址:https://www.cnblogs.com/wwjj4811/p/15191835.html
Copyright © 2011-2022 走看看