学习数据结构笔记(10) --- [赫夫曼树(Huffman Tree)与赫夫曼编码(Huffman coding)]

Posted 小智RE0

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学习数据结构笔记(10) --- [赫夫曼树(Huffman Tree)与赫夫曼编码(Huffman coding)]相关的知识,希望对你有一定的参考价值。

B站学习传送门–>尚硅谷Java数据结构与java算法(Java数据结构与算法)



一:赫夫曼树

1.赫夫曼树的相关概念

赫夫曼树是带权路径长度最短的树;并且权值较大的树距离根结点比较近

路径就是:从一个节点出发,到达它的子节点,或者孙子节点,经过的通路;就是路径;
路径长度的话;比较简单的计算方法; 比如我认为根结点所在的位置是第一层;那么假如这棵树有M层;那么从根结点到达第M层的路径长度就是(M-1);
那么,我从第1层的节点到第4层的节点路径长度就是(4-1 = 3);
从第2层的节点到第4层的节点的路径长度就是(4-2 = 2)

比如,我这图中;
第一个节点到下面左节点的右结点,这段路径的长度为2;
第一个节点到下面右结点 ;这段路径的长度为 1

节点的权:
如果要把给一个节点赋予一个特殊的数值,那么就可以说这个数值就是节点的权;

节点的带权路径长度:
从根结点出发,到达这个结点的路径长度这个结点的权的乘积;

例如这个案例,左下角节点的带权路径长度就是 60

树的带权路径长度
就是所有的节点带权路径之和;

例如这棵树的带权路径长度就是130

若将树改为这样的结构;
这棵树的带全路径长度变为115;
这时的结构就是一棵赫夫曼树

若让最大权 30 远离根结点,
这时树的带权路径长度就变为125

那么,
赫夫曼树就是带权路径长度最短的二叉树;并且权值越大的节点距离根结点越近;


2.赫夫曼树的创建过程图解

(1)首先拿到这个数组后;对数组进行由小到大排序;
可以把每个数当成一个节点;每个节点当成一棵简单的二叉树;

比如说,我这里有个数组[20,30,40,10,5,1,3];
我先让它由大到小进行排序 为
[ 1, 3, 5, 10, 20, 30, 40]
将每个数据看作是一棵简单的二叉树

(2)找出其中根节点权值最小的两棵二叉树

在我这个案例中,就查找 31

(3) 然后让这两个节点组成一棵新的二叉树,这个新的二叉树权值之和为这两个节点的权值之和;

当前案例下,就把这个1 和 3 组成一棵二叉树
一般,让比较小的放在左边;

(4) 这些剩余的二叉树根据根结点的权值由小到大进行排序;
重复(1)(2)(3)(4)步骤;直到形成一棵赫夫曼树;

找到权值最小的根结点 5 和 4;

找到权值最小的根结点 10和 9;

找到权值最小的根结点 20 和 19;

找到权值最小的根结点 30 和 39;

找到权值最小的根结点 40 和 69;这样;一棵基础的赫夫曼树成型


3.用代码创建实现赫夫曼树

这里实现的话,就让数组元素的数值作为权值;
然后,具体存储用封装节点的集合实现;不过需要注意的是,这里逻辑上集合最终保留的只有一个节点(赫夫曼树的根结点);但是它有挂接的左右子节点

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

/**
 * @author by CSDN@小智RE0
 * @date 2021-11-16 20:44
 * 赫夫曼树的基本构建过程;
 */
public class HuffManTreeTest 
    //将数组转换得到一棵赫夫曼树;
    public static TreeNode getHuffManTree(int[] array) 
        //这边实现的话,考虑用一个集合存储节点;
        List<TreeNode> list = new ArrayList<>();
        //那么就先把数组的元素封装为节点存入集合;
        for (int j : array) 
            list.add(new TreeNode(j));
        

        //最终的话,集合就剩余一个节点了;
        while (list.size() > 1) 
            //先用工具类,让这个集合元素进行排序;
            Collections.sort(list);

            //找到最小的两个节点;构成一个新的二叉树;
            TreeNode left = list.get(0);
            TreeNode right = list.get(1);
            TreeNode newNode = new TreeNode(left.value + right.value);
            //把左右节点挂接到树上;
            newNode.leftNode = left;
            newNode.rightNode = right;
            //这时就把刚才操作的两个节点删了;
            list.remove(left);
            list.remove(right);
            //把新的节点添加到集合;
            list.add(newNode);
        
        //最终返回根节点;
        return list.get(0);
    

    //让二叉树进行前序遍历;
    public static void prefixList(TreeNode root)
        if(root==null) throw new RuntimeException("Empty tree, do not traverse");
        root.prefixList();
    




