Files
interview/16-LeetCode Hot 100/翻转二叉树.md
yasinshaw 5c1c974e88 docs: 改进LeetCode二叉树题目解题思路
按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度:

1. 二叉树的中序遍历
   - 增加"思路推导"部分,解释递归到迭代的转换
   - 详细说明迭代法的每个步骤
   - 增加执行过程演示和多种解法

2. 二叉树的最大深度
   - 增加"思路推导",对比DFS和BFS
   - 详细解释递归的基准情况
   - 增加多种解法和变体问题

3. 从前序与中序遍历序列构造二叉树
   - 详细解释前序和中序的特点
   - 增加"思路推导",说明如何分治
   - 详细说明切片边界计算

4. 对称二叉树
   - 解释镜像对称的定义
   - 详细说明递归比较的逻辑
   - 增加迭代解法和变体问题

5. 翻转二叉树
   - 解释翻转的定义和过程
   - 详细说明多值赋值的执行顺序
   - 增加多种解法和有趣的故事

6. 路径总和
   - 详细解释路径和叶子节点的定义
   - 说明为什么使用递减而非累加
   - 增加多种解法和变体问题

每个文件都包含:
- 完整的示例和边界条件分析
- 详细的算法流程和图解
- 关键细节说明
- 常见错误分析
- 复杂度分析(详细版)
- 执行过程演示
- 多种解法
- 变体问题
- 总结

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 21:33:57 +08:00

12 KiB
Raw Blame History

翻转二叉树 (Invert Binary Tree)

LeetCode 226. 简单

题目描述

给你一棵二叉树的根节点 root,翻转这棵二叉树,并返回其根节点。

示例 1:

输入root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]

示例 2:

输入root = [2,1,3]
输出:[2,3,1]

示例 3:

输入root = []
输出:[]

思路推导

什么是翻转二叉树?

翻转定义: 交换每个节点的左右子节点

翻转前:            翻转后:
      4               4
     / \             / \
    2   7           7   2
   / \ / \         / \ / \
  1  3 6  9       9  6 3  1

暴力解法分析

思路: 从根节点开始,递归交换每个节点的左右子节点

观察翻转的性质:

  1. 翻转整棵树 = 翻转左子树 + 翻转右子树 + 交换左右子节点
  2. 这是一个天然的递归问题

递归思路:

invertTree(root):
  1. 翻转左子树
  2. 翻转右子树
  3. 交换左右子节点

图解:

原始树:
      4
     / \
    2   7
   / \ / \
  1  3 6  9

步骤1: 翻转子节点(2和7)
├─ 翻转2的子树
│  ├─ 翻转1 → 1叶子节点不变
│  └─ 翻转3 → 3叶子节点不变
│  └─ 交换1和3 → 2的子树变为 [3, 1]
└─ 翻转7的子树
   ├─ 翻转6 → 6叶子节点不变
   └─ 翻转9 → 9叶子节点不变
   └─ 交换6和9 → 7的子树变为 [9, 6]

步骤2: 交换2和7
最终结果:
      4
     / \
    7   2
   / \ / \
  9  6 3  1

时间复杂度: O(n) - 每个节点访问一次 空间复杂度: O(h) - h为树高递归栈空间

为什么这样思考?

核心思想:

  1. 分治: 大问题分解为小问题(翻转整棵树 → 翻转子树)
  2. 递归定义: 翻转操作可以递归应用到子树上
  3. 原地操作: 直接交换左右指针,不需要额外空间

为什么先翻转再交换?

  • 也可以先交换再翻转
  • 两种顺序都可以,结果相同
  • 但先翻转再交换更直观

解题思路

核心思想

从根节点开始,递归翻转左右子树,然后交换左右子节点。

详细算法流程

步骤1: 处理基准情况

if root == nil {
    return nil  // 空树直接返回
}

关键点: 递归的终止条件

步骤2: 递归翻转左右子树

left := invertTree(root.Left)   // 翻转左子树
right := invertTree(root.Right) // 翻转右子树

关键点: 先递归处理子节点,再处理当前节点

步骤3: 交换左右子节点

root.Left = right
root.Right = left

简化写法:

root.Left, root.Right = root.Right, root.Left

步骤4: 返回当前节点

return root

图解:

翻转过程:

    4              4              4
   / \            / \            / \
  2   7    →    2   7    →    7   2
 / \ / \        / \ / \        / \ / \
1  3 6  9      3  1 9  6     9  6 3  1

   原始          翻转子节点      交换子节点

