哈夫曼树与哈夫曼编码(java实现)

哈夫曼树

相关概念

二叉树的带权路径长度:

设二叉树具有n个带权值的叶子结点,从根结点到各个叶子结点的路径长度与相应叶子结点权值的乘积之和。 记为:
在这里插入图片描述
举例:
给定4个叶子结点,其权值分别为{2,3,4,7},可以构造出形状不同的二叉树
在这里插入图片描述
哈夫曼树: 给定一组具有确定权值的叶子结点,带权路径长度最小的二叉树,称为哈夫曼树,亦称最优二叉树。

哈夫曼树的特点:

  • 权值越大的叶子结点越靠近根结点,而权值越小的叶子结点越远离根结点。(构造哈夫曼树的核心思想)
  • 只有度为0(叶子结点)和度为2(分支结点)的结点,不存在度为1的结点。
  • n个叶结点的哈夫曼树的结点总数为2n-1个。
  • 哈夫曼树不唯一,但WPL唯一。
    在这里插入图片描述

哈夫曼树的创建

构成赫夫曼树的步骤:

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

图解示例: {13, 7, 8, 3, 29, 6, 1}

排序
{1, 3, 6, 7, 8, 13, 29 }
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
创建哈夫曼树代码实现

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<Node> nodes = new ArrayList<Node>();
        for (int value : arr) {
    
    
            nodes.add(new Node(value));
        }
        //我们处理的过程是一个循环的过程
        
        while(nodes.size() > 1) {
    
    
            //排序 从小到大
            Collections.sort(nodes);
          //  System.out.println("nodes =" + nodes);
            //取出根节点权值最小的两颗二叉树
            //(1) 取出权值最小的结点(二叉树)
            Node leftNode = nodes.get(0);
            //(2) 取出权值第二小的结点(二叉树)
            Node rightNode = nodes.get(1);
            //(3)构建一颗新的二叉树
            Node parent = new Node(leftNode.value + rightNode.value);
            parent.left = leftNode;
            parent.right = rightNode;
            //(4)从ArrayList删除处理过的二叉树
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //(5)将parent加入到nodes
            nodes.add(parent);
        }
        //返回哈夫曼树的root结点
        return nodes.get(0);

    }
}

// 创建结点类
// 为了让Node 对象持续排序Collections集合排序
// 让Node 实现Comparable接口
class Node implements Comparable<Node> {
    
    
    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) {
    
    
        // TODO Auto-generated method stub
        // 表示从小到大排序
        return this.value - o.value;
    }

}

哈夫曼编码

基本介绍

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

相关术语

编码的前缀性:如果一组编码中,任意一个编码都不是其它任何一个编码的前缀 ,则称这种编码具有前缀性,简称前缀码。

  • 前缀编码保证了在解码(译码)时的唯一性。
  • 等长编码具有前缀性;
  • 变长编码可能使译码产生二义性,即不具有前缀性。如, E(00), T(01), W(0001), 则译码时无法确定二进制串0001是ET还是W。

平均编码长度:

  • 对于给定的字符集(一组对象),可能存在多种编码方案,但应选择最优的。
  • 平均编码长度:设每个(对象)字符cj 的出现的概率 为pj,其二进制位串长度(码长)为lj,则∑lj·pj 表示该组对象(字符)的平均编码长度。
  • 最优前缀码:使得平均编码长度∑lj· pj最小的前缀编 码称为最优的前缀码

原理剖析

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

i like like like java do you like a java // 共40个字符(包括空格)
对应Ascii码:
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
对应的二进制
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 (包括空格)

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

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 , 编码如下:
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
说明:
原来长度是 359 , 压缩了 (359-133) / 359 = 62.9%
此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性

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

用哈夫曼算法求字符集最优前缀编码的算法

  • 使字符集中的每个字符对应一棵只有叶结点的二叉树,叶的权值为对应字符的使用频率----初始化
  • 利用huffman算法来构造一棵huffman树----构造算法
  • 对huffman树上的每个结点,左支附以0,右支附以1(或者相反),则从根到叶的路径上的0、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 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树

