题目描述

✅ 987. 二叉树的垂序遍历

image-20260928225440695

image-20260928225440697

image-20260928225440698

image-20260928225440699

题意分析

为二叉树节点赋予行列坐标:根在 (0, 0),左孩子的行加一、列减一,右孩子的行加一、列加一。按列从左到右输出,每一列内部按行从上到下排列;如果多个节点处于同一行同一列,再按节点值从小到大排列。

返回按列分组的节点值列表。同一列不同高度的节点不能直接按值排序,同行同列也不能沿用遍历先到先出的顺序。不同节点即使坐标和值都相同,仍分别保留,不进行去重。

解法:坐标采集后排序分组

核心思路

[!blue]

题目对输出提出了三个依次生效的排序条件,可以先完整记录每个节点的坐标和值,再统一排序。这样遍历只负责不漏节点,排序负责最终顺序,两件事不必混在一起处理。

DFS 从根的行列零开始。访问节点时记录三元组 (col, row, value),再将左孩子坐标更新为 (row + 1, col - 1)、右孩子更新为 (row + 1, col + 1)。递归参数按行列传递,而记录把列放在第一项,是为了与排序优先级一致。

排序先比较列,列不同时由列决定先后;同列再比较行,较浅的节点在前;行也相同时才比较值。这个字典序恰好覆盖题目的全部规则,所以无论 DFS 先收集哪一支,最终结果都一致。

排序完成后,同一列的节点一定连续出现,且列内顺序已经正确。再扫描一次,遇到新列就创建结果组,列相同则追加到当前组。hasCol 用来区分是否已经建立第一组,避免把初始的 prevCol = 0 错当成已经处理过零列。

单用 BFS 虽然能保证先浅后深,却不能保证同层同列时按值递增,因此仍需处理这个最终排序条件。统一收集排序可以直接避免遗漏这一规则。

解题步骤

  1. 从根坐标开始遍历,收集每个节点的列、行和值。
  2. 按列升序、行升序、值升序的优先级排序全部记录。
  3. 初始化空结果和“尚未处理任何列”的标记。
  4. 依次扫描记录,首次遇到节点或列号变化时创建新组,然后追加当前节点值。
  5. 返回按列分组的结果。

代码实现

class Solution {
    public List<List<Integer>> verticalTraversal(TreeNode root) {
        List<int[]> nodes = new ArrayList<>();

        dfs(root, 0, 0, nodes);

        // 按列、行、值依次比较,值只决定同行同列的顺序。
        nodes.sort(
                (a, b) -> {
                    if (a[0] != b[0]) {
                        return Integer.compare(a[0], b[0]);
                    }

                    if (a[1] != b[1]) {
                        return Integer.compare(a[1], b[1]);
                    }

                    return Integer.compare(a[2], b[2]);
                });

        List<List<Integer>> res = new ArrayList<>();
        int prevCol = 0;
        boolean hasCol = false;

        for (int[] node : nodes) {
            // 首个节点或列变化时,开启一组结果。
            if (!hasCol || node[0] != prevCol) {
                res.add(new ArrayList<>());
                hasCol = true;
                prevCol = node[0];
            }

            res.get(res.size() - 1).add(node[2]);
        }

        return res;
    }

    private void dfs(TreeNode node, int row, int col, List<int[]> nodes) {
        if (node == null) {
            return;
        }

        nodes.add(new int[] {
            col,
            row,
            node.val
        });

        dfs(node.left, row + 1, col - 1, nodes);
        dfs(node.right, row + 1, col + 1, nodes);
    }
}
import "sort"

func verticalTraversal(root *TreeNode) [][]int {
    type item struct {
        col int
        row int
        val int
    }

    items := make([]item, 0)

    var dfs func(node *TreeNode, row, col int)
    dfs = func(node *TreeNode, row, col int) {
        if node == nil {
            return
        }

        items = append(items, item{col: col, row: row, val: node.Val})

        dfs(node.Left, row+1, col-1)
        dfs(node.Right, row+1, col+1)
    }

    dfs(root, 0, 0)

    // 按列、行、值依次比较,值只决定同行同列的顺序。
    sort.Slice(items, func(i, j int) bool {
        if items[i].col != items[j].col {
            return items[i].col < items[j].col
        }
        if items[i].row != items[j].row {
            return items[i].row < items[j].row
        }
        return items[i].val < items[j].val
    })

    res := make([][]int, 0)
    prevCol := 0
    hasCol := false
    for _, it := range items {
        // 首个节点或列变化时,开启一组结果。
        if !hasCol || it.col != prevCol {
            res = append(res, []int{})
            hasCol = true
            prevCol = it.col
        }

        res[len(res)-1] = append(res[len(res)-1], it.val)
    }

    return res
}

复杂度分析

  • 时间复杂度:$O(n\log(n + 1))$。采集和分组各需 $O(n)$,对 n 个三元组排序是主要开销。
  • 空间复杂度:$O(n)$,用于节点记录集合、排序辅助空间和深度不超过 n 的递归栈,返回结果也为线性大小。

关键点总结

[!green]

  • 比较顺序严格是列、行、值,值仅用于坐标完全相同的节点。
  • 采集顺序与输出顺序分开,DFS 或 BFS 都可以负责采集,最终排序规则不能省略。
  • 同列记录排序后连续出现,因此只需线性扫描即可分组。

易错点总结

[!yellow]

  • 只按列排序,会把同列的上下层次打乱。
  • 同列全部按值排序,会把深层的小值放到浅层节点之前,违反行优先规则。
  • 依赖 BFS 的先后顺序处理同行同列节点,可能不满足值的升序要求。
  • 将左右孩子的列变化写反,会使输出左右列整体颠倒。
  • 不单独处理首个分组,而把默认列号当成已有列,可能在结果还为空时就尝试追加。

相似题目

题目 难度 关联与区别
314. 二叉树的垂直遍历 中等 列坐标相同,但本题同一行同一列按值排序,原题保留层序到达的先后。
102. 二叉树的层序遍历 中等 BFS可得到节点深度,本题还需记录列坐标并处理坐标相同的值排序。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2025/34883774
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!