关键细节说明

细节1: 为什么可以一行代码完成?

root.Left, root.Right = invertTree(root.Right), invertTree(root.Left)

原因: Go支持多值赋值右侧的表达式先求值再赋值给左侧

执行顺序:

  1. 计算 invertTree(root.Right),得到翻转后的右子树
  2. 计算 invertTree(root.Left),得到翻转后的左子树
  3. 同时赋值:root.Left = 翻转后的右子树root.Right = 翻转后的左子树

细节2: 为什么不需要返回值?

实际上需要返回值!

func invertTree(root *TreeNode) *TreeNode {
    if root == nil {
        return nil  // 必须返回
    }
    // ...
    return root  // 必须返回
}

原因:

  • 需要返回翻转后的根节点
  • 即使是原地操作,也需要返回值以保持接口一致

细节3: 为什么是原地操作?

// 直接修改指针,不需要创建新节点
root.Left, root.Right = root.Right, root.Left

原因:

  • 翻转操作只改变指针指向
  • 不需要创建新的节点
  • 空间复杂度只有递归栈

边界条件分析

边界1: 空树

输入: root = nil
输出: nil
处理: 直接返回nil

边界2: 只有根节点

输入: root = [1]
输出: [1]
处理:
- 翻转左子树 → nil
- 翻转右子树 → nil
- 交换nil和nil → 不变

边界3: 只有左子树

输入:
  1
 /
2

输出:
  1
   \
    2

处理:
- 翻转2 → 2叶子节点
- 翻转nil → nil
- 交换 → 2变成右子树

边界4: 只有右子树

输入:
1
 \
  2

输出:
  1
 /
2

处理:
- 翻转nil → nil
- 翻转2 → 2叶子节点
- 交换 → 2变成左子树

边界5: 完全二叉树

输入:
    1
   / \
  2   3
 / \ / \
4  5 6  7

输出:
    1
   / \
  3   2
 / \ / \
7  6 5  4

复杂度分析(详细版)

时间复杂度:

- 每个节点访问一次: O(n)
- 每次访问常数操作: O(1)
- 总计: O(n)

为什么是O(n)?
- 递归遍历所有节点
- 每个节点只处理一次(交换左右)
- 没有重复访问

空间复杂度:

- 递归栈: O(h) - h为树高
  - 最坏情况(链状树): O(n)
  - 最好情况(完全平衡树): O(log n)
- 总计: O(h)

为什么是O(h)而不是O(n)?
- 递归深度 = 树的高度
- 同一时刻栈中最多有h个栈帧
- 不是所有节点同时在栈中

执行过程演示

输入:

    4
   / \
  2   7
 / \
1   3

执行过程:

调用 invertTree(4):
├─ 调用 invertTree(2):
│  ├─ 调用 invertTree(1):
│  │  ├─ 1.Left = nil, 1.Right = nil
│  │  └─ 返回 1
│  ├─ 调用 invertTree(3):
│  │  ├─ 3.Left = nil, 3.Right = nil
│  │  └─ 返回 3
│  ├─ 交换: 2.Left = 3, 2.Right = 1
│  └─ 返回 2
├─ 调用 invertTree(7):
│  ├─ 7.Left = nil, 7.Right = nil
│  └─ 返回 7
├─ 交换: 4.Left = 7, 4.Right = 2
└─ 返回 4

最终结果:
    4
   / \
  7   2
     / \
    3   1

代码实现

方法一:递归(推荐)

func invertTree(root *TreeNode) *TreeNode {
    if root == nil {
        return nil
    }

    // 递归翻转左右子树并交换
    root.Left, root.Right = invertTree(root.Right), invertTree(root.Left)

    return root
}

复杂度: O(n) 时间O(h) 空间

方法二:递归(分步写法)

func invertTree(root *TreeNode) *TreeNode {
    if root == nil {
        return nil
    }

    // 先翻转左右子树
    left := invertTree(root.Left)
    right := invertTree(root.Right)

    // 再交换
    root.Left = right
    root.Right = left

    return root
}

复杂度: O(n) 时间O(h) 空间

方法三:迭代(栈)

func invertTree(root *TreeNode) *TreeNode {
    if root == nil {
        return nil
    }

    stack := []*TreeNode{root}

    for len(stack) > 0 {
        node := stack[len(stack)-1]
        stack = stack[:len(stack)-1]

        // 交换左右子节点
        node.Left, node.Right = node.Right, node.Left

        // 将子节点入栈
        if node.Left != nil {
            stack = append(stack, node.Left)
        }
        if node.Right != nil {
            stack = append(stack, node.Right)
        }
    }

    return root
}

