赫夫曼树及哈夫曼编码(二进制压缩)

1.介绍

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

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

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

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

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

2.构建步骤

①从小到大进行排序,将每个数据,每一个数据都是一个结点,每个结点可以看成是一棵最简单的二叉树。

②取出根节点权值最小的两颗二叉树。

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

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

3.代码实现:

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

public class HuffmanTree {
    public static void main(String[] args) {
        int arr[] = { 13, 7, 8, 3, 29, 6, 1 };
        Node root = createHuffmanTree(arr);

        //测试一把
        preOrder(root); //
    }
    //编写一个前序遍历的方法
    public static void preOrder(Node root) {
        if(root != null) {
            root.preOrder();
        }else{
            System.out.println("是空树,不能遍历~~");
        }
    }

    // 创建赫夫曼树的方法
    /**
     *
     * @param arr 需要创建成哈夫曼树的数组
     * @return 创建好后的赫夫曼树的root结点
     */
    public static Node createHuffmanTree(int[]arr){
        // 第一步为了操作方便
        // 1. 遍历 arr 数组
        // 2. 将arr的每个元素构成成一个Node
        // 3. 将Node 放入到ArrayList中
        List lists=new ArrayList();
        for (int item:arr){
            lists.add(new Node(item));
        }
        while (lists.size()>1){
            // 将数据进行排序
            Collections.sort(lists);
            // 取出根节点权值最小的两颗二叉树
            // (1) 取出权值最小的结点(二叉树)
            Node node1=lists.get(0);
            // (2) 取出权值第二小的结点(二叉树)
            Node node2=lists.get(1);
            //(3)构建一颗新的二叉树
            Node parent=new Node(node1.value+node2.value);
            parent.left=node1;
            parent.right=node2;
            lists.remove(node1);
            lists.remove(node2);
            lists.add(parent);

        }
        return lists.get(0);

    }

}
// 创建结点类
// 为了让Node 对象持续排序Collections集合排序
// 让Node 实现Comparable接口
class Node implements Comparable{
    int value;// 结点权值
    char c; // 字符
    Node left; // 指向左节点
    Node  right; // 指向右结点
    // 编写一个前序遍历
    public  void preOrder(){
        // 输出当前结点
        System.out.println(this);
        if (this.left!=null){
            this.left.preOrder();
        }
        if(this.right!=null){
            this.right.preOrder();
        }
    }
    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;
    }
}

4.赫夫曼编码

  4.1基本介绍

①赫夫曼编码翻译为哈夫曼编码,霍夫曼编码,是一种编码方式,属于程序算法。

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

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

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

4.1压缩案例

传输的 字符串 
1) i like like like java do you like a java    

2)  d:1 y:1 u:1 j:2  v:2  o:2  l:4  k:4  e:4 i:5  a:5   :9  // 各个字符对应的个数

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

步骤:
构成赫夫曼树的步骤:
1) 从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
2) 取出根节点权值最小的两颗二叉树 
3) 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和  
4) 再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复  1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树
赫夫曼树及哈夫曼编码(二进制压缩)_第1张图片

4)  根据赫夫曼树,给各个字符,规定编码 (前缀编码), 向左的路径为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 5) 按照上面的赫夫曼编码,我们的"i like like like java do you like a java"   字符串对应的编码为 (注意这里我们使用的无损压缩) 1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110  通过赫夫曼编码处理  长度为  133。

代码实现:

import java.util.*;

