一文了解赫夫曼树的构建与赫夫曼编码

Posted 温文艾尔

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文了解赫夫曼树的构建与赫夫曼编码相关的知识,希望对你有一定的参考价值。


一、赫夫曼树

基本介绍

  1. 给定n个权值作为n个叶子结点,构造一颗二叉树。若该树的带权路径长度(wpl)达到最小,这样的二叉树为最优二叉树,也称为哈夫曼树(HuffmanTree)还有的书翻译为霍夫曼树
  2. 赫夫曼树是带权路径长度最短的树,权值较大的节点离根较近

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

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

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

  3. 例如下图,从根节点到13这个节点的路径长度为3-1=2,13这个节点的带权路径长度为2*13=26

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

  5. WPL最小的就是赫夫曼树


赫夫曼树创建步骤图解

假如有下面一组数据{13,7,8,3,29,6,1},我们来构建赫夫曼树

  1. 从小到大进行排序,每个数据都是一个节点,每个节点可以看成是一颗最简单的二叉树
    1. 排序{1,3,6,7,8,13,29}
  2. 取出根节点权值最小的两个二叉树,组成一颗新的二叉树,该树的二叉树的根节点的权值是前面两棵二叉树根节点权值的和,比如1,3组成二叉树,二叉树节点权值为1+3=4
  3. 再将这颗新的二叉树,以根节点的权值大小再次排序,不断重复1-2-3-4的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树

代码构建赫夫曼树

package org.wql.huffmantree;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**哈夫曼树
 * Description
 * User:
 * Date:
 * Time:
 */
public class HuffmanTree {
    public static void main(String[] args) {
        int arr[] = {13,7,8,3,29,6,1};
        Node root = huffman(arr);
        preOrder(root);
    }

    //创建赫夫曼树的方法
    public static Node huffman(int[] arr){
        //遍历arr数组
        //1.遍历arr数组
        //2.将arr的每个元素构成一个Node
        //3.将Node放入到ArrayList中

        List<Node> nodes = new ArrayList<Node>();
        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);
        }

        //将赫夫曼树的头节点返回
        return nodes.get(0);
    }

    public static void preOrder(Node root){
        if(root==null){
            System.out.println("树空,无法遍历");
        }else {
            root.preOrder();
        }
    }


}

//为了让Node对象支持排序
//让Node 实现Comparable<Node>
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();
        }

    }
}

二、赫夫曼编码

1基本介绍

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

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

如果按照二进制来传递信息,其中长度为359,其中包括空格,可以看到长度非常的长

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

我们通过统计每个字符出现的次数,作为发送依据完成信息传递,长度大大缩减,但是信息读取的精准度却大大下降

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

赫夫曼编码的原理剖析
如果我们要传递这样的字符串:“i like like like java do you like a java”

  1. 首先统计各个字符出现的个数

    1. d:1

      y:1

      u:1

      j:2

      v:2

      o:2

      l:4

      k:4

      3:4

      i:5

      a:5

      :9

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

  3. 根据赫夫曼树,给每个字符规定编码(前缀编码),向左的路径为0,向右的路径为1

    1. o:1000
    2. u:10010
    3. d:100110
    4. y:100111
    5. i:101
    6. a:110
    7. k:1110
    8. e:1111
    9. j:0000
    10. v:0001
    11. l:001
    12. 空格:01
      按照上方的赫夫曼编码,我们得到字符串对应的编码


可以看到在提高精准度的同时,数据长度也大大减少

注意:

赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl是一样的,都是最小的

将字符串通过赫夫曼进行压缩

package org.wql.huffmancode;

import java.util.*;

/**
 * Description
 * User:
 * Date:
 * Time:
 */
public class HuffmanCode {

    static StringBuilder stringBuilder = new StringBuilder();
    static Map<Byte,String> huffmanCodes = new HashMap<>();

    public static void main(String[] args) {
        String content = "i like like like java do you like a java";
        byte[] contentBytes = content.getBytes();
        System.out.println("未压缩之前的长度:"+contentBytes.length);
        byte[] bytes = huffmanZip(contentBytes);
        System.out.println("压缩后的结果是:"+Arrays.toString(bytes));
        System.out.println("压缩率为:"+(double)(contentBytes.length-bytes.length)/contentBytes.length);
    }

