题目描述

✅ 156. 上下翻转二叉树

题意分析

每个右孩子要么为空,要么是有左兄弟的叶子。要求沿最左路径翻转父子关系:对于原来的 parent(left, right),让 left 上升为父节点,right 成为它的新左孩子,原来的 parent 成为它的新右孩子。

因此新根一定是原树最左端的节点。右孩子只需随所在层搬移,不需要继续递归处理:题目已经保证它没有子树。若一个节点没有左孩子,由于右孩子必须有左兄弟,它也不可能有右孩子。

本题并非对所有节点交换左右孩子。翻转会改变谁是谁的父节点,而且旧的父子边必须删除,否则新边和旧边可能组成环。

解法一:递归翻转最左链

核心思路

[!blue]

设当前根为 root,原左孩子为 left。先递归翻转 left 的子树,得到新根 newRoot。递归完成后,left 已成为这棵新树最右端的节点,它的左右指针均已清空,正好可以接收当前层的两个节点。

接下来执行 left.left = root.right、left.right = root,把原右兄弟与原父亲接到 left 下方。最后清空 root 的两条旧边;这样当前层原根也成为新树最右端的叶子节点,满足上一层继续接边的条件。

这也给出了递归正确性的依据:最左叶子本身无需翻转;若左子树已按规则翻转,当前层只需补上这两条新边并删去旧边,就能得到整棵子树的正确结果。newRoot 始终是最左叶子,回溯时不能改成当前节点。

root == null 时返回空树;root.left == null 时返回当前叶子。其余节点一定能继续沿左链递归。

解题步骤

  1. 若当前节点为空或没有左孩子,直接返回它。
  2. 递归处理 root.left,保存返回的新根 newRoot。
  3. 通过仍指向原左孩子的 root.left,将原父节点接到它的右侧,将原右孩子接到它的左侧。
  4. 清空 root.left 和 root.right,断开原来的两条边。
  5. 返回 newRoot,让所有递归层传回同一个根。

以原树 [1,2,3,4,5] 为例,回溯到节点 2 时先建立 4.left = 5、4.right = 2,并清空节点 2 的旧孩子;回溯到节点 1 时再建立 2.left = 3、2.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$ 为最左路径的节点数。只沿这条路径递归,每层进行常数次指针修改;最坏为 $O(n)$。
  • 空间复杂度:$O(h)$,来自递归调用栈,最坏为 $O(n)$。

关键点总结

[!green]

  • 递归返回值保存新根,root.left 保存当前层重新接边的支点,两者作用不同。
  • 必须先完成左子树翻转,再接上当前层;否则会提前破坏向下递归的路径。
  • 当前层完成后,原根的两个孩子都为空,这既防止成环,也为上一层接边留出了位置。

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

核心思路

[!blue]

也可以从原根沿左链向下走,在下降过程中直接翻转已访问部分。循环开始时维护三个状态:cur 是本轮待处理节点,parent 是已翻转部分的根,parentRight 是原父节点的右孩子。

按题目的翻转规则,cur 的新左孩子应为 parentRight,新右孩子应为 parent。但修改前还要保存两个旧值:原左孩子用于下一轮,原右孩子用于下一轮的新左孩子。

因此先保存 next = cur.left,再改写左边;接着保存原右孩子到 parentRight,最后改写右边。每轮结束令 parent = cur、cur = next,已经翻转的部分便向下扩展一层。

初始的 parent、parentRight 均为空,所以原根在第一轮自然清空两条旧边。沿左链走到空节点时,最后处理的最左叶子保存在 parent 中,它就是新根。

解题步骤

  1. 初始化 cur = root,parent 与 parentRight 为空。
  2. 保存原左孩子 next,将 cur.left 指向上一层保存的右孩子。
  3. 保存 cur 的原右孩子,供下一轮使用,再将 cur.right 指向 parent。
  4. 把当前节点作为新的 parent,沿 next 继续向下。
  5. cur 为空时返回 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)$,只保存常数个节点指针。

关键点总结

[!green]

  • parentRight 属于上一层,不能误用当前节点的右孩子来设置当前节点的新左边。
  • 每次覆盖指针前都要先保存后面仍会使用的旧值。
  • 迭代法与递归法实现同一组局部连接,但通过保存状态将额外空间降为常数。

易错点总结

[!yellow]

  • 把新孩子接反:原右兄弟放在新左侧,原父亲放在新右侧。
  • 递归后返回 root:它已成为末端节点,返回值必须是最左叶子对应的 newRoot。
  • 递归法保留旧边:原父亲和原左孩子会互相指向,形成环。
  • 迭代法覆盖左指针后才找下一层:此时原左链入口已丢失,必须提前保存 next。
  • 忽略树形约束:本解法依赖“非空右孩子是有左兄弟的叶子”,不能直接用于任意二叉树。

相似题目

题目 难度 关联与区别
226. 翻转二叉树 简单 原题只交换左右孩子,本题改变父子关系,让最左节点成为新根。
206. 反转链表 简单 同样先保存下一节点再反向接边,本题沿左链操作并额外搬移右兄弟。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/45278802
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!