public class HuffmanCode {
    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); //40

        byte[] huffmanCodesBytes= huffmanZip(contentBytes);
        System.out.println("压缩后的结果是:" + Arrays.toString(huffmanCodesBytes) + " 长度= " + huffmanCodesBytes.length);

    }
    //使用一个方法,将前面的方法封装起来,便于我们的调用.
    /**
     *
     * @param bytes 原始的字符串对应的字节数组
     * @return 是经过 赫夫曼编码处理后的字节数组(压缩后的数组)
     */
    public static byte[] huffmanZip(byte[] bytes){
        Listlist=getCode(bytes);
        //根据 nodes 创建的赫夫曼树
        Node huffmanTree = createHuffmanTree(list);
        //对应的赫夫曼编码(根据 赫夫曼树)
        Map codes = getCodes(huffmanTree);
        //根据生成的赫夫曼编码,压缩得到压缩后的赫夫曼编码字节数组
        byte[] zip = zip(bytes, codes);
        return zip;
    }
    //编写一个方法,将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte[]
    /**
     *
     * @param bytes 这时原始的字符串对应的 byte[]
     * @param huffmanCodes 生成的赫夫曼编码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 ]
     * huffmanCodeBytes[1] = -88
     */
    public static byte[] zip(byte[]bytes,Map huffmanCodes){
        //1.利用 huffmanCodes 将  bytes 转成  赫夫曼编码对应的字符串
        StringBuilder stringBuilder = new StringBuilder();
        for (byte b:bytes){
            stringBuilder.append(huffmanCodes.get(b));
        }
        System.out.println(stringBuilder);
        int len=0;
        if(stringBuilder.length()%8==0){
            len=stringBuilder.length()/8;
        }else {
           len=stringBuilder.length()/8+1;
        }
        byte[] huffmanCodeBytes  = new byte[len];
        int index=0;//记录是第几个byte
        for(int i=0;istringBuilder.length()){
                strByte=stringBuilder.substring(i);
            }else {
                strByte=stringBuilder.substring(i,i+8);

            }
            //将strByte 转成一个byte,放入到 huffmanCodeBytes
            huffmanCodeBytes[index]=(byte)Integer.parseInt(strByte,2);
            index++;


        }
        return huffmanCodeBytes;

    }
    //生成赫夫曼树对应的赫夫曼编码
    //思路:
    //1. 将赫夫曼编码表存放在 Map 形式
    //   生成的赫夫曼编码表{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 Map huffmanCodes = new HashMap();
    //2. 在生成赫夫曼编码表示,需要去拼接路径, 定义一个StringBuilder 存储某个叶子结点的路径
    static StringBuilder stringBuilder = new StringBuilder();
    //为了调用方便,我们重载 getCodes
    public static Map getCodes(Node node){
        if(node==null){
            return null;
        }
        // 处理root的左子树
        getCodes(node.left,"0",stringBuilder);
        //处理root的右子树
        getCodes(node.right,"1",stringBuilder);
        return huffmanCodes;
    }

    /**
     * 功能:将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合
     * @param node  传入结点
     * @param code  路径: 左子结点是 0, 右子结点 1
     * @param stringBuilder 用于拼接路径
     */
    public static void getCodes(Node node,String code,StringBuilder stringBuilder){
        StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);
        stringBuilder1.append(code);
        if(node!=null){
            if(node.data==null){
                //递归处理
                //向左递归
                getCodes(node.left,"0",stringBuilder1);
                // 向右递归
                getCodes(node.right,"1",stringBuilder1);
            }else {
                huffmanCodes.put(node.data,stringBuilder1.toString());
            }
        }
    }
    //前序遍历的方法
    public static  void preOrder(Node root){
        if(root!=null){
            root.preOrder();
        }else {
            System.out.println("该树为空~~");
        }

    }
    /**
     *
     * @param bytes 接收字节数组
     * @return 返回的就是 List 形式   [Node[date=97 ,weight = 5], Node[]date=32,weight = 9]......],
     */
    public static ListgetCode(byte[] bytes){
        //1创建一个ArrayList
        Listlists=new ArrayList<>();
        //遍历 bytes , 统计 每一个byte出现的次数->map[key,value]
        Map map=new HashMap<>();
        for(byte b:bytes){
            Integer count=map.get(b);
            if(count==null){// Map还没有这个字符数据,第一次
                map.put(b,1);
            }else {
                map.put(b,count+1);
            }
        }
        //把每一个键值对转成一个Node 对象,并加入到nodes集合
        //遍历map
        for(Map.Entry entry:map.entrySet()){
            lists.add(new Node(entry.getKey(),entry.getValue()));

        }
        return  lists;
    }

    //可以通过List 创建对应的赫夫曼树
    public static Node createHuffmanTree(List lists){
        while (lists.size()>1){
            // 将数据进行排序
            Collections.sort(lists);
            // 取出根节点权值最小的两颗二叉树
            // (1) 取出权值最小的结点(二叉树)
            Node node1=lists.get(0);
            // (2) 取出权值第二小的结点(二叉树)
            Node node2=lists.get(1);
            //(3)构建一颗新的二叉树
            Node parent=new Node(null,node1.weight+node2.weight);
            parent.left=node1;
            parent.right=node2;
            lists.remove(node1);
            lists.remove(node2);
            lists.add(parent);

        }
        return lists.get(0);
    }
}


class Node implements Comparable{
    Byte data; // 存放数据(字符)本身,比如'a' => 97 ' ' => 32
    int weight;
    Node left;
    Node right;

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

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

    @Override
    public String toString() {
        return "Node{" +
                "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();
        }
    }
}

总结:上面实现的使用对字符串进行压缩,压缩成一个字节数组下面是结果显示。原来长度为40,压缩后长度为17.赫夫曼树及哈夫曼编码(二进制压缩)_第2张图片 

 

你可能感兴趣的:(算法与数据结构,算法,霍夫曼树)