    public static byte[] huffmanZip(byte[] contentBytes){
        List<Node> nodes = getNodes(contentBytes);
        System.out.println(nodes);
        //生成赫夫曼树
        Node root = huffman(nodes);
        preOrder(root);
        //利用生成的赫夫曼树,完成赫夫曼码表
        Map<Byte, String> codes = root.getCodes(stringBuilder, huffmanCodes);
        codes.forEach((v,i)->{
            System.out.println(v+":"+i);
        });
        //通过生成的赫夫曼编码表,测试是否生成了对应的赫夫曼编码
        byte[] zip = zip(contentBytes, codes);
        return zip;
    }




    //构建赫夫曼树
    private static Node huffman(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);
    }

    public static List<Node> getNodes(byte[] bytes){

        ArrayList<Node> nodes = new ArrayList<>();
        Map<Byte,Integer> map = new HashMap<>();
        for (byte b : bytes) {
            //count是否为零代表是否已经出现过该字符
            Integer count = map.get(b);
            if(count!=null){
                map.put(b,count+1);
            }else{
                map.put(b,1);
            }
        }
        map.forEach((v,i)->{
            nodes.add(new Node(v,i));
        });
        return nodes;
    }

    //前序遍历
    public static void preOrder(Node root){
        if(root==null){
            System.out.println("树空,无法遍历");
        }else {
            root.preOrder();
        }
    }

    /**
     *
     * @param bytes 原始字符串对应的byte
     * @param huffmanCodes huffmanCodes 生成的赫夫曼编码
     * @return 返回赫夫曼编码处理后的byte[]
     * 例如返回字符串100101010101010101010001111
     *
     *huffmanCodeBytes[0]=10010101(补码)
     *10010101因为是补码,所以我们现将其转为反码再减1
     * 10010101-1 = 10010100
     */
    private static byte[] zip(byte[] bytes,Map<Byte,String> huffmanCodes){

        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(huffmanCodes.get(b));
        }
        System.out.println(sb.length());
        //将字符串转成byte数组
        //统计返回byte[] huffmanCodeBytes的长度
        int len = (sb.length()+7)/8;
        byte[] huffmanCodeBytes = new byte[len];
        int index = 0;
        for (int i=0;i<sb.length();i+=8){
            //每8位对应一个byte,所以步长+8
            String strByte;
            if(i+8>sb.length()){
                strByte = sb.substring(i);
            }else {
                strByte = sb.substring(i,i+8);
            }
            //将strByte转成byte,放进huffmanCodeBytes
            huffmanCodeBytes[index] = (byte)Integer.parseInt(strByte,2);
            index++;
        }
        return huffmanCodeBytes;
    }
}

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

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

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

    //前序遍历
    public void preOrder(){
        System.out.println(this);
        if(this.left!=null){
            this.left.preOrder();
        }
        if(this.right!=null){
            this.right.preOrder();
        }
    }

    public Map<Byte,String> getCodes(StringBuilder stringBuilder,Map<Byte,String> huffmanCodes){
        StringBuilder builder = new StringBuilder(stringBuilder);
        if(this.data!=null){
            huffmanCodes.put(this.data,builder.toString());
            builder=new StringBuilder("");
            return huffmanCodes;
        }
        if(this.left!=null){
            builder.append("0");
            this.left.getCodes(builder,huffmanCodes);
        }
        if(this.right!=null){
            builder.append("1");
            this.right.getCodes(builder,huffmanCodes);
        }
        return huffmanCodes;
    }
}

以上是关于一文了解赫夫曼树的构建与赫夫曼编码的主要内容,如果未能解决你的问题,请参考以下文章

赫夫曼树及赫夫曼编码

数据结构 赫夫曼树

基础数据结构-二叉树-赫夫曼树的解码(详解)

使用优先队列构建赫夫曼树

霍夫曼树和霍夫曼编码以及霍夫曼编码的应用

基于哈夫曼树的任意文件解压缩实现