//树的节点;为使得节点具有可比性; 需实现Compareable接口;
class TreeNode implements Comparable<TreeNode> 
    //这里直接就让value表示权值了;
    public int value;
    //左子树,右子树;
    public TreeNode leftNode;
    public TreeNode rightNode;

    //初始化;
    public TreeNode(int value) 
        this.value = value;
    

    //输出节点;
    @Override
    public String toString() 
        return "TreeNode" + "value=" + value + '';
    

    @Override
    public int compareTo(TreeNode o) 
        //由小到大排序
        return this.value - o.value;
    

    //前序遍历;
    public void prefixList() 
        //先输出当前结点;
        System.out.println(this);
        //若左子树不为空就递归;
        if (this.leftNode != null)
            this.leftNode.prefixList();
        //若右子树不为空就递归;
        if (this.rightNode != null)
            this.rightNode.prefixList();
    

测试使用;

//测试;
public static void main(String[] args) 
    int[] array= 20,30,40,10,5,1,3;
    TreeNode root = getHuffManTree(array);
    System.out.println("赫夫曼树根结点为->"+root.value);
    System.out.println("前序遍历--------");
    prefixList(root);

测试结果;和刚才图解分析的树一致

赫夫曼树根结点为->109
前序遍历--------
TreeNodevalue=109
TreeNodevalue=40
TreeNodevalue=69
TreeNodevalue=30
TreeNodevalue=39
TreeNodevalue=19
TreeNodevalue=9
TreeNodevalue=4
TreeNodevalue=1
TreeNodevalue=3
TreeNodevalue=5
TreeNodevalue=10
TreeNodevalue=20

二: 赫夫曼编码

1.关于赫夫曼编码

赫夫曼编码是一种编程算法;广泛地用于数据文件的压缩;
作为可变字长编码(VLC)之一

在线转换工具: 转换工具

通信领域的信息处理方式:定长编码;变长编码

还有一种方式就是赫夫曼编码;

首先看课程中的案例,后面我会做个案例;
(1)赫夫曼编码的话,首先会统计字符出现的次数,这样每个字符可以暂时用出现次数表示权值;
(2)将这些字符看做是一个个简单的二叉树;按照创建赫夫曼树的流程,创建出一棵赫夫曼树;
(3)在这个赫夫曼树的基础上;规定向左的节点路径为0,向右的节点路径为1;
这样计算的话,每个字符得到的编码值绝对不是一样的;
那么对应的 ;生成的字符编码就是

d: 100110 y: 100111 u: 10010 j:0000 v:0001    o:1000
l: 001    k: 1110   e:1111   i:101  a:110  空格:01

(5)那么翻译字符i like like like java do you like a java后得到的编码就是

10101001101111011110100110111101111010011011110111101000011000011101001101000011001111000100100100110111101111011100100001100001110

这样的话,会发现每次字符生成的编码都不会是一样的;
之前用ascall码表示这段字符的话得到的是长度为359的编码;
而使用赫夫曼编码后;生成的编码长度才133;那么对数据的压缩效果还是比较可观的;

2.赫夫曼编码创建案例

这里处理的话,首先会统计每个字符出现的次数;

比如说,我这里写个字符串xiaozhi like java 注意,中间有空格;

那么;统计字符出现的次数  x:1 i:3 a:3 o:1 z:1 h:1 l:1 k:1 e:1 j:1 v:1 空格:2

简单排序后;

x:1   o:1   z:1   h:1   l:1   k:1   e:1   j:1   v:1   空格:2   i:3   a:3

然后再将这些次数组合为一个数组;将字符出现的次数作为权值;
按照创建赫夫曼树的过程;将这个字符串变为一棵赫夫曼树;

大概创建出来就是这个情况的;

之前第一次创建出来是这样的;
这样的结构也是符合的;权值路径算出来的话都是59

还没结束;这里最后就按照第一种创建出的结构说说吧;
左子树路径标记为 0 ;右子树路径为1;
就按这个绿色的树来说吧;
那么对原先的字符进行编码读取;

x:0111   o:000   z:1000    h:0110  l:1011  k:1010  
e:0100   j:1000  v:0101  空格:001  i:111   a:110

将字符串xiaozhi like java进行编码翻译;

01111111100001000011011100110111111010010000110001100101110


3.用代码具体实现赫夫曼编码的创建(文本数据的压缩)

存放的每个节点包括数据域和权值;数据域就是字符(可使用ascall码表示),权值就是字符出现的次数;

3.1首先是转成赫夫曼树

import java.util.*;

