目录

题目描述

156. 上下翻转二叉树

题意分析

给定一棵满足特殊约束的二叉树:每个右孩子要么为空,要么是一个拥有左兄弟的叶子节点。要求沿着整棵树的最左路径逐层翻转,使原来的最左叶子成为新根。

对任意局部结构 parent(left, right),翻转后原来的 left 上升:right 变成它的新左孩子,parent 变成它的新右孩子。也就是把

parent.left = left, parent.right = right

改成

left.left = right, left.right = parent

题目的树形约束非常关键:右孩子没有自己的子树,因此把它搬到左孩子下面不会丢失后续结构;若允许右孩子继续向下生长,这套局部旋转就不足以覆盖所有节点。

[1,2,3,4,5] 为例,原树的最左链是 1 → 2 → 4。翻转后 4 成为新根,4.left = 54.right = 2,再令 2.left = 32.right = 1,结果为 [4,5,2,null,null,3,1]

解法一:递归翻转最左链

核心思路

递归函数的返回值始终表示:以当前节点为根的子树翻转完成后,整棵新树的根节点。这个返回值一旦在最左叶子处确定,回溯过程中必须原样向上传递。

先递归翻转 root.left。递归返回时,原来的左孩子 root.left 已经位于新树最右侧,正好可以接收两个新孩子:把原右孩子挂到它的左边,把原根挂到它的右边。

完成新连接后,必须把 root.leftroot.right 同时置空。原根已经被挂到新位置,旧边若不清除,会让同一个节点拥有多条父边,甚至形成环。

递归基是 root == null || root.left == null。空树直接返回空;没有左孩子时,当前节点就是最左端,也就是翻转后的新根。题目约束保证此时不会存在一个孤立的右子树需要继续处理。

解题步骤

  • 向左递归到底:调用 upsideDownBinaryTree(root.left),获得最终的新根 newRoot
  • 建立新左边:执行 root.left.left = root.right,把原来的右兄弟挂到原左孩子的左侧。
  • 建立新右边:执行 root.left.right = root,把原父节点挂到原左孩子的右侧。
  • 断开旧边:将 root.leftroot.right 置空。
  • 返回同一个新根:所有递归层都返回 newRoot,不能返回当前的 root

[1,2,3,4,5] 走一遍:递归先到节点 4 并返回 4;回到节点 2 时执行 4.left = 54.right = 2,再清空节点 2 的旧孩子;回到节点 1 时,此刻原左孩子节点 2 已是新树最右端,执行 2.left = 32.right = 1。最终各层都返回节点 4。

代码实现

class Solution {
    public TreeNode upsideDownBinaryTree(TreeNode root) {
        if (root == null || root.left == null) {
            return root;
        }

        TreeNode newRoot = upsideDownBinaryTree(root.left);

        // 原左孩子上升:原右孩子放左边,原父节点放右边。
        root.left.right = root;
        root.left.left = root.right;

        // 原根已经移动到新位置,必须断开旧边。
        root.left = null;
        root.right = null;
        return newRoot;
    }
}
func upsideDownBinaryTree(root *TreeNode) *TreeNode {
    if root == nil || root.Left == nil {
        return root
    }

    newRoot := upsideDownBinaryTree(root.Left)

    // 原左孩子上升:原右孩子放左边,原父节点放右边。
    root.Left.Right = root
    root.Left.Left = root.Right

    // 原根已经移动到新位置,必须断开旧边。
    root.Left = nil
    root.Right = nil
    return newRoot
}

复杂度分析

  • 时间复杂度:$O(h)$,其中 $h$ 是最左路径长度,每层只访问和改写一次;在最坏的退化树中 $h = n$,因此也可写成 $O(n)$。
  • 空间复杂度:$O(h)$,来自递归调用栈;最坏为 $O(n)$。

