哈夫曼树及其应用

news2025/7/23 5:58:04

一、基本概念

给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。 ——摘自百度百科

要理解哈夫曼树,首先需要理解什么是“带权路径长度”

  • 路径:一棵树中一个节点到达其子孙节点之间的通路称为路径。通路中的分支数目称为路径长度。从根节点到第L层节点的路径长度为L-1。例如上面这棵树中,从17->6的路径长度为3,从14->6的路径长度为2。

  • 节点的带权路径长度:从根节点到该节点之间的路径长度与该节点的权的乘积。例如上图中从17->6的带权路径长度为 3 × 6 = 18 3×6=18 3×6=18

  • 树的带权路径长度:所有叶子节点的带权路径之和。上图的树的带权路径长度为6+3+5+3=17

二、构造哈夫曼树

构造一棵哈夫曼树需要如下步骤:
(1)将给定的n个节点看做n棵独立的树(每棵树只有一个节点)
(2)选出权值最小的两棵树作为左右子树合并为一棵树,其根节点的权值为左右子树权值的和
(3)将这两棵树从森林中删除,将新生成的数加入森林
(4)重复(2)(3)步骤直到只剩一棵树

下面通过一个具体的示例演示构造的过程。首先将给定的节点按权值排序:

选出其中权值最小的两个节点构成一棵新的树

将这两个节点从集合中删除,将新生成的树的根节点加入集合

再将G、C节点构成一棵新的树

将G、C节点删除,并将新树的根节点H加入集合

将C、H构造成一棵新的树。因为C节点权值比H小,所以C作为左孩子

删除C、H,将I加入到集合中

。。。。。。

重复上述过程,直到只剩一棵树,完成哈夫曼树的构造。

三、代码实现

首先定义出节点的结构

public class TreeNode:IComparable<TreeNode>
{
    public int Weight;
    public TreeNode? Left;
    public TreeNode? Right;

    public TreeNode(int weight)
    {
        Weight = weight;
    }
    
    public int CompareTo(TreeNode? other)
    {
        if (other == null) return 1;
        return Weight - other.Weight;
    }
}

构造哈夫曼树:

public TreeNode ConstructHuffmanTree(int[] weights)
{
	if (weights == null || weights.Length == 0) return null;
	HeapList<TreeNode> nodes = new();
	foreach (var weight in weights)
	{
		TreeNode node = new TreeNode(weight);
		nodes.Push(node);
	}

	while (nodes.Count > 1)
	{
		var left = nodes.Pop();
		var right = nodes.Pop();

		var head = new TreeNode(left.Weight + right.Weight);
		head.Left = left;
		head.Right = right;
		
		nodes.Push(head);
	}
	return nodes[0];
}

这里的HeapList是自己简单实现的一个小根堆,代码如下

public class HeapList<T> where T : IComparable<T>
{
    private T[] _items;
    // 默认数组大小
    private const int DefaultCapacity = 4;
    // 元素数量
    private int _size;
    public int Count => _size;
    // 当前数组大小
    public int Capacity
    {
        get => _items.Length;
        set
        {
            if (value < _size)
            {
                throw new ArgumentOutOfRangeException();
            }

            if (value != _items.Length)
            {
                if (value > 0)
                {
                    T[] newItems = new T[value];
                    if (_size > 0)
                    {
                        Array.Copy(_items, newItems, _size);
                    }
                    _items = newItems;
                }
                else
                {
                    _items = new T[DefaultCapacity];
                }
            }
        }
    }
    
    public HeapList()
    {
        _items = new T[DefaultCapacity];
    }

    public HeapList(int length)
    {
        _items = new T[length];
    }
    
    public T this[int index]
    {
        get
        {
            if (index < 0 || index >= _size) throw new ArgumentOutOfRangeException();
            return _items[index];
        }
    }

