数据结构-构造哈夫曼树【详解+代码+图示】一文解惑!

news2025/1/23 12:13:32

哈夫曼树 (Huffman Tree)

文章目录

  • 哈夫曼树 (Huffman Tree)
    • 导论
    • 构造哈夫曼树
      • 语言描绘
      • 图形化理解
    • 证明结论
    • 代码实现
    • 测试
    • 结果对照
    • 总结

导论

我们在学习哈夫曼树之前需要先了解 什么是哈夫曼树?

  • 哈夫曼树 是一种最优树,是一类带权路径长度最短的二叉树,通过哈夫曼算法可以构建一棵哈夫曼树,利用哈夫曼树可以构造一种不等长的二进制编码,并且构造所得的哈夫曼编码是一种最优前缀码.

  • 通俗来讲 : n 个带权节点均作为叶子节点,构造出的一棵带权路径长度最短的二叉树,则把这棵树称为"哈夫曼树" 、“赫夫曼树” 、“Huffman Tree” 或者 “最优二叉树” . (备注 : 本文均用 “哈夫曼树” 来称呼)


第一次接触 哈夫曼树 的小伙伴可能对上述的定义不能第一时间消化,我们可以先来明确几个学哈夫曼树之前必须掌握的几个会用到的名词的概念,把这几个概念理解透了之后会帮助你更好的认识哈夫曼树.

  • 路径: 在一棵树中 ,从一个结点到另外一个结点的通路就称为 树的路径 .如图1.1 从根节点root到D节点之间的通路就是二叉树中的一条路径
  • 路径长度 : 在路径中,每经过一个结点路径长度就加一 .如果我们规定根节点所在的树的 层数为第一层,那么从根节点出发 走到第 i 层 ,那么当前层的节点的 路径长度 等于 i-1,如图1.1所示
  • 树的路径长度 : 从根节点出发到所有结点的路径长度之和.
  • 结点的权值 :  一个结点的权值实际上就是这个结点子树在整个树中所占的比例. 如图1.1 所示 ,C、A、D、B、E结点的权分别为2、5、9、6、7.
  • 结点的带权路径长度 : 结点到树根之间的路径长度与该结点权值的乘积.如图1.1 所示 ,D结点的带权路径长度 = 2 * 9 = 18
  • 树的带权路径长度 ( Weighted Path Length of Tree 简称为 WPL): 树中所有叶子结点的带权路径长度之和.如图1.1 所示,该哈夫曼树的带权路径长度 WPL =2 * 3 + 5 * 3 + 9 * 2 + 6 * 2 + 7 * 2 = 65
    ![image-20231129173839803](https://img-blog.csdnimg.cn/img_convert/67d545380210f6612为二叉树第 **i** 个叶子结点的权值,![image-20231129173908282](https://img-blog.csdnimg.cn/img_convert/264283ffeadb2ca4580159f32d6d9509.png)为从根结点到第 **i** 个叶子结点的路径长度,则有
    image-20231129165622248

构造哈夫曼树

要记住 哈夫曼树 中的权值越大的结点离根节点越近,权值越小的结点离根节点越远,这意味着一组带权结点构造出来的哈夫曼树中权值最小的结点离哈夫曼树最远,权值最大的结点离哈夫曼树最近,也就是我们 图1.1 中可以观察出来的.

构造哈夫曼树的过程其实就是"爸爸去哪儿"的过程.

语言描绘

  • 形象叙述构造哈夫曼树的步骤

    从下往上构造,从已知的剩余权值集合中权值最小的那两个结点(如果权值相同那么直接就是相同的这两个结点),让他们成为兄弟结点,两兄弟的权值相加,合力找到父亲结点,此时父亲节点的权值就是两兄弟结点的权值的和,然后把父亲节点的权值添加到权值集合中的同时删除这两个兄弟节点,.重复循环"爸爸去哪儿"这个过程,直至权值集合中只剩下祖宗(根节点)为止. 那么这棵哈夫曼树也就构造完了.

    备注: 感觉核心就是你现在计划创建一棵二叉树,但是这棵二叉树你不能随意创建,而是要按照生成 “最优二叉树” 的规则来创建 ,这样想是不是就简单许多 ! 而这个规则就是哈夫曼算法.


  • 严谨的构造哈夫曼树的步骤

对于给定的有各自权值的 n 个结点

  1. 在 n 个权值中选出两个最小的权值,对应的两个结点组成一个新的二叉树,且新二叉树的根结点的权值为左右孩子权值的和;
  2. 在原有的 n 个权值中删除那两个最小的权值,同时将新的权值加入到 n–2 个权值的行列中,以此类推;
  3. 重复 1 和 2 ,直到所以的结点构建成了一棵二叉树为止,这棵树就是哈夫曼树/最优二叉树。

注意:

  1. 最优二叉树的形态不唯一,但是WPL最小.
  2. 最优二叉树中,权越大的叶子离根越近.

图形化理解

例如: 已知权值 W = {2,5,9,6,7},请构造哈夫曼树.

image-20231129185547329


证明结论

最优二叉树的形态不唯一,但是WPL最小

  • 先证明第一句话: 相同的权值构造出来的哈夫曼树形态不唯一

同样是上述的例子,我们可以构造出另一种形态的哈夫曼树,同时我们可以计算出两种形态的二叉树的带权路径,看看是否相等 ?

image-20231129190728301


  • 然后我们来证明第二句话 : 证明哈夫曼算法构造出的哈夫曼树的WPL一定是最小的,且其他二叉树的WPL一定大于等于哈夫曼树的WPL。

1. 定义:

  • 带权路径长度(WPL)是指每个叶子节点的权值乘以其深度的总和。

2. 引理:

  • 对于任意的二叉树,交换任意两个叶子节点的位置,WPL都会增加。

3. 证明:

  • 哈夫曼树的构造过程

    1. 哈夫曼算法每次选择权值最小的两个节点合并,构造出一颗二叉树。
    2. 重复这个过程,直到所有节点合并成一个根节点,形成哈夫曼树。
  • 交换叶子节点的影响

    • 考虑哈夫曼树的构造过程,每次选择最小的两个节点合并,如果我们交换了两个叶子节点的位置,这两个节点的权值就不再是最小的了。
    • 在构造过程中,我们总是选择权值最小的节点,因此交换叶子节点位置后,这两个节点的合并会在构造树的过程中晚一些,导致它们在更深的层次上,从而增加了WPL。

    如果这里不懂这里的影响是什么,我会再下一篇的博客中详细解释,这涉及到了 贪心策略

  • 结论

    • 由于哈夫曼算法保证了每次合并都选择最小权值的节点,任何对于叶子节点位置的交换都会导致合并发生在更深的层次上,增加WPL。
    • 因此,哈夫曼树的构造方式保证了WPL最小。

4. 总结:

  • 哈夫曼树的构造方式保证了每次合并都是最优的选择,从而使得整个树的WPL最小。
  • 如果使用其他方式构造二叉树,由于不能保证每次都选择最小权值的节点进行合并,可能会导致WPL更大。

代码实现

在构造哈夫曼树的过程中,我们首先需要定义一个节点类来表示树的节点,然后编写一个构造哈夫曼树的算法。

  1. 定义了一个HuffmanNode类,用于表示哈夫曼树的节点
  2. HuffmanTree类中,实现一个buildHuffmanTree方法,该方法接受一个权值数组
  3. 使用优先队列来构造哈夫曼树
  4. 通过main方法演示了如何使用这个方法来构造哈夫曼树

这个实现涉及到了优先队列(PriorityQueue),它是一个能够保证每次取出的元素都是队列中权值最小的元素的数据结构。在哈夫曼树的构建中,我们不断地合并权值最小的两个节点,而优先队列正好能够满足这个需求。

package src.test.java;

import java.util.PriorityQueue;

// 定义哈夫曼树节点类
class HuffmanNode implements Comparable<HuffmanNode> {
    int weight;          // 权值
    HuffmanNode left;    // 左子节点
    HuffmanNode right;   // 右子节点

    public HuffmanNode(int weight) {
        this.weight = weight;
    }

    // 实现Comparable接口,用于PriorityQueue的比较
    @Override
    public int compareTo(HuffmanNode other) {
        return this.weight - other.weight;
    }
}

public class HuffmanTree {

    // 构造哈夫曼树的方法
    public static HuffmanNode buildHuffmanTree(int[] weights) {
        // 使用优先队列来存储节点,每次都取出权值最小的两个节点进行合并
        PriorityQueue<HuffmanNode> pq = new PriorityQueue<>();
        
        // 将权值数组中的每个元素转化为节点并添加到优先队列中
        for (int weight : weights) {
            pq.add(new HuffmanNode(weight));
        }

        // 不断合并节点直到只剩一个节点,即哈夫曼树的根节点
        while (pq.size() > 1) {
            HuffmanNode left = pq.poll();   // 弹出权值最小的节点
            HuffmanNode right = pq.poll();  // 弹出权值次小的节点

            // 创建一个新节点,权值为两个子节点的权值之和
            HuffmanNode parent = new HuffmanNode(left.weight + right.weight);
            parent.left = left;
            parent.right = right;

            // 将新节点添加回优先队列
            pq.add(parent);
        }

        // 返回哈夫曼树的根节点
        return pq.poll();
    }

    // 打印哈夫曼树的方法(可选)
    public static void printHuffmanTree(HuffmanNode root, String prefix) {
        if (root != null) {
            System.out.println(prefix + root.weight);
            printHuffmanTree(root.left, prefix + "0");
            printHuffmanTree(root.right, prefix + "1");
        }
    }

    public static void main(String[] args) {
        int[] weights = {2, 5, 9, 6, 7};

        // 构造哈夫曼树
        HuffmanNode root = buildHuffmanTree(weights);

        // 打印哈夫曼树(可选)
        printHuffmanTree(root, "");
    }
}


测试

package src.test.java;

public class HuffmanTreeTest {

    public static void main(String[] args) {
        int[] weights = {2, 5, 9, 6, 7};

        // 构造哈夫曼树
        HuffmanNode root = HuffmanTree.buildHuffmanTree(weights);

        // 打印哈夫曼树的结构
        System.out.println("Huffman Tree Structure:");
        printHuffmanTreeStructure(root, "");
    }

    // 递归打印哈夫曼树的结构
    private static void printHuffmanTreeStructure(HuffmanNode root, String prefix) {
        if (root != null) {
            System.out.println(prefix + "Weight: " + root.weight);
            if (root.left != null || root.right != null) {
                System.out.println(prefix + "├── Left:");
                printHuffmanTreeStructure(root.left, prefix + "│    ");
                System.out.println(prefix + "└── Right:");
                printHuffmanTreeStructure(root.right, prefix + "     ");
            }
        }
    }
}

结果对照

  • 控制台输出结果

image-20231129195843163

  • 哈夫曼树图形

image-20231129195856013


总结

  1. 基础概念:深入了解了哈夫曼树的概念,包括路径、路径长度、带权路径长度等基础知识。

  2. 构造过程:通过图形化和语言描述,清晰演示了从下而上构造哈夫曼树的步骤,以及最终得到最优二叉树的过程。

  3. 证明方法:通过引理和推导,解释了为何哈夫曼树的构造方式能够保证带权路径长度最小,涉及到了贪心策略的应用。

  4. 实际应用:通过Java代码实现展示了哈夫曼树的构造过程,将理论知识转化为实际应用,加深了对算法的理解。

    image-20231129200715576

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

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

相关文章

虚幻学习笔记7—蓝图接口

一、前言 蓝图接口就是可以在蓝图中实现的接口&#xff0c;有它方便的地方&#xff0c;可以很方便的调用到实现了接口的函数。 二、实现 2.1、创建一个蓝图接口 1&#xff09;可以添加多个函数。 2&#xff09;函数在蓝图接口中只能规定输入和输出参数。 只有输入参数的可以…

Vue3生命周期函数(简述题)

1.图示 2.说明 3.补充 1.在vue3组合式API中&#xff0c;我们需要将生命周期函数先导入&#xff0c;然后才能使用。 import {onMounted} from vue2.beforeCreate和created被setup()方法所代替

Revit导出3D模型插件【GLTF|OBJ|DAE|STL|PLY|OFF|XYZ】

3dconvert_for_revit插件是NSDT 3DConvert工具集中的一种&#xff0c;可以快速将Revit模型导出为8种目标格式&#xff1a;GLTF、OBJ、GLB、DAE、STL、OFF、XYZ和PLY。 用户在进行格式转换之前&#xff0c;需要先下载安装对应Revit版本的插件。 NSDT在线工具推荐&#xff1a; T…

RabbitMQ消息模型之发布订阅Publish-Subscribe

发布订阅模型 Publish/Subscribe 发布订阅模型也称为广播模型&#xff0c;交换机类型需要指定为Fanout&#xff0c;正如从名称中猜到的那样&#xff0c;它是将接收到的所有消息广播到它知道的所有队列中。每个消费者都监听自己的队列&#xff0c;所以同一个消息&#xff0c;会…

多模块项目打包部署

目录结构 这些模块间相互依赖&#xff0c;打包的时候打父模块&#xff0c;就是带root的这个 先clean&#xff0c;再package&#xff0c;就跟一般的项目一样了。 有可能遇到报错Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.22.2:test&#xff…

[Java]JUC并发编程

JUC并发编程 一、什么是JUC 使用到 java.util 工具包、包、分类 二、线程和进程 进程&#xff1a;一个正在运行的程序&#xff0c;QQ.exe Music.exe 程序的集合&#xff1b; 一个进程往往可以包含多个线程&#xff0c;至少包含一个&#xff01; Java默认有两个线程&#x…

微服务链路追踪组件SkyWalking实战

概述 微服务调用存在的问题 串联调用链路&#xff0c;快速定位问题&#xff1b;理清服务之间的依赖关系&#xff1b;微服务接口性能分析&#xff1b;业务流程调用处理顺序&#xff1b; 全链路追踪&#xff1a;对请求源头到底层服务的调用链路中间的所有环节进行监控。 链路…

短 URL 生成器设计:百亿短 URL 怎样做到无冲突?

Java全能学习面试指南&#xff1a;https://javaxiaobear.cn 我们先来看看&#xff0c;当高并发遇到海量数据处理时的架构。在社交媒体上&#xff0c;人们经常需要分享一些 URL&#xff0c;但是有些 URL 可能会很长&#xff0c;比如&#xff1a; https://time.geekbang.org/hyb…

C#图像处理OpenCV开发指南(CVStar,03)——基于.NET 6的图像处理桌面程序开发实践第一步

1 Visual Studio 2022 开发基于.NET 6的OpenCV桌面程序 1.1 为什么选择.NET 6开发桌面应用&#xff1f; 选择 .NET 6&#xff08;最早称为 .NET Core&#xff09;而非 Frameworks.NET 的理由是&#xff1a;&#xff08;1&#xff09;跨平台&#xff1b;已经支持Windows,Linux…

PyQt基础_008_ 按钮类控件QSpinbox

基本操作 import sys from PyQt5.QtCore import * from PyQt5.QtGui import * from PyQt5.QtWidgets import *class spindemo(QWidget):def __init__(self, parentNone):super(spindemo, self).__init__(parent)self.setWindowTitle("SpinBox 例子")self.resize(300,…

错误 LNK2001 无法解析的外部符号 __imp__CrtDbgReport

”属性“ -->”设置“ --> ”c“ – > ”代码生成“ --> ”运行库“ &#xff0c;将 ”多线程(MT)“ 改为 ”多线程(MTD)“。

JavaScript包装类型

前端面试大全JavaScript包装类型 &#x1f31f;经典真题 &#x1f31f;包装类型 &#x1f31f;真题解答 &#x1f31f;总结 &#x1f31f;经典真题 是否了解 JavaScript 中的包装类型&#xff1f; &#x1f31f;包装类型 在 ES 中&#xff0c;数据的分类分为基本数据类型…

NCo3.1(08) - Nco3 服务器端编程

本篇博文不再重复ABAP调用外部服务器的基础&#xff0c;只介绍 NCo3 开发的过程和要点。需要了解相关知识点的小伙伴们自行参考&#xff1a; SAP接口编程 之JCo3.0系列(06) - Jco服务器端编程 PyRFC 服务器端编程要点 创建项目 新建一个 Console 项目&#xff0c;选择 .Net …

第一个C代码讲解

文章目录 编写C文件创建文本文件编写代码修改文件后缀切换文件路径 编译代码打开命令行使用gcc编译代码运行程序双击运行使用命令行运行 代码分析编译过程 编写C文件 编辑C代码文件的工具有很多&#xff0c;为了让大家初学的时候摆脱编译软件的干扰&#xff0c;更容易理解编译过…

数据结构 -- 并查集与图

目录 1.并查集 1.结构 2.原理 3.代码实现 1.存储 2.寻找根节点 3.是否为同一集合 4.求集合个数 5.合并为同一集合中 整体代码 2.图 1.基本知识 1.各个属性 2.特殊名词 3.图的解释 2.图的表示 1.邻接矩阵 2.邻接表 3.图的遍历 1.BFS--广度优先遍历 2.DFS--…

Python with提前退出:坑与解决方案

Python with提前退出&#xff1a;坑与解决方案 问题的起源 早些时候使用with实现了一版全局进程锁&#xff0c;希望实现以下效果&#xff1a; Python with提前退出&#xff1a;坑与解决方案 全局进程锁本身不用多说&#xff0c;大部分都依靠外部的缓存来实现的&#xff0c;r…

python读取excel自动化生成sql建表语句和java实体类字段

1、首先准备一个excel文件&#xff1a; idtypenameidint学号namestring姓名ageint年龄sexstring性别weightdecimal(20,4)体重scoredecimal(20,4)分数 2、直接生成java字段和注释&#xff1a; import pandas as pddf pd.read_excel(test.xlsx, sheet_nameSheet1)for i in ran…

k8s中批量处理Pod应用的Job和CronJob控制器介绍

目录 一.Job控制器 1.简介 2.Jobs较完整解释 3.示例演示 4.注意&#xff1a;如上例的话&#xff0c;执行“kubectl delete -f myJob.yaml”就可以将job删掉 二.CronJob&#xff08;简写为cj&#xff09; 1.简介 2.CronJob较完整解释 3.案例演示 4.如上例的话&#xf…

【MySQL】常用内置函数:数值函数 / 字符串函数 / 日期函数 / 其他函数

文章目录 数值函数round()&#xff1a;四舍五入ceiling()&#xff1a;上限函数floor()&#xff1a;地板函数abs()&#xff1a;计算绝对值rand()&#xff1a;生成0-1的随机浮点数 字符串函数length()&#xff1a;获取字符串中的字符数upper() / lower()&#xff1a;将字符串转化…

使用Ray创建高效的深度学习数据管道

大家好&#xff0c;用于训练深度学习模型的GPU功能强大但价格昂贵。为了有效利用GPU&#xff0c;开发者需要一个高效的数据管道&#xff0c;以便在GPU准备好计算下一个训练步骤时尽快将数据传输到GPU&#xff0c;使用Ray可以大大提高数据管道的效率。 1.训练数据管道的结构 首…