目录

题目描述

314. 二叉树的垂直遍历

题意分析

把二叉树画在平面上,根节点占一列,往左走一步就落到左边相邻的一列,往右走一步就落到右边相邻的一列。要求按列从最左到最右输出,每一列内部的节点从上到下排列。

关键在于「列」这个坐标不是树本身自带的属性,需要我们在遍历时自己带下去:根记 0,左孩子记 col - 1,右孩子记 col + 1。同一列可能来自树中完全不相邻的两棵子树,所以必须有一个以列号为键的容器把它们收集到一起。

题面对同列内部的顺序有明确规定:先按深度从上到下,深度相同时按从左到右。这句话是整道题的算法信号——它描述的正是层序扩散的顺序,意味着只要遍历顺序本身满足「先浅后深、同深先左」,把节点值直接追加到所属列的尾部就自动是对的,不需要任何排序。反过来,如果遍历顺序不满足这个性质,就得额外记录深度并做稳定排序,代码会复杂得多。

还有一个容易被忽略的信号:列号可正可负,不能直接当数组下标用,要么用哈希表,要么先做偏移。输出时列号必须连续覆盖从最小到最大的范围,而不是按插入顺序或哈希顺序。

边界:根为空时返回空列表;只有一条左链的退化树会让最小列号一路变负;节点值本身允许为负,不能拿值当哨兵。

解法:BFS 按列分组

核心思路

先看暴力想法:先跑一遍 DFS,把每个节点记成三元组 (列号, 深度, 值) 存进一个大列表,最后按「列号升序、深度升序」排序再分组输出。这个做法是对的,但排序带来 $O(n \log n)$,而且必须显式记录深度、必须保证排序稳定否则同深度节点的左右顺序会乱。瓶颈就在这里:我们为一个本来可以「顺手生成」的顺序付出了排序代价。

观察题面对顺序的要求——同列内先按深度、深度相同按从左到右——这恰好就是 BFS 出队的顺序。BFS 按层推进保证了浅的节点先出队,同一层内如果入队时坚持「先左孩子后右孩子」,出队顺序也就自左向右。于是深度这个维度根本不需要记录,更不需要排序。

由此得到这个解法的不变量:在 BFS 的任意时刻,对每个列号 ccolToValues[c] 中已有的节点恰好是所有已出队且列号为 c 的节点,且它们的排列顺序就是题目要求的最终顺序。换句话说,每次出队时直接 append 到对应列的尾部,这个列表从头到尾都不需要回头调整。

剩下的就是「怎么把列号带下去」。BFS 队列里存的不能只是节点,还得带上它的列号,所以用两个平行队列(Java)或一个 {节点, 列号} 结构体队列(Go)。同时在出队时顺手维护 minColmaxCol,最后从 minColmaxCol 逐列取出即可。这里有个隐含但重要的性质:从根(列 0)走到任意节点,列号每步只变化 1,所以 [minCol, maxCol] 之间的每个整数都必然被某个节点占据,遍历区间时不会取到空列。

解题步骤

  • 根为空直接返回空列表。为什么:后续逻辑无条件地把根入队,若不拦截会对空指针取 val。这也是唯一需要的特判。
  • 准备哈希表 colToValues、节点队列、列号队列,把根与列号 0 一起入队。为什么用哈希表而不是数组:列号可以是负数,且范围事先未知,哈希表免去了估算偏移量的麻烦。
  • 循环出队,取出节点与它配对的列号。为什么两个队列要同步 poll:它们是一一对应的平行结构,任何一次只弹其中之一都会让后续所有配对错位。
  • 把节点值追加到 colToValues[col] 的尾部,并用 col 更新 minCol / maxCol。为什么可以直接追加不排序:由上面的不变量,BFS 出队顺序已经等于题目要求的顺序。
  • 左孩子存在则以 col - 1 入队,右孩子存在则以 col + 1 入队,且必须先左后右。为什么顺序不能反:同一层同一列若同时有两个节点,入队顺序决定了它们的出队顺序,写反会让左右颠倒。
  • BFS 结束后从 minCol 遍历到 maxCol,依次把每列的列表加入结果。为什么不能直接遍历哈希表:哈希表的迭代顺序与列号大小无关,结果的列顺序会是乱的。