    /// <summary>
    /// 压入元素
    /// </summary>
    /// <param name="e"></param>
    public void Push(T e)
    {
        // 扩容
        if (_size >= _items.Length)
        {
            int newCapacity = _items.Length == 0 ? DefaultCapacity : _items.Length * 2;
            if ((uint)newCapacity > Array.MaxLength) newCapacity = Array.MaxLength;
            Capacity = newCapacity;
        }

        _items[_size] = e;
        HeapInsert(_size++);
    }

    /// <summary>
    /// 弹出元素
    /// </summary>
    public T Pop()
    {
        if (_size <= 0) throw new ArgumentOutOfRangeException();
        var node = _items[0];
        // 交换首尾元素
        (_items[_size - 1], _items[0]) = (_items[0], _items[_size - 1]);
        _size--;
        // 下沉操作
        Heapify(0);
        return node;
    }
    /// <summary>
    /// 上浮操作
    /// </summary>
    /// <param name="index"></param>
    private void HeapInsert(int index)
    {
        // 当前节点比父节点小,交换两者位置
        int parentIndex = (index - 1) / 2;
        while(_items[index].CompareTo(_items[parentIndex]) < 0 )
        {
            (_items[index], _items[parentIndex]) = (_items[parentIndex], _items[index]);
            index = parentIndex;
            parentIndex = (index - 1) / 2;
        }
    }

    /// <summary>
    /// 下沉操作
    /// </summary>
    /// <param name="index"></param>
    private void Heapify(int index)
    {
        // 左孩子节点下标
        int left = index * 2 + 1;
        while (left < _size)
        {
            int min = left;
            // 左右孩子比较
            if (left + 1 < _size && _items[left + 1].CompareTo(_items[left]) < 0)
            {
                min = left + 1;
            }
            // 与父节点比较
            if (_items[index].CompareTo(_items[min]) < 0)
            {
                break;
            }
            // 父节点与子节点交换
            (_items[index], _items[min]) = (_items[min], _items[index]);
            // 继续向下寻找
            index = min;
            left = index * 2 + 1;
        }
    }
}

四、哈夫曼树的应用

研究哈夫曼树最初的目的是为了解决当时的远距离通信数据传输的最优化问题。比如我们有“A”、“B”、“C”、“D”、“E”、“F”六个字母组成的信息。假如将它们用如下二进制表示(图片源自《大话数据结构》)

那么编码后的数据长度将会是字母数×3

但事实上,组成信息的字母的出现频率可能是不同的。假设这几个字母出现的频率为“A 27,B 8,C 15,D 15,E 30,F 5”,那么我们就可以按照哈夫曼树来规划它们。

首先根据这些字母的权值构建哈夫曼树。再将权值左分支改为0,右分支改为1。(图片源自《大话数据结构》)

然后按照从根节点到叶子结点所经过的路径,重新对这几个字母进行编码,结果如下(图片源自《大话数据结构》)

我们分别使用上面两种方式对字符串“BADCADFEED”进行编码,可得到如下结果:

原编码方式:001000011010000011101100100011
哈夫曼编码方式:1001010010101001000111100

可以很明显地看出编码长度的减少。当然,在接收方收到数据时,也需要通过相同的哈夫曼树进行解码。

最后总结一下:哈夫曼树实际上是根据节点的权值决定节点在树中的位置。权值较大的节点离根节点越近,权值小的节点离根节点越远。以此来保证从根节点访问时可以更快的到达权值较大的节点,从而达到数据压缩的目的。

五、参考资料

[1].《大话数据结构》
[2].百度百科

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/7629.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Web前端:如何提高React原生应用性能

React Native拥有大量追随者&#xff0c;从财富500强公司到新的创业公司。开发人员可以使用React Native为IOS和Android上的移动应用程序创建出色的移动UI。 随着React Native的一切进展顺利&#xff0c;它甚至有负面影响吗?是的&#xff0c;确实如此。这是React Native应用程…

整形提升和算术转换

