当前位置: 首页 > article >正文

【华为OD】2024D卷——生成哈夫曼树

题目描述:
给定长度为n的无序的数字数组,每个数字代表二叉树的叶子节点的权值,数字数组的值均大于等于1。
请完成一个函数,根据输入的数字数组,生成哈夫曼树,并将哈夫曼树按照中序遍历输出。

为了保证输出的二叉树中序遍历结果统一,增加以下限制:
二叉树节点中,左节点权值小于等于右节点权值,
根节点权值为左右节点权值之和。
当左右节点权值相同时,左子树高度高度小于等于右子树。

注意:所有用例保证有效,并能生成哈夫曼树。

提醒:哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树。
所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度
(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数)。

解题思路:

由于未找到输入输出示例,自己手动创建一个输入权重列表weights = [5,5,15,40,30,10]

根据题意中,哈夫曼树的要求,可以构造树形如下:

故中序遍历结果[40, 105, 30, 65, 15, 35, 10, 20, 5, 10, 5]

构造哈夫曼树

1、创建一个优先级队列(最小堆)并将数组中的每个叶子节点作为一个节点加入堆中。

由于直接将节点压入最小堆中,使其只基于节点的权重进行比较。需要在树节点中重写类的 __lt__() 方法

解决可以解决heapq 无法比较 Node 实例的问题,否则会遇见报错TypeError: '<' not supported between instances of 'TreeNode' and 'TreeNode'。

2、不断从堆中取出两个最小的节点作为左右子树,构造一个新的父节点,其权值为两个节点权值之和,并将新节点插入堆中。

3、重复此过程,直到堆中只剩下一个节点,即为哈夫曼树的根节点。

对限制条件进行处理

1、在合并左右节点时,如果两个节点的权值相等,则优先选择左子树高度小于或等于右子树的情况。

故需要统计每个节点的高度,可以在树节点类型中添加height值。

2、根节点权值为左右节点权值之和。

在构造时,父节点权值 = left_weight + right_weight,重新压入heapq中。

3、如果左右节点的权值不等,左子树权值必须小于等于右子树权值。

heapq弹出的第一个元素为左孩子、第二个为右孩子。

中序遍历输出

使用递归进行中序遍历:左子树 -> 根节点 -> 右子树。


代码部分

class TreeNode:
    def __init__(self, weight, left = None, right = None):
        self.weight = weight
        self.left = left
        self.right = right
        self.height = 1
    #   递归计算节点的高度
    def get_height(self):
        if not self.left and not self.right:
            return 1
        left_height = self.left.get_height() if self.left else 0
        right_height = self.right.get_height() if self.right else 0
        return 1 + max(left_height, right_height)
    #   重写方法,基于节点的权重进行比较。这解决了 heapq 无法比较 Node 实例的问题
    def __lt__(self, other):
        return self.weight < other.weight
import heapq
class Solution:
    def build_huffman_tree(self, weights):
        if not weights:
            return 
        # 初始化最小堆,创建叶子节点
        heap = [TreeNode(weight) for weight in weights]
        heapq.heapify(heap)
        while len(heap) > 1:
            # 取出两个权值最小的节点
            left = heapq.heappop(heap)
            right = heapq.heappop(heap)
            # 如果权值相同,按照高度进行处理
            if left.weight == right.weight:
                left_height = left.get_height()
                right_height = right.get_height()
                # 确保左子树高度 <= 右子树高度
                if left_height > right_height:
                    left, right = right, left
            # 创建新的父节点,权值为左右节点权值之和
            merged_node = TreeNode(left.weight + right.weight, left, right)
            heapq.heappush(heap, merged_node)
        # 返回堆中唯一的节点,即哈夫曼树的根节点
        return heap[0]
    #   栈迭代法进行中序遍历
    def midorder_traversal(self, root:TreeNode):
        res = []
        if root is None:
            return
        stack = [root]
        while stack:
            node = stack.pop()
            if node != None:
                #   按照顺序压栈:右节点、中节点、左节点
                if node.right:
                    stack.append(node.right)
                stack.append(node)
                #   中节点访问过,但是还没有处理,加入空节点做为标记。
                stack.append(None)
                if node.left:
                    stack.append(node.left)
            #  只有遇到空节点的时候,才将下一个节点放进结果集
            else:
                #   按照左节点、中节点、右节点出栈
                node = stack.pop()
                res.append(node.weight)
        return res
    #   递归法进行中序遍历
    def midorder_traversal_I(self, root:TreeNode):
        res = []
        def dfs(node):
            if node is None:
                return
            dfs(node.left)
            res.append(node.weight)
            dfs(node.right)
        dfs(root)
        return res
    #   递归法简化写法
    def midorder_traversal_II(self, root:TreeNode):
        if root is None:
            return []
        return self.midorder_traversal_II(root.left) + [root.weight] + self.midorder_traversal_II(root.right)