具体用例 root = [3,9,20,null,null,15,7] 走一遍。这棵树是根 3,左孩子 9,右孩子 20,20 的左右孩子分别是 15 和 7。

初始:队列 [(3, col=0)]minCol = maxCol = 0,哈希表为空。
第一次出队 (3, 0)colToValues[0] = [3]minCol = maxCol = 0。左孩子 9 以列 -1 入队,右孩子 20 以列 1 入队,队列变为 [(9,-1), (20,1)]
第二次出队 (9, -1)colToValues[-1] = [9]minCol 更新为 -1。9 无孩子。
第三次出队 (20, 1)colToValues[1] = [20]maxCol 更新为 1。左孩子 15 以列 1 - 1 = 0 入队,右孩子 7 以列 1 + 1 = 2 入队,队列变为 [(15,0), (7,2)]
第四次出队 (15, 0):列 0 已存在列表 [3],追加后变为 [3, 15]。注意 15 比 3 深,正因为 BFS 保证浅节点先出队,追加到尾部就是正确的上下顺序。
第五次出队 (7, 2)colToValues[2] = [7]maxCol 更新为 2。队列空,循环结束。
收尾:从 -1 遍历到 2,依次取出 [9][3,15][20][7],最终答案 [[9],[3,15],[20],[7]]

代码实现

class Solution {
    // 题目要求同一列从上到下输出。
    public List<List<Integer>> verticalOrder(TreeNode root) {
        List<List<Integer>> res = new ArrayList<>();
        if (root == null) {
            return res;
        }

        Map<Integer, List<Integer>> colToValues = new HashMap<>();
        Queue<TreeNode> nodeQueue = new ArrayDeque<>();
        Queue<Integer> colQueue = new ArrayDeque<>();
        nodeQueue.offer(root);
        colQueue.offer(0);

        int minCol = 0;
        int maxCol = 0;
        while (!nodeQueue.isEmpty()) {
            TreeNode node = nodeQueue.poll();
            int col = colQueue.poll();

            colToValues.computeIfAbsent(col, key -> new ArrayList<>()).add(node.val);
            minCol = Math.min(minCol, col);
            maxCol = Math.max(maxCol, col);

            if (node.left != null) {
                nodeQueue.offer(node.left);
                colQueue.offer(col - 1);
            }
            if (node.right != null) {
                nodeQueue.offer(node.right);
                colQueue.offer(col + 1);
            }
        }

        for (int col = minCol; col <= maxCol; col++) {
            res.add(colToValues.get(col));
        }
        return res;
    }
}
func verticalOrder(root *TreeNode) [][]int {
    // 题目要求同一列从上到下输出。
    res := make([][]int, 0)
    if root == nil {
        return res
    }

    type item struct {
        node *TreeNode
        col  int
    }

    colToValues := make(map[int][]int)
    queue := make([]item, 0)
    queue = append(queue, item{node: root, col: 0})
    minCol, maxCol := 0, 0

    for head := 0; head < len(queue); head++ {
        cur := queue[head]
        node := cur.node
        col := cur.col

        colToValues[col] = append(colToValues[col], node.Val)
        if col < minCol {
            minCol = col
        }
        if col > maxCol {
            maxCol = col
        }

        if node.Left != nil {
            queue = append(queue, item{node: node.Left, col: col - 1})
        }
        if node.Right != nil {
            queue = append(queue, item{node: node.Right, col: col + 1})
        }
    }

    for col := minCol; col <= maxCol; col++ {
        res = append(res, colToValues[col])
    }
    return res
}

复杂度分析

  • 时间复杂度:$O(n)$。每个节点恰好入队一次、出队一次,出队时做的是一次哈希表追加与两次比较,均摊常数;末尾按列号收集的循环长度等于列数,不超过 $n$。凭什么不是 $O(n \log n)$:因为顺序由 BFS 直接产出,全程没有排序。
  • 空间复杂度:$O(n)$。哈希表总共存放 $n$ 个节点值,队列在最宽一层的规模上界也是 $O(n)$(完全二叉树时最后一层约 $n/2$ 个节点同时在队)。凭什么不能更小:答案本身就要输出全部 $n$ 个值。