表达式求值 表达式求值的一部分由符号的优先级和结合性决定。 同时&#xff0c;表达式求值一部分也与数据类型的转换有关。 文章目录1.隐式类型转换2.算术转换1.隐式类型转换 C的整数类型运算总是至少以缺省整形类型的精度来进行的。&#xff08;缺省就是如果程序员没定义函数…

【学习日志】2022.11.11 合同矩阵、惯性指数、委托构造、继承控制、=delete、可变参数模板类

class Info { public:Info() : Info(1) { } // 委托构造函数Info(int i) : Info(i, a) { } // 既是目标构造函数&#xff0c;也是委托构造函数Info(char e): Info(1, e) { }private:Info(int i, char e): type(i), name(e) { /* 其它初始化 */ } // 目标构造函数int type;c…

UD4KB100-ASEMI智能家居专用整流桥UD4KB100

编辑-Z UD4KB100在D3K封装里采用的4个芯片&#xff0c;其尺寸都是72MIL&#xff0c;是一款智能家居专用整流桥。UD4KB100的浪涌电流Ifsm为125A&#xff0c;漏电流(Ir)为10uA&#xff0c;其工作时耐温度范围为-55~150摄氏度。UD4KB100采用光阻GPP芯片材质&#xff0c;里面有4颗…

【POJ No. 3134】幂运算 Power Calculus

【POJ No. 3134】幂运算 Power Calculus POJ 题目地址 【题意】 从x 开始&#xff0c;反复乘以x &#xff0c;可以用30次乘法计算x^31 平方运算可以明显地缩短乘法序列&#xff0c;以下是用8次乘法计算x^31 的方 法&#xff1a; 这不是计算x^31 的最短乘法序列。有很多方法只…

20221115使用google文档翻译SRT格式的字幕

20221115使用google文档翻译SRT格式的字幕 2022/11/15 18:52 &#xff08;一&#xff09;将SRT格式的字幕用WPS转换为DOCX的文档。 &#xff08;请上传 .docx、.pdf、.pptx 或 .xlsx 文件&#xff09; https://www.google.com.hk/?gws_rdssl Google 拍照搜索 Google 提供&a…

F - Double Chance(期望,数学,树状数组优化)[AtCoder Beginner Contest 276]

题目如下&#xff1a; F - Double Chance 题目链接 思路 or 题解&#xff1a; 期望公式&#xff1a;∑valp\sum val \times p∑valp 还可以细分&#xff1a; 如果两次抽出的值是相同的&#xff0c;都是 xxx&#xff0c;那么抽出的方案数为 cntxcntxcnt_x \times cnt_xcntx​c…

Allegro 274X格式gerber输出全流程详细介绍

Allegro 274X格式gerber输出全流程详细介绍 下面介绍Allegro gerber输出的全流程介绍 首先把光绘设置好 设置光钻孔精度 会出现对话框,勾选Enhanced Excellon format,点击close 输出钻孔文件,选择Auto Tool select,点击Drill 输出椭圆孔文件,默认设置,然后点击rout…

一套SCDM脚本建模与二次开发攻略

导读&#xff1a;ANSYS SpaceClaim Direct Modeler&#xff08;简称 SCDM&#xff09;&#xff0c;是基于直接建模思想的新一代3D建模和几何处理软件&#xff0c;摒弃了基于历史的概念建模的约束的概念&#xff0c;让我们轻松完成几何的创建与修改&#xff0c;不会带来传统CAD系…

【附源码】计算机毕业设计JAVA晨光文具店进销存系统设计与开发

【附源码】计算机毕业设计JAVA晨光文具店进销存系统设计与开发 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xf…

m基于STBC的MIMO通信系统性能仿真和信道容量仿真

目录 1.算法概述 2.仿真效果预览 3.核心MATLAB预览 4.完整MATLAB程序 1.算法概述 空时分组编码STBC&#xff08;Space Time Block Coding&#xff09;用在无线通信中传输一个数据流的多个拷贝。通过许多天线来产生数据的多种接收版本&#xff0c;提高数据传输的可靠性。接收…

4、Redis配置文件介绍

文章目录4、Redis配置文件介绍4.1、###Units单位###4.2、###INCLUDES包含###4.3、###网络相关配置4.3.1、bind4.3.2、protected-mode4.3.3、Port4.3.4、tcp-backlog4.3.5、timeout4.3.6、tcp-keepalive4.4、###GENERAL通用###4.4.1、daemonize4.4.2、pidfile4.4.3、loglevel4.4…

一次SpringBoot版本升级,引发的血案

前言 最近项目组升级了SpringBoot版本&#xff0c;由之前的2.0.4升级到最新版本2.7.5&#xff0c;却引出了一个大Bug。 到底是怎么回事呢&#xff1f; 1.案发现场 有一天&#xff0c;项目组的同事反馈给我说&#xff0c;我之前有个接口在新的测试环境报错了&#xff0c;具体…

从应用层深入Framework层,Android Framework 该如何学习?

对于咱们Android开发来说&#xff0c;一般来说都是干上个几年之后&#xff0c;都得要考虑进阶或者是转行的问题。但老话说转行穷三年&#xff0c;不到万不得已我想大多数人都 不会去放弃现在的岗位与薪资。 如果你还在干Android并且想要进阶&#xff0c;那么对Framework的了解…

ava面试八股文-基础概念二

Java面试八股文-基础概念二1.重载与重写的区别2.接口与抽象类区别3.Java集合类-Collection6.lambda表达式与匿名内部类的区别1.重载与重写的区别 重载是编译时多态&#xff0c;重写是运⾏时多态。 方法重写&#xff1a; &#xff08;1&#xff09;参数列表与被重写方法的参数列…

低代码维格云明细视图入门教程

功能简介 低代码维格云可以将基础的数据通过设置操作权限、查询条件、限制数据范围、设置字段显示来创建数据表的明细视图。 设置步骤 功能入口 具体见报表简介 操作权限 数据表是拥有操作权限类别最多的自定义图表,操作权限包括: 可导出可见流程日志可留言可打印可添加数…

C. Balanced Bitstring(思维+子字符串规律)

Problem - 1405C - Codeforces 一个比特串是一个只由0和1字符组成的字符串&#xff0c;如果这个比特串的每个大小为k的子串都有相同数量的0和1字符&#xff08;各为k2&#xff09;&#xff0c;那么这个比特串就被称为k平衡的。 给你一个整数k和一个只由0&#xff0c;1&#xf…

来自BAT的一份Java高级开发岗面试指南:金三银四必定面试无忧

作为一名即将求职的程序员&#xff0c;你的就业机会和风口会出现在哪里&#xff1f;在这种新环境下&#xff0c;工作应该选择大厂还是小公司&#xff1f;已有几年工作经验的老兵&#xff0c;又应该如何保持和提升自身竞争力&#xff0c;转被动为主动&#xff1f; 就目前大环境…

VMware16虚拟机添加硬盘(磁盘)和挂载硬盘(磁盘)

记录&#xff1a;317 场景&#xff1a;在VMware16虚拟机&#xff0c;安装了CentOS 7.9操作系统场景下&#xff0c;添加硬盘(磁盘)和挂载硬盘(磁盘)。 版本&#xff1a; 操作系统&#xff1a;CentOS 7.9 1.机器配置 机器名称&#xff1a;B200&#xff1b;主机名称&#xff…

企业级数据中台应用架构和技术架构

一、什么是数据中台 数据中台是一种将企业沉睡的数据变成数据资产&#xff0c;持续使用数据、产生智能、为业务服务&#xff0c;从而实现数据价值变现的系统和机制。通过数据中台提供的方法和运行机制&#xff0c;形成汇聚整合、提纯加工、建模处理、算法学习&#xff0c;并以…