关键点总结

  • 递归返回值是整棵翻转后子树的新根,不是当前层的新父节点。
  • 必须先递归再改边;下降阶段仍需要完整的原始左链。
  • root.left 在回溯时仍指向原左孩子,而该节点恰好是已翻转子树的最右端,因此可以直接作为重新接边的支点。
  • 两条旧边都要清空,树的指针改写题必须同时考虑“建立新边”和“删除旧边”。

解法二:迭代维护父节点与原右孩子

核心思路

递归回溯实际只依赖三份信息:当前节点 cur、已经翻转好的父节点 parent、以及上一层暂存的原右孩子 parentRight。把这三份状态显式保存,就能沿最左链从上到下原地翻转,省掉递归栈。

循环不变量是:parent 是已经翻转部分的根,parentRight 应成为 cur 的新左孩子,而 cur.left 仍指向下一轮要处理的节点。每轮必须先保存 next = cur.left,再覆盖 cur.left

解题步骤

  • 初始化 cur = rootparent = nullparentRight = null
  • 保存下一层原左孩子 next = cur.left
  • cur.left = parentRight,接上上一层保存的原右孩子。
  • 先保存本层原右孩子到 parentRight,再令 cur.right = parent
  • 推进 parent = curcur = next;循环结束时 parent 就是新根。

代码实现

class Solution {
    public TreeNode upsideDownBinaryTree(TreeNode root) {
        TreeNode cur = root;
        TreeNode parent = null;
        TreeNode parentRight = null;

        while (cur != null) {
            TreeNode next = cur.left;
            cur.left = parentRight;
            parentRight = cur.right;
            cur.right = parent;

            parent = cur;
            cur = next;
        }
        return parent;
    }
}
func upsideDownBinaryTree(root *TreeNode) *TreeNode {
    cur := root
    var parent, parentRight *TreeNode

    for cur != nil {
        next := cur.Left
        cur.Left = parentRight
        parentRight = cur.Right
        cur.Right = parent

        parent = cur
        cur = next
    }
    return parent
}

复杂度分析

  • 时间复杂度:$O(h)$,沿最左路径处理每层一次,最坏为 $O(n)$。
  • 空间复杂度:$O(1)$,只维护常数个节点指针。

关键点总结

  • next 必须在覆盖 cur.left 之前保存,否则剩余左链会丢失。
  • parentRight 的含义是“上一层原来的右孩子”,它在当前层成为新左孩子。
  • 四个赋值的顺序就是迭代法的核心:保存左链 → 接新左边 → 保存原右边 → 接新右边。

解法对比

解法 时间复杂度 额外空间 面试取舍
递归 $O(h)$ $O(h)$ 结构与题意最贴合,容易证明,推荐先讲
迭代 $O(h)$ $O(1)$ 空间更优,但三个指针的语义和赋值顺序更容易写错

易错点总结

  • 把新左右孩子接反:样例中节点 4 的新左孩子应是原右兄弟 5,新右孩子才是原父节点 2;写反会得到另一棵树。
  • 递归前就修改 root.left:会丢掉通往最左叶子的入口,[1,2,3,4,5] 无法继续访问节点 4。
  • 忘记清空原根的孩子:节点 1、2 仍保留旧指针,新旧边同时存在,结果不再是一棵合法树。
  • 返回当前 root:样例最终会返回节点 1,而正确的新根是节点 4。递归各层必须返回同一个 newRoot
  • 把本题当成 226 的左右子树交换:本题改变父子关系并让最左叶子上升,不是对每个节点交换左右孩子。
  • 忽略输入约束:若出现“只有右孩子且右孩子还有子树”的普通二叉树,这套算法并不保证得到题目定义的翻转结果;面试时应主动说明算法依赖题设结构。

相似题目

题目 难度 考察点
206. 反转链表 简单 同样需要先保存后继再原地反转指针,是迭代写法的直接类比
114. 二叉树展开为链表 中等 原地改写树指针,同时必须避免丢失尚未处理的子树
226. 翻转二叉树 简单 只交换每个节点的左右孩子,可对照本题为何会改变父子关系
430. 扁平化多级双向链表 中等 多指针结构重连,重点同样是保存入口并清除旧边