关键点总结

  • 让遍历顺序替你完成排序。当题面对输出顺序的描述恰好等于某种遍历的天然顺序时,选对遍历方式就能省掉一次排序和一个维度的状态,这是树与图题里最值钱的一类观察。
  • 坐标要随节点一起在队列中传递。凡是「节点自身不携带、但由路径决定」的信息(列号、深度、路径和),都应作为队列元素的一部分,而不是事后重算。
  • 可负的键用哈希表,输出时再按序遍历键的区间。不要指望哈希表的迭代顺序,最终顺序必须由显式的 minCol → maxCol 循环给出。
  • 先左后右的入队顺序是语义的一部分,不是风格问题;它承载了「同层同列时左边优先」这条规则。
  • 面试视角:面试官问到这题,通常会紧接着追问「如果同列同层还要按节点值从小到大排呢」。这正是 987. 二叉树的垂序遍历 的加强要求,此时 BFS 的天然顺序不再够用,必须退回到「收集三元组 + 自定义比较器排序」的方案。能主动说清「314 靠遍历顺序、987 必须排序」这条分界线,比写出代码更能体现理解深度。

易错点总结

  • 错误写法:最后直接 for (List<Integer> v : colToValues.values()) res.add(v) → 用例 [3,9,20,null,null,15,7]HashMap 的迭代顺序按哈希值而非列号,可能输出 [[3,15],[9],[20],[7]],列的左右次序完全错乱。
  • 错误写法:把列号当数组下标写成 res[col] → 用例 [1,2,null,3] 这条左斜链会产生列号 -1、-2,负下标直接数组越界异常。
  • 错误写法:入队时先右后左 → 用例 [1,2,3,4,5,6,7],节点 5 与 6 同层同列 0,先右后左会让输出的列 0 变成 [1,6,5] 而不是正确的 [1,5,6]
  • 错误写法:只弹节点队列忘了同步弹列号队列 → 任意含两层以上的树,第二次出队起节点与列号就错位,所有节点被归到错误的列。
  • 错误写法:改用 DFS 前序递归并直接追加到列表 → 用例 [3,9,8,4,0,1,7],DFS 会先把左子树整条深路径写完再回头处理右子树,同一列里深层节点排到了浅层节点前面,上下顺序错误。
  • 错误写法:minCol / maxCol 初始化为 Integer.MAX_VALUEInteger.MIN_VALUE 却在根为空时跳过更新 → 空树若没有提前返回,末尾循环会从 MAX_VALUE 跑到 MIN_VALUE,虽然不进循环但把 minCol 参与运算的写法极易溢出。
  • 错误写法:用节点值 0 或 -1 当作「无孩子」的标记 → 用例 [0,-1,null],题目允许节点值取 0 和负数,用值判空会漏掉真实存在的孩子。
  • 错误写法:末尾用 colToValues.get(col) 却对区间内某列做了非空判断后 continue → 逻辑上多余,且一旦误写成跳过就会让结果少一列;实际上 [minCol, maxCol] 中每列必然非空,因为列号沿根到节点的路径每步只变动 1。

相似题目

题目 难度 考察点
987. 二叉树的垂序遍历 困难 同列同深度还要按节点值升序,BFS 的天然顺序失效,必须收集三元组后自定义排序
102. 二叉树的层序遍历 中等 只按行分组不按列,队列里不用带坐标,改为按层批量出队
103. 二叉树的锯齿形层序遍历 中等 在层序基础上按奇偶层反向拼接,考的是输出顺序的后处理
199. 二叉树的右视图 中等 只取每层最后一个节点,需要显式的层边界而非列号
662. 二叉树最大宽度 中等 带下去的是完全二叉树编号而非列号,重点是防止编号溢出
515. 在每个树行中找最大值 中等 层内做聚合取最大值,不保留层内全部元素
513. 找树左下角的值 中等 反过来先右后左入队,最后一个出队的即为答案,是入队顺序的另一种用法