【华为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个叶节点的森林开始,每个叶节点代表一个字符及其出现的频率。
·然后,算法反复地选择两个频率最小的节点,将它们合并为一个新的节点,新节点的频率是两个子节点频率之和。
·新节点作为这两个子节点的父节点,而原来的两个节点则成为新节点的左右子节点(通常约定左子节点频率小于等于右子节点)。
·这个过程一直进行,直到森林中只剩下一个节点为止,这个节点即为根节点,整棵树就是所求的哈夫曼树。
知识点:哈夫曼树、堆、二叉树的中序遍历、贪心
结语:越简单的题目解法应该越多,请路过大神留下新的思路供本小白学习一下,打开思路