17.C#写算法之什么是 “哈夫曼树” ?(最优二叉树)

文章参考自: 程序员小灰:漫画:什么是 “哈夫曼树” ?

在介绍哈夫曼树前,我们先来弄清楚和树有关的四个概念

概念1:什么是路径?

在一棵树中,从一个结点到另一个结点所经过的所有结点,被我们称为两个结点之间的路径。

上面的二叉树当中,从根结点A到叶子结点H的路径,就是A,B,D,H

概念2:什么是路径长度?

在一棵树中,从一个结点到另一个结点所经过的“边”的数量,被我们称为两个结点之间的路径长度。

仍然用刚才的二叉树举例子,从根结点A到叶子结点H,共经过了3条边,因此路径长度是3

概念3:什么是 结点的带权路径长度?

树的每一个结点,都可以拥有自己的“权重”(Weight),权重在不同的算法当中可以起到不同的作用。

结点的带权路径长度,是指树的根结点到该结点的路径长度,和该结点权重的乘积。

假设结点H的权重是3,从根结点到结点H的路径长度也是3,因此结点H的带权路径长度是 3 X 3 = 9 

概念4:什么是 树的带权路径长度?

在一棵树中,所有叶子结点的带权路径长度之和,被称为树的带权路径长度,也被简称为WPL

仍然以这颗二叉树为例,树的路径长度是 3X3 + 6X3 + 1X2 + 4X2 + 8X2 = 53

理解了上面四个概念,下面正式介绍哈夫曼树:

哈夫曼树是由麻省理工学院的哈夫曼博士于1952年发明,这到底是一颗什么样的树呢?

刚才我们学习了树的带权路径长度(WPL),而哈夫曼树(Huffman Tree)是在叶子结点和权重确定的情况下,带权路径长度最小的二叉树,也被称为最优二叉树。

举个例子,给定权重分别为1,3,4,6,8的叶子结点,我们应当构建怎样的二叉树,才能保证其带权路径长度最小?

原则上,我们应该让权重小的叶子结点远离树根,权重大的叶子结点靠近树根。

下图左侧的这棵树就是一颗哈夫曼树,它的WPL是46,小于之前例子当中的53:

需要注意的是,同样叶子结点所构成的哈夫曼树可能不止一颗,下面这几棵树都是哈夫曼树:

那么,有没有一个通用的方法来帮助我们利用给定的结点和权重,构建一颗哈夫曼树呢?

假设有6个叶子结点,权重依次是2,3,7,9,18,25,如何构建一颗哈夫曼树,也就是带权路径长度最小的树呢?

第一步:构建森林

我们把每一个叶子结点,都当做树一颗独立的树(只有根结点的树),这样就形成了一个森林:

在上图当中,右侧是叶子结点的森林,左侧是一个辅助队列,按照权值从小到大存储了所有叶子结点。至于辅助队列的作用,我们后续将会看到。

第二步:选择当前权值最小的两个结点,生成新的父结点

借助辅助队列,我们可以找到权值最小的结点2和3,并根据这两个结点生成一个新的父结点,父节点的权值是这两个结点权值之和:

第三步:从队列中移除上一步选择的两个最小结点,把新的父节点加入队列

也就是从队列中删除2和3,插入5,并且仍然保持队列的升序:

第四步:选择当前权值最小的两个结点,生成新的父结点

这是对第二步的重复操作。当前队列中权值最小的结点是5和7,生成新的父结点权值是5+7=12:

第五步:从队列中移除上一步选择的两个最小结点,把新的父节点加入队列

这是对第三步的重复操作,也就是从队列中删除5和7,插入12,并且仍然保持队列的升序:

第六步:选择当前权值最小的两个结点,生成新的父结点这是对第二步的重复操作。当前队列中权值最小的结点是9和12,生成新的父结点权值是9+12=21:

第七步:从队列中移除上一步选择的两个最小结点,把新的父节点加入队列

这是对第三步的重复操作,也就是从队列中删除9和12,插入21,并且仍然保持队列的升序:

第八步:选择当前权值最小的两个结点,生成新的父结点

这是对第二步的重复操作。当前队列中权值最小的结点是18和21,生成新的父结点权值是18+21=39:

第九步:从队列中移除上一步选择的两个最小结点,把新的父节点加入队列

这是对第三步的重复操作,也就是从队列中删除18和21,插入39,并且仍然保持队列的升序:

 

第十步:选择当前权值最小的两个结点,生成新的父结点

这是对第二步的重复操作。当前队列中权值最小的结点是25和39,生成新的父结点权值是25+39=64:

第十一步:从队列中移除上一步选择的两个最小结点,把新的父节点加入队列

这是对第三步的重复操作,也就是从队列中删除25和39,插入64:

此时,队列中仅有一个结点,说明整个森林已经合并成了一颗树,而这棵树就是我们想要的哈夫曼树:

代码实现:

using System;
using System.Collections.Generic;
using UnityEngine;

namespace Algorithm
{
    public class MyNode : IComparable
    {
        public int weight;      // 权重
        public MyNode lChild;
        public MyNode rChild;
        public MyNode(int weight)
        {
            this.weight = weight;
        }
        public MyNode(int weight, MyNode lChild, MyNode rChild)
        {
            this.weight = weight;
            this.lChild = lChild;
            this.rChild = rChild;
        }

        // 实现比较接口,用于排序
        public int CompareTo(object obj)
        {
            int result = 0;
            MyNode tmp = obj as MyNode;
            if (tmp.weight > this.weight)
            {
                result = 1;
            }
            else if (tmp.weight < this.weight)
            {
                result = -1;
            }
            return result;
        }
    }

    // 哈夫曼树
    public class HuffmanTree
    {
        public MyNode root;
        private MyNode[] nodes;
        /// <summary>
        /// 构建哈夫曼树
        /// </summary>
        /// <param name="weights">权重数组</param>
        public void CreateHuffman(int[] weights)
        {
            // 优先队列,用于辅助构建哈夫曼树
            Queue<MyNode> nodeQueue = new Queue<MyNode>();
            nodes = new MyNode[weights.Length];

            // 构建森林,初始化nodes数组
            for (int i = 0; i < weights.Length; i++)
            {
                nodes[i] = new MyNode(weights[i]);
                nodeQueue.Enqueue(nodes[i]);
            }

            // 主循环,当节点队列只剩下一个节点结束
            while (nodeQueue.Count > 1)
            {
                // 从节点队列选择权值最小的两个节点
                MyNode left = nodeQueue.Dequeue();
                MyNode right = nodeQueue.Dequeue();
                // 创建新节点作为两节点的父节点
                MyNode parent = new MyNode(left.weight + right.weight, left, right);
                nodeQueue.Enqueue(parent);
            }
            root = nodeQueue.Dequeue();
        }

        public void Output(MyNode head)
        {
            if (head == null)
            {
                return;
            }
            Debug.Log(head.weight);
            Output(head.lChild);
            Output(head.rChild);
        }
    }
}

在这段代码中,为了保证结点队列当中的结点始终按照权值升序排列,我们使用了优先队列PriorityQueue

与此同时,静态内部类Node需要实现比较接口,重写compareTo方法,以保证Node对象在进入队列时按照权值来比较。

假如给定的叶子结点个数是n,那么主循环的次数是n-1,循环体内要进行优先队列的出队和入队操作,每一次的时间复杂度是O(logn),两者是乘积关系,所以上面构建哈夫曼树的时间复杂度是O(nlogn)。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kerven_HKW

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值