华为OD机试中关于生成哈夫曼树的题目通常要求根据给定的叶子节点权值数组,构建一棵哈夫曼树,并按照某种遍历方式(如中序遍历)输出树中节点的权值序列。以下是对这道题目的详细解析和解答思路:
一、题目要求
给定一个长度为n的正整数数组,每个数字代表二叉树叶子节点的权值。要求生成一棵哈夫曼树,并将其按中序遍历的顺序输出。树中每个非叶子节点的权值等于其左右子节点权值之和。对于权值相同的两个节点,左子树的高度应小于等于右子树的高度。在满足上述条件的前提下,左子节点的权值应小于等于右子节点的权值。
二、哈夫曼树简介
哈夫曼树是一种带权最优二叉树,其特点是带权路径长度最短。所谓带权路径长度,是指树中所有叶子节点的权值乘上其到根节点的路径长度(若根节点为0层,则叶子节点到根节点的路径长度为叶子节点的层数)之和。
三、解题思路
- 构建森林:将每个叶子节点看作一棵独立的树,并将这些树放入一个优先队列(最小堆)中。优先队列用于每次选择权值最小的两个节点进行合并。
- 合并节点:从优先队列中取出权值最小的两个节点,将它们作为新树的左右子树,并计算新树的权值为两棵子树权值之和。然后,将新树加入优先队列中。
- 重复合并:重复上述步骤,直到优先队列中只剩下一棵树,这棵树即为所求的哈夫曼树。
- 中序遍历:对构建好的哈夫曼树进行中序遍历,得到节点权值序列,并按要求输出。
四、代码实现
java
import java.util.*;
class HuffmanNode implements Comparable<HuffmanNode> {
int weight;
HuffmanNode left, right;
HuffmanNode(int weight) {
this.weight = weight;
this.left = this.right = null;
}
@Override
public int compareTo(HuffmanNode other) {
return this.weight - other.weight;
}
}
public class HuffmanTree {
/**
* 构建哈夫曼树
* 哈夫曼树是一种带权路径长度最短的二叉树,也称为最优二叉树
* 在数据压缩、编码等领域有广泛的应用
*
* @param weights 每个节点的权重,权重越小的节点,在构建哈夫曼树时,越靠近根节点
* @return 返回哈夫曼树的根节点
*/
public static HuffmanNode buildHuffmanTree(int[] weights) {
// 优先队列,用于存储哈夫曼树的节点,队列头部元素权值最小
PriorityQueue<HuffmanNode> pq = new PriorityQueue<>();
// 遍历所有节点权重,初始化哈夫曼树节点并加入优先队列
for (int weight : weights) {
pq.add(new HuffmanNode(weight));
}
// 当优先队列中节点数量大于1时,循环构建哈夫曼树
while (pq.size() > 1) {
// 从优先队列中取出权值最小的两个节点
HuffmanNode left = pq.poll();
HuffmanNode right = pq.poll();
// 断言right节点非空,确保树能正常构建
assert right != null;
// 创建新的哈夫曼节点,作为left和right节点的父节点,其权重为两个子节点权重之和
HuffmanNode parent = new HuffmanNode(left.weight + right.weight);
// 设置父节点的左右子节点
parent.left = left;
parent.right = right;
// 将新的父节点加入优先队列
pq.add(parent);
}
// 返回优先队列中剩余的唯一节点,即哈夫曼树的根节点
return pq.poll();
}
/**
* 中序遍历哈夫曼树
* 中序遍历的特点是左子树-根节点-右子树的顺序访问树中的节点
* 这种遍历顺序在哈夫曼树中主要用于验证树的结构,而不用于构建最短编码
*
* @param root 哈夫曼树的根节点
* @param result 用于存储遍历结果的列表,通常是节点的权重
*/
public static void inorderTraversal(HuffmanNode root, List<Integer> result) {
// 判断当前节点是否为空,为空则直接返回
if (root == null) {
return;
}
// 递归中序遍历左子树
inorderTraversal(root.left, result);
// 将当前节点的权重添加到结果列表中
result.add(root.weight);
// 递归中序遍历右子树
inorderTraversal(root.right, result);
}
/**
* 主函数入口
* 本程序用于演示霍夫曼树的构建及其遍历过程
* 霍夫曼树是一种带权路径长度最短的二叉树,具有重要应用价值
*
* @param args 命令行参数
*/
public static void main(String[] args) {
// 创建Scanner对象,用于读取命令行输入
Scanner scanner = new Scanner(System.in);
// 读取霍夫曼树节点数量
int n = scanner.nextInt();
// 初始化霍夫曼树节点权重数组
int[] weights = new int[n];
// 读取每个节点的权重
for (int i = 0; i < n; i++) {
weights[i] = scanner.nextInt();
}
// 构建霍夫曼树并返回根节点
HuffmanNode root = buildHuffmanTree(weights);
// 初始化用于存储中序遍历结果的列表
List<Integer> result = new ArrayList<>();
// 中序遍历霍夫曼树,并将结果存储到列表中
inorderTraversal(root, result);
// 输出中序遍历结果
for (int weight : result) {
System.out.print(weight + " ");
}
}
}
五、代码解析
-
HuffmanNode 类:
HuffmanNode
是一个表示哈夫曼树节点的类。- 它包含三个属性:
weight
(节点权重),left
(左子节点),right
(右子节点)。 - 实现了
Comparable<HuffmanNode>
接口,以便节点可以根据权重进行排序。
-
HuffmanTree 类:
- 包含构建哈夫曼树的静态方法
buildHuffmanTree
。 - 包含进行中序遍历的静态方法
inorderTraversal
。 main
方法用于读取输入、构建哈夫曼树、进行中序遍历并输出结果。
- 包含构建哈夫曼树的静态方法
-
main 方法:
- 读取节点数量
n
和每个节点的权重。 - 调用
buildHuffmanTree
方法构建哈夫曼树。 - 初始化结果列表
result
并调用inorderTraversal
方法进行中序遍历。 - 输出遍历结果。
- 读取节点数量
六、运行实例解析
输入:
5
5 15 40 30 10
步骤:
-
读取输入:
- 节点数量
n = 5
。 - 节点权重数组
weights = [5, 15, 40, 30, 10]
。
- 节点数量
-
构建哈夫曼树:
- 初始化优先队列
pq
并添加所有节点。 - 不断从
pq
中取出两个最小权重的节点,合并成一个新节点,并将新节点添加回pq
。 - 最终,
pq
中剩下的唯一节点即为哈夫曼树的根节点。
构建过程(可能的一种情况,因为合并顺序可能不同):
- 初始:
pq = [5, 10, 15, 30, 40]
- 合并
5
和10
,得到新节点15
,加入pq
:pq = [15, 15, 30, 40]
- 合并两个
15
,得到新节点30
,加入pq
:pq = [30, 30, 40]
- 合并
30
和30
,得到新节点60
,加入pq
:pq = [60, 40]
- 合并
60
和40
,得到根节点100
。
- 初始化优先队列
-
中序遍历:
- 由于哈夫曼树不是二叉搜索树,中序遍历的结果取决于节点的合并顺序和树的结构。
- 假设按上述构建过程,中序遍历结果可能是一种特定的节点权重顺序(注意,这不是唯一的)。
-
输出结果:
- 输出中序遍历得到的节点权重列表。
注意:由于哈夫曼树的构建过程中节点合并顺序可能不同(当存在相同权重的节点时),因此中序遍历的结果也可能不同。上述构建过程和中序遍历结果是基于一种假设的合并顺序。
七、实际输出
由于代码中的中序遍历会按照左子树-根节点-右子树的顺序访问节点,并且哈夫曼树的构建依赖于节点的合并顺序,因此实际输出将取决于具体的合并过程。要获得确切的输出,需要运行代码并观察结果。
如果您想要验证哈夫曼树的正确性,通常不会使用中序遍历,而是会检查每个节点的权重和从根到该节点的路径长度(用于计算哈夫曼编码的长度等)。中序遍历在这里主要用于演示和验证树的结构。