数据结构 - 哈夫曼树及哈夫曼编码:压缩、解压

什么是哈夫曼树?

哈夫曼树(Huffman Tree):给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度(WPL)达到最小,这样的二叉树也称为最优二叉树

一棵带权二叉树:

在这里插入图片描述

这里有几个概念:

  • 路径:从一个结点到另一个结点经过的所有结点为两结点间的路径,如上图结点A到结点8的路径为:A-B-C-D-8

  • 路径长度:树中一个结点到另一个结点的经过的边数,A到结点8的路径长度为4

  • 权值:对于树的结点,可以定义一个有某种意义的数值为权重值
    如结点为商品,我们就可以设置权值为商品价格,权值根据需求定义

  • 结点的带权路径长度(WPL):对于某个结点,树的根结点到该结点的路径长度和该结点权重的乘积,即是该结点的带权路径长度,结点8的带权路径长度为32

哈夫曼树即是所有叶子结点的带权路径长度之和最小的树

上面的带权二叉树的带权路径长度为:
WPL = 413+48+329+21+26+33+3*7=215
很明显这带权路径长度不是最小的,即上面的带权二叉树不是哈夫曼树

如何实现带权路径长度最小?把权值较大的结点离根较近


带权路径长度最小

具体步骤如下:

  1. 将树叶子结点存储,并根据权值排序(以从小到大为例),把每个结点看成一个二叉树,权值即为该二叉树根节点的权值

在这里插入图片描述

  1. 取出权值最小的两棵树组成一棵新树,并将这两棵树的根节点权值相加作为新二叉树的根节点权值,把这棵新树加入数组,然后排序

在这里插入图片描述

  1. 后续就是重复上面的步骤,如下一步就是根节点为4的二叉树与根节点为6的二叉树组成新树

在这里插入图片描述

  1. 最终结果:数组仅有一个结点,即哈夫曼树的根结点

在这里插入图片描述


Java实现哈夫曼树

树结点类:

  • 权值为value属性
  • 继承Comparable<Node>接口,实现结点比较方法this.value - o.value,从小到大排序
  • 前序遍历方法:先打印当前结点,再循环遍历左子树,再循环遍历右子树
//树的结点,实现排序方法
class Node implements Comparable<Node>{
   
   
    //结点权值
    int value;
    //左子结点
    Node left;
    //右子结点
    Node right;


    //前序遍历
    public void preOrder(){
   
   
        System.out.print(this.value+" ");
        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;
    }
}

哈夫曼树:用Java完成了上面的步骤

public class HuffmanTree {
   
   

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

        preOrder(root);
    }

    /**
     * 前序遍历哈夫曼树
     * @param root 根结点
     */
    public static void preOrder(Node root){
   
   
        if (root != null){
   
   
            System.out.println("=== 哈夫曼树前序遍历 ===");
            root.preOrder();
        }
        else {
   
   
            System.out.println("=== 哈夫曼树为空 ===");
        }
    }


    /**
     * 创建哈夫曼树
     * @param arr 对应的数组
     */
    public static Node createHuffmanTree(int[] arr){
   
   
        //1.遍历arr数组
        //2.将arr每个元素构成一个Node
        //3.将Node放入List
        List<Node> nodes = new ArrayList<>();

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

            //将parent加入nodes,构成新的二叉树
            nodes.add(parent);
            Collections.sort(nodes);

        }

        //最终nodes仅一个结点,为哈夫曼树的根结点
        return nodes.get(0);

    }
}

测试结果,哈夫曼树的前序遍历:

在这里插入图片描述


哈夫曼树有什么用?哈夫曼编码

哈夫曼编码是一种编码方式,属于一种算法,是电讯通信的经典应用之一,广泛用于数据文件的压缩,其压缩率通常在20%~90%

关于编码方式

定长编码

我们知道通信是二进制通信,要传输字符串Hello World Hello Hello World
需要以下步骤:

  • 字符串分割为字符数组,所有的字符解析为ASCII码
    根据ASCII码:H-72,e-101,l-108,o-111,W-87,r-114,d-100,空格-32
Hello World Hello Hello World
=>
(Hello)72 101 108 108 111 32
(World)87 111 114 108 100 32 
(Hello)72 101 108 108 111 32
(Hello)72 101 108 108 111 32 
(World)87 111 114 108 100
  • 将ASCII码转为二进制数:也就是十进制转二进制
01001000 01100101 01101100 01101100 01101111 00100000
01010111 01101111 01110010 01101100 01100100 00100000
01001000 01100101 01101100 01101100 01101111 00100000
01001000 01100101 01101100 01101100 01101111 00100000
01010111 01101111 01110010 01101100 01100100 

虽然这样传输是可以达成目标的,但是太浪费了,为了传输29个字符,需要传输29*8=232个二进制数

变长编码

对于字符串Hello World Hello Hello World,各个字符出现次数 H:3,e:3,l:8,o:5,W:2,r:2,d:2,空格:4
根据出现次数编码,出现次数越多编码越小:

可以自定义编码规则为:

l=0,o=1,空格=10,H=11,e=100,r=101,d=110,W=111

字符串对应的编码为:1110000110111…

这样编码是简便了,但是存在一个问题:无法解码,11可以解码为H,也可以解码为ll

前缀编码:符号字符的编码不能是其他字符编码的前缀要求的编码

哈夫曼编码

对于字符串Hello World Hello Hello World,各个字符出现次数 H:3,e:3,l:8,o:5,W:2,r:2,d:2,空格:4

根据这些字符构建一颗哈夫曼树,以字符出现的次数为权值

关于构建哈夫曼树的步骤上面解释了,这里直接编程:

树结点实体类:新增data、weight属性

//哈夫曼树结点
class Node implements Comparable<Node>{
   
   
    //存放数据本身,'a' => 97
    Byte data;
    //权值,表示字符出现的次数
    int weight;
    //左右结点
    Node left;
    Node right;

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

    @Override
    public String toString
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值