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

    1 前言

    霍夫曼树是二叉树的一种特殊形式,又称为最优二叉树,其主要作用在于数据压缩和编码长度的优化。

    2 重要概念

    2.1 路径和路径长度

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

     
    图2.1

    图2.1所示二叉树结点A到结点D的路径长度为2,结点A到达结点C的路径长度为1。

    2.2 结点的权及带权路径长度

    若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
    图2.2展示了一棵带权的二叉树


     
    图2.2

    2.3 树的带权路径长度

    树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。
    图2.2所示二叉树的WPL:
    WPL = 6 * 2 + 3 * 2 + 8 * 2 = 34;

    3 霍夫曼树

    3.1 定义

    给定n个权值作为n个叶子结点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为霍夫曼树(Huffman Tree)。
    如图3.1所示两棵二叉树


     
    图3.1

    叶子结点为A、B、C、D,对应权值分别为7、5、2、4。
    3.1.a树的WPL = 7 * 2 + 5 * 2 + 2 * 2 + 4 * 2 = 36
    3.1.b树的WPL = 7 * 1 + 5 * 2 + 2 * 3 + 4 * 3 = 35
    由ABCD构成叶子结点的二叉树形态有许多种,但是WPL最小的树只有3.1.b所示的形态。则3.1.b树为一棵霍夫曼树。

    3.2 构造霍夫曼树

    构造霍夫曼树主要运用于编码,称为霍夫曼编码。现考虑使用3.1中ABCD结点以及对应的权值构成如下长度编码。
    AACBCAADDBBADDAABB。
    编码规则:从根节点出发,向左标记为0,向右标记为1。
    采用上述编码规则,将图3.1编码为图3.2所示:

     
    图3.2

    构造过程:
    3.1.a所示二叉树称为等长编码,由于共有4个结点,故需要2位编码来表示,编码结果为:

    结点编码
    A 00
    B 01
    C 10
    D 11

    则AACBCAADDBBADDAABB对应编码为:
    00 00 10 01 10 00 00 11 11 01 01 00 11 11 00 00 01 01
    长度为36。
    3.1.b构造过程如下:
    1)选择结点权值最小的两个结点构成一棵二叉树如图3.3:


     
    图3.3

    2)则现在可以看作由T1,A,B构造霍夫曼树,继续执行步骤1。
    选则B和T1构成一棵二叉树如图3.4:


     
    图3.4

    3)现只有T2和A两个结点,继续执行步骤1。
    选择A和T2构成一棵二叉树如图3.5:


     
    图3.5

    经过上述步骤则可以构造完一棵霍夫曼树。通过观察可以发现,霍夫曼树中权值越大的结点距离根结点越近。
    按照图3.5霍夫曼树编码结果:

    结点编码
    A 0
    B 10
    C 110
    D 111

    则AACBCAADDBBADDAABB对应编码为:
    0 0 110 10 110 0 0 111 111 10 10 0 111 111 0 0 10 10
    编码长度为35。
    由此可见,采用二叉树可以适当降低编码长度,尤其是在编码长度较长,且权值分布不均匀时,采用霍夫曼编码可以大大缩短编码长度。

    4.代码实现

    1.创建结点

    public class Node implements Comparable<Node>  {
        int value;
        Node left;//左节点
        Node right;//右节点
    
        public Node(int value) {
            this.value = value;
        }
    
        @Override
        public String toString() {
            return "Node{" +
                    "value=" + value +
                    '}';
        }
    
    
        @Override
        public int compareTo(Node o) {
            /*从小到大排序*/
            return this.value-o.value;
        }
    
    
        /*前序遍历*/
    
        public  void  preOrder(){
            System.out.println(this);
            if (this.left!=null){
                  this.left.preOrder();
            }
            if (this.right!=null){
                this.right.preOrder();
            }
        }
    }

    2.创建霍夫曼树

    public class HuffmanTree {
    
        public static void main(String[] args) {
            int[] arr = {13,7,8,3,29,6,1};
            //1.数组进行升序排序
            Node root = createHuffmanTree(arr);
            preOrder(root);
    
        }
    
       /*创建赫夫曼树*/
        public  static  Node createHuffmanTree(int[] arr){
            //数组进行升序排序
            ArrayList<Node> list = new ArrayList<>();
            for (int item : arr) {
                Node node = new Node(item);
                list.add(node);
            }
            while (list.size()>1){
                Collections.sort(list);
                //System.out.println("list ="+list);
                /*取出权值最小的二叉树*/
                Node leftNo = list.get(0);
                /*取出第二小的*/
                Node rightNo = list.get(1);
                /*构建新得二叉树*/
                Node parent = new Node(leftNo.value+rightNo.value);
                parent.left=leftNo;
                parent.right=rightNo;
                //删除list删除掉处理的二叉树
                list.remove(leftNo);
                list.remove(rightNo);
                //将父节点加入到list
                list.add(parent);
    
               // System.out.println("第一次处理" +list);
            }
            return list.get(0); //返回赫夫曼数的root节点
        }
        /*前序遍历*/
        public  static  void  preOrder(Node root){
            if (root!=null){
                root.preOrder();
            }else {
                System.out.println("空赫夫曼数");
            }
        }
    }

    5.利用霍夫曼编码进行运算文件

    1.结点类支持排序 实现比较器的接口

    public class NodeCode implements Comparable<NodeCode>  {
    
        Byte data;//存放数据本身a:97  " ":32
        int weight;//字符出现的个数
        NodeCode left;//左节点
        NodeCode right;//右节点
    
        public NodeCode(Byte data, int weight) {
            this.data = data;
            this.weight = weight;
        }
    
    
        @Override
        public int compareTo(NodeCode o) {
            /*从小到大排序*/
            return this.weight-o.weight;
        }
    
        @Override
        public String toString() {
            return "NodeCode{" +
                    "data=" + data +
                    ", weight=" + weight +
                    '}';
        }
        /*前序遍历*/
        public  void  preOrder(){
            System.out.println(this);
            if (this.left!=null){
                  this.left.preOrder();
            }
            if (this.right!=null){
                this.right.preOrder();
            }
        }
    }

    2.赫夫曼编码对文件进行压缩和解压缩

    public class HuffmanCodeTree {
    
        public static void main(String[] args) {
            //测试压缩文件
            String src = "F:\a.png";
            String dst = "F:\dst.zip";
            //zipFile(src,dst);
            //System.out.println("压缩文件成功");
            //测试解压文件
            String zip = "F:\dst.zip";
            String dstFile = "F:\a1.png";
            unZipFile(zip,dstFile);
            System.out.println("解压缩成功");
    
    
    
    
    
    
    
    
           String str = "i like like like java do you like a java";
          byte[] strBytes = str.getBytes();
          //分部测试
           //System.out.println(strBytes.length);
          /* List<NodeCode> list = getNodes(strBytes);
            System.out.println("list= "+list);
            *//*测试创建的二叉树*//*
            System.out.println("赫夫曼二叉树");
            NodeCode root = createHuffmanTree(list);
            preOrder(root);
    
            *//*测试生成对应的赫夫曼编码*//*
            Map<Byte, String> gethuffmanCode = gethuffmanCode(root);
            System.out.println("生成对应的赫夫曼编码: "+gethuffmanCode);
    
            //测试压缩
            byte[] zip = zip(strBytes, gethuffmanCode);
            System.out.println("zip =" + Arrays.toString(zip));*/
           /* byte[] huffmanZip = huffmanZip(strBytes);
            System.out.println("压缩后的结果 " +Arrays.toString(huffmanZip));
            System.out.println("长度为:" + huffmanZip.length);
            //发送zip数组;
    
            *//*解压缩*//*
            System.out.println("toBinaryString ="+ byteToBitSring(true,(byte)1));;
    
            *//**//*
            //赫夫曼字节数组对应的二进制字符串
           byte[] source = decode(huffmanCodes,huffmanZip);
            System.out.println("原来的字符串:" +  new String(source));*/
    
    
    
        }
    
    
    
         /*使用一个方法方便调用*/
        /**
         *
         * @description:TODO
         * @params:bytes:原始字符串对应的数组
         * @return: 
         * @author: 
         * @time: 2020/3/13 21:04
         */    
        public  static  byte[] huffmanZip(byte[] bytes){
            List<NodeCode> list = getNodes(bytes);
             //创建的赫夫曼树
            NodeCode root = createHuffmanTree(list);
            //生成对应的赫夫曼编码
            Map<Byte, String> gethuffmanCode = gethuffmanCode(root);
            byte[] huffmanZip = zip(bytes, gethuffmanCode);
            return huffmanZip;
        }
    
    
    
    
    
        /*为了方便调用,进行重载*/
        public static  Map<Byte,String> gethuffmanCode(NodeCode root){
             if (root==null){
                 return null;
             }
             //处理root左子树
            gethuffmanCode(root.left,"0",stringBuilder);
             gethuffmanCode(root.right,"1",stringBuilder);
             return huffmanCodes;
        }
    
    
    
    
    
        private  static List<NodeCode> getNodes(byte[] bytes){
             //创建list
            ArrayList<NodeCode> nodeList = new ArrayList<>();
            Map<Byte, Integer> map = new HashMap<>();
            //存储byte出现的次数
            for (byte b : bytes) {
                Integer count =map.get(b);
                if (count == null){
                    map.put(b,1);
                }else {
                    map.put(b,count+1);
                }
            }
            //把每一个键值对转成NodeCode对象,放到list集合中
            for (Map.Entry<Byte,Integer> entry:map.entrySet()){
                nodeList.add(new NodeCode(entry.getKey(),entry.getValue()));
            }
    
            return nodeList;
        }
    
    
       /*创建赫夫曼树*/
        public  static  NodeCode createHuffmanTree(List<NodeCode> nodeList){
            //数组进行升序排序
            while (nodeList.size()>1){
                Collections.sort(nodeList);
                //System.out.println("list ="+list);
                //取出权值最小的二叉树
                NodeCode leftNo = nodeList.get(0);
               // 取出第二小的
                NodeCode rightNo = nodeList.get(1);
               // 构建新得二叉树
                /*根节点没有data只有权值*/
                NodeCode parent = new NodeCode(null,leftNo.weight+rightNo.weight);
                parent.left=leftNo;
                parent.right=rightNo;
                //删除list删除掉处理的二叉树
                nodeList.remove(leftNo);
                nodeList.remove(rightNo);
                //将父节点加入到list
                nodeList.add(parent);
               // System.out.println("第一次处理" +list);
            }
            return nodeList.get(0); //返回赫夫曼数的root节点
        }
        /*前序遍历*/
        public  static  void  preOrder(NodeCode root){
            if (root!=null){
                root.preOrder();
            }else {
                System.out.println("空赫夫曼数");
            }
        }
    
        /*生成赫夫曼编码表
          左节点:0
         右节点:1*/
        //生成对应的赫夫曼编码: {32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
    
       static StringBuffer stringBuilder = new StringBuffer();//存储某个叶子节点的路径
       static Map<Byte,String> huffmanCodes = new HashMap<>();
    
       /**
        *
        * @description:生成赫夫曼编码,存储到Map
        * @params:1.根节点2.路径 左节点:0右节点:1   3.存储某个叶子节点的路径编码
        * @return:
        * @author: 
        * @time: 2020/3/13 19:49
        */
       public static  void gethuffmanCode(NodeCode node, String code, StringBuffer stringBuilder){
           StringBuffer stringBuffer2= new StringBuffer(stringBuilder);
           stringBuffer2.append(code);
           if (node!=null){
               if (node.data==null){//判断节点是否是叶子节点
                   //递归处理
                   //向左
                   gethuffmanCode(node.left,"0",stringBuffer2);
                   //向右
                   gethuffmanCode(node.right,"1",stringBuffer2);
               }else {
                   //找到叶子节点
                   huffmanCodes.put(node.data,stringBuffer2.toString());
               }
           }
       }
    
    
    
       /*将一个字符对应的byte数组利用赫夫曼编码表,返回一个压缩编码的byte数组*/
        /**
         *
         * @description:TODO
         * @params:1.原始数组2.赫夫曼编码
         * @return:byte[]八位数组吧b[0] =10101000{补码}  ===>补码-1(10100111)反码
         * @author: 
         * @time: 2020/3/13 20:16
         */
        public  static  byte[] zip(byte[] bytes,Map<Byte,String> huffmanCodes){
             //利用赫夫曼编码表转成赫夫曼编码字符串
            StringBuffer buffer = new StringBuffer();
            for (byte b : bytes) {
               buffer.append(huffmanCodes.get(b));
            }
           /* System.out.println("测试buffer= "+buffer.toString());
            System.out.println(buffer.length());*/
    
            //统计字符串长度
            /*(buffer.length()+7)  /8 */
            int len;
            if (buffer.length()%8==0){
                len = buffer.length()/8;
            }else {
                len = buffer.length()/8+1;
            }
            //创建压缩后数组
            byte[] huffmanBys = new byte[len];
            int index = 0;//第几个byte
            for (int i = 0; i <buffer.length() ; i+=8) {//步长为8
                String strBy;
                if (i+8>buffer.length()){
                     strBy = buffer.substring(i);
                }else {
                     strBy = buffer.substring(i,i+8);
                }
                //将strBy转换成byte 放到by数组
                huffmanBys[index] = (byte)Integer.parseInt(strBy,2);
                  index++;
    
    
            }
            return huffmanBys;
        }
    
    
           /*进行解压缩*/
    
        /**
         *
         * @description:将byte转成一个二进制字符串
         * @params:flag 真:需要不高位
         * @return:b对应二进制的字符串(按照补码返回)
         * @author: 
         * @time: 2020/3/13 21:20
         */
        public  static  String byteToBitSring(boolean flag , byte b) {
            //使用保存b
            int temp = b;
            /*如果是正数需要不高位*/
            if (flag) {
                temp |= 256;//按位与 1 0000 0000 |0000 0001 =>1 0000 0001
            }
            String str = Integer.toBinaryString(temp);//二进制的补码
            if (flag) {
                return str.substring(str.length() - 8);
            } else {
                return str;
            }
        }
    
    
        /*解码的方法*/
        /**
         *
         * @description:
         * @params:1.赫夫曼编码对照表 2.压缩的数组
         * @return:
         * @author: 
         * @time: 2020/3/13 21:46
         */
            public  static byte[] decode(Map<Byte,String> huffmanCodes , byte[]huffmanZip){
                //得到huffmanZip 对应的二进制的字符串101010000...
                StringBuilder builderString = new StringBuilder();
    
                //byte转换成二进制字符串
    
                /*如果是最后一个数不需要补高位*/
                for (int i = 0; i < huffmanZip.length; i++) {
                    byte by = huffmanZip[i];
                    boolean flag = (i==huffmanZip.length-1);
                     builderString.append(byteToBitSring(!flag, by));
                }
                System.out.println("赫夫曼字节数组对应的二进制字符串 ="+ builderString.toString());
    
                /*把字符串按照对应的赫夫曼编码进行编码*/
                Map<String,Byte> map =new HashMap<>();
                for (Map.Entry<Byte,String> entry : huffmanCodes.entrySet()){
                    map.put(entry.getValue(),entry.getKey());
                }
                System.out.println("map =" +map);
    
    
                //创建集合存放byte
    
                List<Byte> list = new ArrayList<>();
                for (int i= 0; i < builderString.length();){
                    int count = 1;//小的计数器
                    boolean flag = true;
                    Byte b = null;
                    while (flag){
                        //递增取出key取出一个1或者0
                        String key = builderString.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;
            }
    
    
    
    
    
    
            /*编写方法,将一个文件进行压缩*/
        /**
         *
         * @description:进行压缩文件
         * @params:1.传入压缩文件的全部路径2.压缩文件存放的位置
         * @return:
         * @author: 
         * @time: 2020/3/13 22:41
         */
        public  static void  zipFile(String srcFile,String dstFile){
            //创建输出流
            //创建文件的输入流
            FileInputStream is = null;
            FileOutputStream os = null;
            ObjectOutputStream oos = null;
            try {
                 is = new FileInputStream(srcFile);
                //创建一个源文件大小相同的字节数组
                byte[]  b = new byte[is.available()];
                //读取文件
                is.read(b);
                //获取到文件进行压缩 得到字节数组
                byte[] huffmanBys = huffmanZip(b);
                //创建输出流
                os = new FileOutputStream(dstFile);
                //创建一个和文件输出流的objectOutStream
                oos = new ObjectOutputStream(os);
                //采用对象流将赫夫曼编码后的字节数组写入文件,可以有效的解压缩
                oos.writeObject(huffmanBys);
                /*将赫夫曼编码写入文件*/
                oos.writeObject(huffmanCodes);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    is.close();
                    oos.close();
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    
        }
    
    
    
        /*文件的解压操作的方法*/
        /**
         *
         * @description:TODO
         * @params:1.解压文件文字 2.保存的路径
         * @return: 
         * @author: 
         * @time: 2020/3/13 23:20
         */    
        public  static  void  unZipFile(String zipFile , String dsfFile){
    
            //创建文件的输入流
            InputStream is = null;
            //对象输入流
            ObjectInputStream ois = null;
            //文件的输出流
            OutputStream os = null;
    
    
            try {
                is = new FileInputStream(zipFile);
                //对象输入流
                ois =new ObjectInputStream(is);
                //读取数组
               byte[] huffmanBytes = (byte[]) ois.readObject();
               //读取赫夫曼编码表
                Map<Byte,String> huffmanCodes = (Map<Byte, String>) ois.readObject();
                //进行解码
                byte[] bytes = decode(huffmanCodes, huffmanBytes);
                //将bytes写入文件
                os = new FileOutputStream(dsfFile);
                os.write(bytes);
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                try {
                    os.close();
                    ois.close();
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
    
            }
    
        }
     
    }
  • 相关阅读:
    剑指offer--38.左旋转字符串
    剑指offer--37.和为S的两个数字
    剑指offer--35.数组中只出现一次的数字
    剑指offer--34.数字在排序数组中出现的次数
    剑指offer--33.丑数
    剑指offer--36.整数中1出现的次数(从1到n整数中1出现的次数)
    剑指offer--32.把数组排成最小的数
    剑指offer--31.二叉树中和为某一值的路径
    剑指offer--30.二叉搜索树的后序遍历序列
    剑指offer--29.从上往下打印二叉树
  • 原文地址:https://www.cnblogs.com/sxw123/p/12805933.html
Copyright © 2011-2022 走看看