在这里插入图片描述

(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

(6) 长度为 : 133
说明:
原来长度是 359 , 压缩了 (359-133) / 359 = 62.9%
此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性
赫夫曼编码是无损处理方案

代码实现

import java.util.*;

/**
 * @anthor longzx
 * @create 2021 02 04 11:25
 * @Description
 **/
public class HuffmanCode {
    
    

    //生成赫夫曼树对应的赫夫曼编码
    //思路:
    //1. 将赫夫曼编码表存放在 Map<Byte,String> 形式
    //   生成的赫夫曼编码表{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<Byte, String> huffmanCodes = new HashMap<Byte,String>();
    //2. 在生成赫夫曼编码表示,需要去拼接路径, 定义一个StringBuilder 存储某个叶子结点的路径
    static StringBuilder stringBuilder = new StringBuilder();


    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
        List<Node> nodes = getNodes(contentBytes);
        System.out.println("nodes:"+nodes);

        //创建哈夫曼树
        System.out.println("哈夫曼树");
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        huffmanTreeRoot.preOrder();
        //生成哈夫曼编码
        getCodes(huffmanTreeRoot,"",stringBuilder);
        System.out.println("生成的哈夫曼编码:" + huffmanCodes);
    }

    /**
     * 功能:将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合
     * @param node  传入结点
     * @param code  路径: 左子结点是 0, 右子结点 1
     * @param stringBuilder 用于拼接路径
     */
    private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
    
    
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //将code 加入到 stringBuilder2
        stringBuilder2.append(code);
        if(node != null) {
    
     //如果node == null不处理
            //判断当前node 是叶子结点还是非叶子结点
            if(node.data == null) {
    
     //非叶子结点
                //递归处理
                //向左递归
                getCodes(node.left, "0", stringBuilder2);
                //向右递归
                getCodes(node.right, "1", stringBuilder2);
            } else {
    
     //说明是一个叶子结点
                //就表示找到某个叶子结点的最后
                huffmanCodes.put(node.data, stringBuilder2.toString());
            }
        }
    }


    /**
     *
     * @param bytes 接收字节数组
     * @return 返回的就是 List 形式   [Node[date=97 ,weight = 5], Node[]date=32,weight = 9]......],
     */
    private static List<Node> getNodes(byte[] bytes) {
    
    

        //1创建一个ArrayList
        ArrayList<Node> nodes = new ArrayList<Node>();
        //遍历 bytes , 统计 每一个byte出现的次数->map[key,value]
        Map<Byte, Integer> counts = new HashMap<>();
        for (byte b : bytes) {
    
    
            Integer count = counts.get(b);
            if (count == null) {
    
     // Map还没有这个字符数据,第一次
                counts.put(b, 1);
            } else {
    
    
                counts.put(b, count + 1);
            }
        }
        //把每一个键值对转成一个Node 对象,并加入到nodes集合
        //遍历map
        for(Map.Entry<Byte, Integer> entry: counts.entrySet()) {
    
    
            nodes.add(new Node(entry.getKey(), entry.getValue()));
        }
        return nodes;

    }


    //可以通过List 创建对应的赫夫曼树
    private static Node createHuffmanTree(List<Node> nodes) {
    
    

        while(nodes.size() > 1) {
    
    
            //排序, 从小到大
            Collections.sort(nodes);
            //取出第一颗最小的二叉树
            Node leftNode = nodes.get(0);
            //取出第二颗最小的二叉树
            Node rightNode = nodes.get(1);
            //创建一颗新的二叉树,它的根节点 没有data, 只有权值
            Node parent = new Node(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;
            //将已经处理的两颗二叉树从nodes删除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //将新的二叉树,加入到nodes
            nodes.add(parent);

        }
        //nodes 最后的结点,就是赫夫曼树的根结点
        return nodes.get(0);

    }

}

//创建Node ,待数据和权值
class Node implements Comparable<Node>  {
    
    
    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;
    }
    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();
        }
    }
}

猜你喜欢

转载自blog.csdn.net/qq_41784749/article/details/113622210