# 翻转二叉树 (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: 处理基准情况** ```go if root == nil { return nil // 空树直接返回 } ``` **关键点**: 递归的终止条件 **步骤2: 递归翻转左右子树** ```go left := invertTree(root.Left) // 翻转左子树 right := invertTree(root.Right) // 翻转右子树 ``` **关键点**: 先递归处理子节点,再处理当前节点 **步骤3: 交换左右子节点** ```go root.Left = right root.Right = left ``` **简化写法**: ```go root.Left, root.Right = root.Right, root.Left ``` **步骤4: 返回当前节点** ```go return root ``` **图解**: ``` 翻转过程: 4 4 4 / \ / \ / \ 2 7 → 2 7 → 7 2 / \ / \ / \ / \ / \ / \ 1 3 6 9 3 1 9 6 9 6 3 1 原始 翻转子节点 交换子节点 ``` ### 关键细节说明 **细节1: 为什么可以一行代码完成?** ```go 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: 为什么不需要返回值?** 实际上需要返回值! ```go func invertTree(root *TreeNode) *TreeNode { if root == nil { return nil // 必须返回 } // ... return root // 必须返回 } ``` **原因**: - 需要返回翻转后的根节点 - 即使是原地操作,也需要返回值以保持接口一致 **细节3: 为什么是原地操作?** ```go // 直接修改指针,不需要创建新节点 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 ``` ## 代码实现 ### 方法一:递归(推荐) ```go 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) 空间 ### 方法二:递归(分步写法) ```go 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) 空间 ### 方法三:迭代(栈) ```go 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) ```go 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: 忘记返回节点 ❌ **错误写法**: ```go func invertTree(root *TreeNode) *TreeNode { if root == nil { return nil } root.Left, root.Right = invertTree(root.Right), invertTree(root.Left) // 忘记返回 root } ``` ✅ **正确写法**: ```go func invertTree(root *TreeNode) *TreeNode { if root == nil { return nil } root.Left, root.Right = invertTree(root.Right), invertTree(root.Left) return root // 必须返回 } ``` **原因**: 函数签名要求返回 `*TreeNode` ### 错误2: 交换后忘记处理子节点 ❌ **错误写法**: ```go func invertTree(root *TreeNode) *TreeNode { if root == nil { return nil } root.Left, root.Right = root.Right, root.Left return root // 错误:只交换了当前节点 } ``` ✅ **正确写法**: ```go func invertTree(root *TreeNode) *TreeNode { if root == nil { return nil } root.Left, root.Right = invertTree(root.Right), invertTree(root.Left) return root // 递归处理子节点 } ``` **原因**: 需要递归翻转所有子节点,不只是当前节点 ### 错误3: 多值赋值理解错误 ❌ **错误理解**: ```go // 错误理解:先赋值 root.Left,再赋值 root.Right root.Left, root.Right = root.Right, root.Left ``` **正确理解**: ```go // Go的多值赋值是同时的 // 右侧先求值,再同时赋值给左侧 temp1, temp2 := root.Right, root.Left // 先求值 root.Left = temp1 // 再赋值 root.Right = temp2 ``` ## 变体问题 ### 变体1: 判断是否是翻转二叉树 判断两棵树是否互为翻转: ```go 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叉树 ```go 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: 镜像对称二叉树 实际上就是翻转二叉树的应用: ```go 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 Howell(Homebrew作者)说Google面试官因为这道题拒绝了他,但这道题确实很简单!😄