复杂度: O(n) 时间O(n) 空间

方法四:迭代(队列 - BFS

func invertTree(root *TreeNode) *TreeNode {
    if root == nil {
        return nil
    }

    queue := []*TreeNode{root}

    for len(queue) > 0 {
        node := queue[0]
        queue = queue[1:]

        // 交换左右子节点
        node.Left, node.Right = node.Right, node.Left

        // 将子节点入队
        if node.Left != nil {
            queue = append(queue, node.Left)
        }
        if node.Right != nil {
            queue = append(queue, node.Right)
        }
    }

    return root
}

复杂度: O(n) 时间O(n) 空间

常见错误

错误1: 忘记返回节点

错误写法:

func invertTree(root *TreeNode) *TreeNode {
    if root == nil {
        return nil
    }
    root.Left, root.Right = invertTree(root.Right), invertTree(root.Left)
    // 忘记返回 root
}

正确写法:

func invertTree(root *TreeNode) *TreeNode {
    if root == nil {
        return nil
    }
    root.Left, root.Right = invertTree(root.Right), invertTree(root.Left)
    return root  // 必须返回
}

原因: 函数签名要求返回 *TreeNode

错误2: 交换后忘记处理子节点

错误写法:

func invertTree(root *TreeNode) *TreeNode {
    if root == nil {
        return nil
    }
    root.Left, root.Right = root.Right, root.Left
    return root  // 错误:只交换了当前节点
}

正确写法:

func invertTree(root *TreeNode) *TreeNode {
    if root == nil {
        return nil
    }
    root.Left, root.Right = invertTree(root.Right), invertTree(root.Left)
    return root  // 递归处理子节点
}

原因: 需要递归翻转所有子节点,不只是当前节点

错误3: 多值赋值理解错误

错误理解:

// 错误理解:先赋值 root.Left再赋值 root.Right
root.Left, root.Right = root.Right, root.Left

正确理解:

// Go的多值赋值是同时的
// 右侧先求值,再同时赋值给左侧
temp1, temp2 := root.Right, root.Left  // 先求值
root.Left = temp1                       // 再赋值
root.Right = temp2

变体问题

变体1: 判断是否是翻转二叉树

判断两棵树是否互为翻转:

func isFlipTree(a, b *TreeNode) bool {
    if a == nil && b == nil {
        return true
    }
    if a == nil || b == nil {
        return false
    }
    if a.Val != b.Val {
        return false
    }
    // a的左等于b的右a的右等于b的左
    return isFlipTree(a.Left, b.Right) &&
           isFlipTree(a.Right, b.Left)
}

变体2: 翻转N叉树

type Node struct {
    Val      int
    Children []*Node
}

func invertNTree(root *Node) *Node {
    if root == nil {
        return nil
    }

    // 翻转子节点列表
    for i, j := 0, len(root.Children)-1; i < j; i, j = i+1, j-1 {
        root.Children[i], root.Children[j] = root.Children[j], root.Children[i]
    }

    // 递归翻转每个子节点
    for _, child := range root.Children {
        invertNTree(child)
    }

    return root
}

变体3: 镜像对称二叉树

实际上就是翻转二叉树的应用:

func isSymmetric(root *TreeNode) bool {
    if root == nil {
        return true
    }
    return isMirror(root.Left, root.Right)
}

func isMirror(left, right *TreeNode) bool {
    if left == nil && right == nil {
        return true
    }
    if left == nil || right == nil {
        return false
    }
    if left.Val != right.Val {
        return false
    }
    return isMirror(left.Left, invertTree(right.Right)) &&
           isMirror(left.Right, invertTree(right.Left))
}

总结

核心要点:

  1. 递归定义: 翻转整棵树 = 翻转子树 + 交换左右
  2. 原地操作: 直接交换指针,不需要额外空间
  3. 多值赋值: Go的多值赋值右侧先求值再同时赋值
  4. 必须返回: 即使是原地操作,也要返回根节点

易错点:

  • 忘记返回根节点
  • 只交换当前节点,忘记递归处理子节点
  • 多值赋值的理解错误

方法选择:

  • 递归法(推荐):代码简洁,逻辑清晰
  • 迭代法:避免递归栈溢出

有趣的事实: 这道题有个著名的故事——Max HowellHomebrew作者说Google面试官因为这道题拒绝了他但这道题确实很简单😄