if __name__=='__main__':
    weights = list(map(int, input().split(',')))
    solution = Solution()
    huffman_tree = solution.build_huffman_tree(weights)
    result = solution.midorder_traversal(huffman_tree)
    print(result)

拓展:

__lt__() 方法是一个特殊方法(也称为魔术方法或双下划线方法),它用于定义小于(<)运算符的行为。

在尝试比较两个类的实例时,如果这两个实例之间的比较逻辑不是简单的基于它们的值或身份(即不是直接通过 ==!=<<=>>= 这样的操作符),就可以通过定义这些特殊方法来自定义这些操作符的行为。

__lt__() 方法应该接收一个参数(即与当前实例进行比较的另一个实例),并返回一个布尔值:如果当前实例小于传入的参数,则返回 True;否则返回 False。

哈夫曼树的特点:

带权路径长度(WPL, Weighted Path Length)最小

·对于一组给定的字符及其出现的频率(即权重),哈夫曼树能够构造出一种编码方式,使得这些字符的平均编码长度最短。

·这是通过构建一种特殊的二叉树来实现的,其中每个字符都位于树的一个叶节点上,且该叶节点到根节点的路径上的边所代表的字符(在哈夫曼编码中通常使用0和1表示)共同构成了该字符的编码。

贪心算法构建

·哈夫曼树的构建过程采用了贪心算法的策略。

·它从包含n个叶节点的森林开始,每个叶节点代表一个字符及其出现的频率。

·然后,算法反复地选择两个频率最小的节点,将它们合并为一个新的节点,新节点的频率是两个子节点频率之和。

·新节点作为这两个子节点的父节点,而原来的两个节点则成为新节点的左右子节点(通常约定左子节点频率小于等于右子节点)。

·这个过程一直进行,直到森林中只剩下一个节点为止,这个节点即为根节点,整棵树就是所求的哈夫曼树。


知识点:哈夫曼树、堆、二叉树的中序遍历、贪心


结语:越简单的题目解法应该越多,请路过大神留下新的思路供本小白学习一下,打开思路


http://www.kler.cn/a/309247.html

相关文章:

  • 开发语言中,堆区和栈区的区别
  • sql server 查看io资源使用
  • Sigrity SPEED2000 Power Ground Noise Simulation模式如何查看PDS系统的自阻抗操作指导
  • 【日志】392.判断子序列
  • vscode下nuget包的本地引入方法
  • 《AI 使生活更美好》
  • CAD图纸加密软件哪个好?10款2024主流CAD图纸加密软件分享!
  • 如何利用Samba跨平台分享Ubuntu文件夹
  • 电路设计学习(一)
  • 【Day14-单例设计模式动态代理】
  • 一文吃透JVM面试八股文
  • 每日学习一个数据结构-DFA确定有限状态机
  • 【linux】VisiData:强大的命令行数据处理工具
  • 跟李沐学AI:序列到序列seq2seq
  • 本地部署大模型并使用知识库Windows下Ollama+Docker+MaxKB安装的记录
  • 影刀RPE学习——自动化
  • 地大信息-基础信息平台 GetImg 任意文件读取漏洞复现
  • http和https分别是什么?区别是什么?
  • GO GIN SSE DEMO
  • Springboot项目打war包运行及错误解决
  • SpringCloud Alibaba入门简介
  • 最优化理论与自动驾驶(一):概述
  • 你认为嵌入式软件开发的尽头是什么?
  • 了解 React 应用程序中的渲染和重新渲染:它们如何工作以及如何优化它们
  • NEXT.js 中间件 NextResponse.redirect 无效
  • 2576. 求出最多标记下标(24.9.12)