public class HuffManCodeTest01 
    //创建赫夫曼树;
    private static Node getHuffman(byte[] bytes)
        //调用方法,数组转为集合getNodeToList;
        List<Node> list = getNodeToList(bytes);
        //只要不到根结点,就不停止;
        while (list.size()>1)
            //先对集合进行排序;
            Collections.sort(list);
            //取出左右节点;
            Node left  = list.get(0);
            Node right = list.get(1);
            //生成新节点后,这里不存储数据域,仅存权值;
            Node newNode = new Node(null,left.weight+right.weight);
            //将左右节点挂接到新节点后;
            newNode.leftNode  = left;
            newNode.rightNode = right;
            //在集合中删除左右节点;
            list.remove(left);
            list.remove(right);
            //将新节点添加到集合中;
            list.add(newNode);
        
        return list.get(0);
    


    //将字符数据和出现的次数封装为节点,存入集合中;
    private static List<Node> getNodeToList(byte[] bytes)
        //创建最终需要返回的数组集合;
        List<Node> list = new ArrayList<>();

        //使用map接收字符与次数;
        Map<Byte,Integer> map = new HashMap<>();
        //将字符数组先存入map集合;
        for (byte b : bytes) 
            //第一次就直接存入;后面若还重复则覆盖前面的,出现次数加1;
            map.merge(b, 1, Integer::sum);
        
        //最后再去遍历map集合;放入集合;
        for (Map.Entry<Byte, Integer> entry : map.entrySet()) 
            list.add(new Node(entry.getKey(), entry.getValue()));
        
        return list;
    

    //二叉树的前序遍历;
    public static void prefixNode(Node root)
        if(root!=null)
            root.prefixList();
        else 
            throw new RuntimeException("Empty tree, do not traverse");
        
    


//节点;
//为具有可比性,实现Comparable接口
class Node implements Comparable<Node> 
    //数据域,存放字符;
    public Byte character;
    //存放权值;
    public int weight;
    //左节点,右结点;
    Node leftNode;
    Node rightNode;

    public Node(Byte character, int weight) 
        this.character = character;
        this.weight = weight;
    

    //权重按照由小到大进行排序
    @Override
    public int compareTo(Node o) 
        return this.weight - o.weight;
    

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

    //前序遍历方法;
    public void prefixList() 
        System.out.println(this);
        //向左递归
        if (this.leftNode != null) this.leftNode.prefixList();
        //向右递归
        if (this.rightNode != null) this.rightNode.prefixList();
    

测试使用

public static void main(String[] args) 
    //先将字符装进数组;
    String str = "xiaozhi like java";
    byte[] bytes = str.getBytes();
    System.out.println("字符串的长度:"+bytes.length);
    //调用方法,创建赫夫曼树;
    Node node = getHuffman(bytes);
    //对赫夫曼树进行前序遍历;
    prefixNode(node);

测试结果;前序遍历差不多

字符串的长度:17
Nodecharacter=null, weight=17
Nodecharacter=null, weight=7
Nodecharacter=null, weight=3
Nodecharacter=111, weight=1
Nodecharacter=32, weight=2
Nodecharacter=null, weight=4
Nodecharacter=null, weight=2
Nodecharacter=101, weight=1
Nodecharacter=118, weight=1
Nodecharacter=null, weight=2
Nodecharacter=104, weight=1
Nodecharacter=120, weight=1
Nodecharacter=null, weight=10
Nodecharacter=null, weight=4
Nodecharacter=null, weight=2
Nodecharacter=106, weight=1
Nodecharacter=122, weight=1
Nodecharacter=null, weight=2
Nodecharacter=107, weight=1
Nodecharacter=108, weight=1
Nodecharacter=null, weight=6
Nodecharacter=97, weight=3
Nodecharacter=105, weight=3

3.2由赫夫曼树得到生成赫夫曼编码

这些字符节点实际上都分布在赫夫曼树的叶子节点上,(数据域不为空)
可使用map集合取到节点字符后拼接路径字符串;
最终得到这样的格式;

x(120):0111   o(111):000   z(122):1000 

以上是关于学习数据结构笔记(10) --- [赫夫曼树(Huffman Tree)与赫夫曼编码(Huffman coding)]的主要内容,如果未能解决你的问题,请参考以下文章

数据结构学习笔记——哈夫曼树

数据结构学习笔记——哈夫曼树

学习数据结构笔记(11) --- [二分搜索树(BinarySearchtTree)]

数据结构与算法学习笔记树

《大话数据结构》笔记(6-3)--树:赫夫曼树

数据结构笔记11 哈夫曼树与哈夫曼编码