1. 问题背景与需求分析
今天在LeetCode上刷到一道挺有意思的二叉树题目(编号987),要求实现二叉树的垂序遍历。第一次看到这个题目时,我有点懵——平时常见的二叉树遍历方式无非前序、中序、后序和层序遍历,这个"垂序"是什么操作?
仔细阅读题目描述后发现,垂序遍历(Vertical Traversal)的定义是这样的:假设我们给二叉树的每个节点赋予坐标 (x,y),根节点位于 (0,0),左子节点坐标 (x-1,y+1),右子节点坐标 (x+1,y+1)。然后按照x坐标从小到大,y坐标从小到大,节点值从小到大的顺序输出所有节点。
举个例子,对于二叉树:
code复制 1
/ \
2 3
/ \ / \
4 5 6 7
垂序遍历结果应该是:[[4],[2],[1,5,6],[3],[7]]
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解题思路与算法设计
2.1 坐标映射与数据结构选择
要实现垂序遍历,首先需要为每个节点计算坐标。这可以通过修改版的DFS或BFS实现。我选择使用BFS,因为:
- BFS天然适合处理层级(y坐标)关系
- 可以保证同一层节点按从左到右顺序处理
我们需要一个数据结构来存储坐标和节点值的关系。这里使用TreeMap(Java)或OrderedDict(Python)来维护x坐标到节点列表的映射,因为:
- 需要按x坐标排序输出
- 每个x坐标对应的节点列表需要按y坐标和值排序
2.2 具体实现步骤
- 初始化:创建队列用于BFS,存储节点及其坐标;创建TreeMap存储结果
- BFS遍历:
- 根节点入队,坐标(0,0)
- 出队节点,记录其坐标和值
- 左子节点入队,坐标(x-1,y+1)
- 右子节点入队,坐标(x+1,y+1)
- 排序处理:对同一坐标(x,y)的多个节点按值排序
- 结果生成:按x顺序输出节点列表
2.3 边界条件考虑
- 空树:直接返回空列表
- 所有节点值相同:保持原始顺序
- 单侧倾斜树:确保坐标计算正确
3. Java实现与代码解析
java复制class Solution {
public List<List<Integer>> verticalTraversal(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) return result;
// 使用TreeMap自动按x排序
TreeMap<Integer, TreeMap<Integer, PriorityQueue<Integer>>> map = new TreeMap<>();
// BFS队列,存储节点和坐标
Queue<Pair<TreeNode, int[]>> queue = new LinkedList<>();
queue.offer(new Pair<>(root, new int[]{0, 0}));
while (!queue.isEmpty()) {
Pair<TreeNode, int[]> pair = queue.poll();
TreeNode node = pair.getKey();
int x = pair.getValue()[0];
int y = pair.getValue()[1];
// 初始化数据结构
if (!map.containsKey(x)) {
map.put(x, new TreeMap<>());
}
if (!map.get(x).containsKey(y)) {
map.get(x).put(y, new PriorityQueue<>());
}
// 添加节点值
map.get(x).get(y).offer(node.val);
// 处理子节点
if (node.left != null) {
queue.offer(new Pair<>(node.left, new int[]{x - 1, y + 1}));
}
if (node.right != null) {
queue.offer(new Pair<>(node.right, new int[]{x + 1, y + 1}));
}
}
// 构建结果
for (TreeMap<Integer, PriorityQueue<Integer>> ys : map.values()) {
List<Integer> list = new ArrayList<>();
for (PriorityQueue<Integer> nodes : ys.values()) {
while (!nodes.isEmpty()) {
list.add(nodes.poll());
}
}
result.add(list);
}
return result;
}
}
3.1 代码关键点解析
-
三层嵌套数据结构:
- 外层TreeMap:按x坐标排序
- 中层TreeMap:按y坐标排序
- 内层PriorityQueue:对同一(x,y)的节点按值排序
-
BFS实现细节:
- 使用Pair同时存储节点和坐标
- 每次处理节点时更新三个层级的Map
-
结果构建:
- 按x顺序遍历
- 对每个x,按y顺序遍历
- 对每个(x,y),按值顺序输出
4. 复杂度分析与优化思路
4.1 时间复杂度
- BFS遍历:O(N),每个节点访问一次
- 排序操作:
- TreeMap插入:O(logN) per operation
- PriorityQueue插入:O(logK),K为同一(x,y)的节点数
- 总体:O(NlogN),最坏情况下所有节点在同一(x,y)
4.2 空间复杂度
- 存储所有节点:O(N)
- 数据结构开销:O(N)
- 总体:O(N)
4.3 优化方向
-
减少排序开销:
- 可以在BFS后统一排序,而不是使用自动排序的数据结构
- 对Java来说,使用List然后Collections.sort可能更高效
-
内存优化:
- 可以使用更紧凑的数据结构
- 例如,用数组代替多层Map
-
并行处理:
- BFS的各层可以并行处理
- 适合大规模树结构
5. 测试用例与调试技巧
5.1 必备测试用例
java复制// 用例1:普通二叉树
// 1
// / \
// 2 3
// / \ / \
// 4 5 6 7
// 预期:[[4],[2],[1,5,6],[3],[7]]
// 用例2:所有节点值相同
// 1
// / \
// 1 1
// 预期:[[1],[1,1],[1]]
// 用例3:左斜树
// 1
// /
// 2
// /
// 3
// 预期:[[3],[2],[1]]
// 用例4:空树
// 预期:[]
5.2 调试技巧
-
可视化坐标:
- 打印每个节点的(x,y)和值
- 验证坐标计算是否正确
-
中间结果检查:
- 在构建最终结果前,检查TreeMap的结构
- 确保每个层级的排序正确
-
边界条件测试:
- 特别注意空树、单节点、全相同值等情况
- 测试极端倾斜的树结构
6. 常见问题与解决方案
6.1 为什么使用PriorityQueue而不是List?
当多个节点具有相同的(x,y)坐标时,需要按值排序。PriorityQueue可以在插入时自动维护顺序,比最后统一排序更高效。
6.2 如何处理相同值的节点?
题目没有明确说明,但通常保持原始顺序即可。PriorityQueue对于相同值的节点可能改变顺序,如果需要严格保持原始顺序,可以使用List然后自定义比较器。
6.3 为什么我的结果顺序不对?
常见原因:
- 坐标计算错误(特别是左右子节点的x坐标)
- 排序层级不对(应先按x,再按y,最后按值)
- 数据结构选择不当(没有自动排序功能)
6.4 如何优化内存使用?
如果内存是瓶颈:
- 使用数组代替Map
- 先收集所有节点再统一排序
- 使用更紧凑的坐标表示方法
7. 变种问题与扩展思考
7.1 水平遍历(Horizontal Traversal)
类似垂序遍历,但改为按y坐标排序。实现方式类似,只需调整排序优先级。
7.2 对角线遍历(Diagonal Traversal)
定义对角线为x+y相同的节点。可以修改坐标映射方式,按x+y分组。
7.3 三维坐标扩展
如果给二叉树增加第三个维度(如z坐标),可以定义更复杂的遍历顺序。数据结构需要进一步扩展。
7.4 实际应用场景
- 二叉树可视化:垂序遍历有助于绘制二叉树的垂直布局
- 数据序列化:特定顺序的遍历可用于二叉树序列化
- 数据库索引:类似B树的结构需要多种遍历方式
8. 个人实现心得
在实际实现过程中,我最初尝试用DFS+HashMap的方案,发现排序处理起来很麻烦。后来切换到BFS+TreeMap的方案,代码清晰了很多。几个关键体会:
- 数据结构的选择至关重要:TreeMap和PriorityQueue的组合让排序逻辑变得简单
- 坐标计算要小心:特别是左右子节点的x坐标变化方向容易搞反
- 测试要全面:特别是边界条件,如空树、所有节点值相同等情况
对于Java实现,使用Pair类来同时存储节点和坐标很方便。如果使用较旧版本的Java没有Pair,可以自定义一个简单类或者用Map.Entry代替。
