数据结构和算法之九:赫夫曼树

数据结构树论之赫夫曼树

本文,我们来讨论一个用于压缩、加密等场景的赫夫曼编码,而赫夫曼编码通过赫夫曼树生成出来。

假如有一个字符串“abcdeaaaaa”,请问如何用最小的存储空间来保存?

在不压缩的情况下, java中char类型用两个字节表示,那么上面这个串的话得用20个字节才能装下。

我们知道,计算机底层都是二进制的,对于上面这个字符串,我们发现不同的字符为5个,因此可以使用3个位来编码,3个位的编码空间为8,从而节省存储空间:

比如a:000, b:001,c:010, d:011, e:100

那么“abcdeaaaaa” 编码为 000 001 010 011 100 000 000 000 000 000 ,一共30个位就可以了,比起原来的20个字节=160位的话,压缩了5倍有余。在解压缩时,通过编码表,也非常容易完成解压缩过程,即每次读取3个位,查找编码表,反向得出字符。

仔细观察上面的这种编码,我们发现,字符a出现了很多次,如果将a的编码搞短一点,是不是最终编码会更加短了呢,我们来试上一试:

比如a就一位来编码,a:0,那么为了区别a和其他字符,因此其他字符不能用0开头进行编码,用1开始对其他几个字符编码为 b:100, c:101,d:110, e:111。

通过新的编码对“abcdeaaaaa” 编码为 0 100 101 110 111 0 0 0 0 0 一共18个位就可以解决问题了。在解码时,读取一位后去码表中找,找到了得出字符;找不到则往后拼一位再找。比如示例中,首先读取0,码表中找到0的解码字符为a;继续读取1,码表中没有1对应的编码,继续读一位出来,变为10,再去码表找,还是找不到,再读取一位出来,变为100,此时去码表中找字符为b;依次这样找下去,即可完成解码。

前面编码的时候讲到,a的编码为0后,其他的编码都不能以0开头,这是因为在解码的流程中,不能引起歧义。也就是说,每一个编码都不能是其他所有编码的前缀,否则解码就会出错,这种编码就叫赫夫曼编码,也叫前缀编码。

现在,我们知道了赫夫曼编码是怎么回事了,但是怎么把它编出来呢?不可能像上面示例一样,靠手工吧。这就回到今天的主题,赫夫曼树了。

在这里插入图片描述

从根到叶子节点的路径长度*叶子节点的权重,称之为带权路径长度。

那么对于图示中的三种树的组织方式,所有叶子节点的的带权路径长度之和为:

WPL(a):7*2+5*2+2*2+4*2=36()

WPL(b):7*3+5*3+2*1+4*2=46()

WPL©:7*1+5*2+2*3+4*3=35()

带权路径长度最小的树,就叫最优二叉树, 也就是赫夫曼树了。

因为是二叉树,如果将左孩子的路径编码为0,将右孩子的路径编码为1,那么从根到叶子节点的路径形成的编码就是赫夫曼编码了。

比如图示中c树的编码为:a:0, b:10, c:110, d:111。

到这儿,你可能有个疑问,为啥赫夫曼树(也就是最优二叉树)出来的编码,就满足前缀编码,没有一个编码是另外一个的前缀?这个想要通过数学来证明,我也不会,不过如果我们明白了,赫夫曼树是怎么构建的话,也许你会有自己的想法。

现在来看一看如何构建赫夫曼树,通过赫夫曼树的定义,其实我们明显可以得出,权重越大应该离根节点约近,权重越小应该离根节点越远。构建的思路还是比较巧妙的,倒着从叶子到根进行构建:

  1. 从节点列表中,选择权重最小的两个节点,组成一个子树,子树的根的权重为这两个节点的权重之和
  2. 从节点列表中,移除第一步选择的两个节点,同时加入新的子树的根节点
  3. 重复1、2,直到节点列表中只有一个节点为止(剩下的最后的节点就是整棵树的根节点)

我们可以通过画个图来理解下这个过程

在这里插入图片描述

首先有ABCDEFG这些节点

  1. 选择权重最小的两个节点,那么就是G和A,构建成一个子树,子树根权重为3(用x:3来表示)
  2. 新的节点列表去除G和A,并加入x:3节点后,剩下BCDEF以及x:3。
  3. 选择权重最小的两个节点,那么就是x:3和B,构建成一个子树,子树根权重为7(用x:7来表示)
  4. 节点列表去除x:3和B,并加入x:7节点后,剩下CDEF以及x:7
  5. 重复这样执行下去,直到列表中只有一个节点为止。

以上就是赫夫曼树的构建过程,理解了这个过程,用代码实现起来就不算复杂了:

/**
 * 赫夫曼树
 *
 * 通过编码映射的方式,可用于对文本数据压缩、加密等场景应用,应用比较广泛。
 *
 * 赫夫曼树又叫最优二叉树,它也是这一种二叉树,它有一个特性就是
 * 带权路径总和最小:
 * 1. 叶子节点有权重
 * 2. 根到叶子节点的路径*叶子节点的权重叫带权路径
 * 3. 所有根到叶子节点的带权路径最小。
 *
 * 当我们对一个字符串进行压缩,首先想到的就是编码映射,比如字符串"abcdea",
 * 原本占用的空间是12个字节(假如一个字符占用一个字节),也就是12*8=96个byte,
 * 但如果我们通过3个byte的二级制对字符进行编码,
 * a:000
 * b:001
 * c:010
 * d:011
 * e:100
 * 那么对"abcdea"就可以通过000001010011100000来表示,缩短到18个byte,大大减少存储空间。
 *
 * 更进一步,假如字符串""abcdeaaaaa",我们发现a字符出现了若干次,如果使用上面的定长编码,还是存在压缩空间的。
 * 这儿我们可以使用变长编码来实现,出现次数越多,用越短的编码,比如
 * a:0
 * b:101
 * c:110
 * d:101
 * e:111
 * 通过这种方式,bcde长度不变,但字符a的压缩空间进一步变低了。
 * 需要注意的是,这种变长的编码,不能出现某个编码是另外一个编码的前缀,否则在解码的时候会产生歧义。因此也叫前缀编码。
 *
 * 这种编码也叫赫夫曼编码,那么如何来自动生成这样的编码表呢,可通过赫夫曼树来生成。
 *
 */
public class HuffmanTree {
   

    HuffmanNode root;           //根节点
    List<HuffmanNode> leafs;   //所有叶子节点

    public HuffmanTree() {
   
        leafs = new ArrayList<>();
    }

    /**
     * 通过字符权重表构建赫夫曼树
     *
     * 赫夫曼树的构建过程
     * 1. 所有的节点通过权重排序
     * 2. 取最小权重的两个节点为左右孩子,生成一个根,组成一个子树,子树的根的权重为两个孩子的权重之和
     * 3. 新根加入权重排序
     * 4. 重复2-3的步骤,直到剩下一个节点为止,这个节点就是最后的根节点
     *
     * @param charToWeightMap
     * @return
     */
    public static HuffmanTree buildTree(Map<Character, Integer> charToWeightMap){
   
        HuffmanTree tree = new HuffmanTree();

        //所有的叶子节点生成
        charToWeightMap.entrySet().forEach((entry) -
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值