赫夫曼树

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();
            }

        }

    }
 
}
原文地址:https://www.cnblogs.com/sxw123/p/12805933.html