【LeetCode】99. 恢复二叉搜索树
文章目录
- 99. 恢复二叉搜索树
- 题目描述
- 示例 1:
- 示例 2:
- 提示:
- 进阶:使用 O(n) 空间复杂度的解法很容易实现。你能想出一个只使用 O(1) 空间的解决方案吗?
- 解题思路
- 问题深度分析
- 问题本质
- 核心思想
- 关键难点分析
- 典型情况分析
- 算法对比
- 算法流程图
- 主算法流程(中序遍历递归)
- 错误节点识别流程
- Morris遍历流程
- 复杂度分析
- 时间复杂度详解
- 空间复杂度详解
- 关键优化技巧
- 技巧1:中序遍历递归 + 错误节点记录(推荐解法)
- 技巧2:中序遍历迭代
- 技巧3:中序遍历优化(只记录错误节点)
- 技巧4:Morris遍历(O(1)空间,最优解法)
- 边界条件处理
- 边界情况1:只有两个节点
- 边界情况2:相邻节点交换
- 边界情况3:非相邻节点交换
- 边界情况4:根节点参与交换
- 边界情况5:整数边界值
- 测试用例设计
- 基础测试用例
- 进阶测试用例
- 常见错误和陷阱
- 错误1:只记录一个错误节点
- 错误2:交换节点而不是交换值
- 错误3:Morris遍历后未恢复树结构
- 错误4:prev更新时机错误
- 实用技巧
- 进阶扩展
- 扩展1:恢复多个错误节点
- 扩展2:恢复并返回错误信息
- 扩展3:验证恢复后的BST
- 扩展4:最小交换次数
- 应用场景
- 总结
- 完整题解代码
99. 恢复二叉搜索树
题目描述
给你二叉搜索树的根节点 root ,该树中的 恰好 两个节点的值被错误地交换。请在不改变其结构的情况下,恢复这棵树 。
示例 1:

输入:root = [1,3,null,null,2]
输出:[3,1,null,null,2]
解释:3 不能是 1 的左孩子,因为 3 > 1 。交换 1 和 3 使二叉搜索树有效。
示例 2:

输入:root = [3,1,4,null,null,2]
输出:[2,1,4,null,null,3]
解释:2 不能在 3 的右子树中,因为 2 < 3 。交换 2 和 3 使二叉搜索树有效。
提示:
- 树上节点的数目在范围 [2, 1000] 内
- -2^31 <= Node.val <= 2^31 - 1
进阶:使用 O(n) 空间复杂度的解法很容易实现。你能想出一个只使用 O(1) 空间的解决方案吗?
解题思路
问题深度分析
这是经典的BST修复问题,也是中序遍历的典型应用。核心在于利用BST中序遍历严格递增的性质,找出违反递增关系的两个节点并交换它们的值。
问题本质
给定一个BST,其中恰好两个节点的值被错误交换,需要在不改变树结构的情况下恢复BST。这是一个树遍历 + 错误检测问题,需要找到两个错误节点并交换它们的值。
核心思想
BST中序遍历的递增性:
- 中序遍历BST:得到严格递增的序列
- 错误检测:找出违反递增关系的节点对
- 错误分类:
- 情况1(相邻交换):中序遍历中只有一个逆序对,如
[1,3,2,4]→ 交换3和2 - 情况2(非相邻交换):中序遍历中有两个逆序对,如
[1,5,3,4,2,6]→ 交换5和2
- 情况1(相邻交换):中序遍历中只有一个逆序对,如
- 节点交换:交换找到的两个错误节点的值
关键技巧:
- 利用中序遍历严格递增的性质
- 记录前一个访问的节点,检测逆序对
- 分类处理相邻和非相邻交换的情况
- 使用Morris遍历实现O(1)空间复杂度
关键难点分析
难点1:错误节点的识别
- 需要准确识别两个错误交换的节点
- 相邻交换:只有一个逆序对,两个节点都需要交换
- 非相邻交换:有两个逆序对,第一个逆序对的前一个节点和第二个逆序对的后一个节点需要交换
难点2:逆序对的判断
- 中序遍历中,如果前一个节点值 > 当前节点值,说明存在逆序
- 第一个逆序对:记录前一个节点为first错误节点
- 第二个逆序对:记录当前节点为second错误节点
难点3:O(1)空间实现
- 传统中序遍历需要O(n)或O(h)空间
- Morris遍历可以在O(1)空间内完成中序遍历
- 需要在Morris遍历过程中记录错误节点
典型情况分析
情况1:相邻节点交换
原始BST: 中序遍历: [1,2,3,4,5]
错误BST: 中序遍历: [1,3,2,4,5]错误位置: ↑ ↑逆序对: (3,2)交换: 3 ↔ 2
结果: [1,2,3,4,5] ✓
情况2:非相邻节点交换
原始BST: 中序遍历: [1,2,3,4,5,6]
错误BST: 中序遍历: [1,5,3,4,2,6]错误位置: ↑ ↑ ↑逆序对1: (5,3)逆序对2: (4,2)交换: 5 ↔ 2
结果: [1,2,3,4,5,6] ✓
情况3:根节点参与交换
原始BST: 中序遍历: [1,2,3]
错误BST: 中序遍历: [3,2,1]错误位置: ↑ ↑ ↑逆序对1: (3,2)逆序对2: (2,1)交换: 3 ↔ 1
结果: [1,2,3] ✓
情况4:叶子节点交换
原始BST: 中序遍历: [1,2,3,4]
错误BST: 中序遍历: [1,4,3,2]错误位置: ↑ ↑ ↑逆序对1: (4,3)逆序对2: (3,2)交换: 4 ↔ 2
结果: [1,2,3,4] ✓
算法对比
| 算法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| 中序遍历递归 | O(n) | O(h) | 代码简洁,易于理解 |
| 中序遍历迭代 | O(n) | O(n) | 避免递归栈溢出 |
| 中序遍历优化 | O(n) | O(h) | 只记录错误节点,优化 |
| Morris遍历 | O(n) | O(1) | 最优解法,空间最优 |
注:n为节点数,h为树高度
算法流程图
主算法流程(中序遍历递归)
graph TDA[recoverTree(root)] --> B[初始化prev, first, second]B --> C[inorder(root)]C --> D{root==nil?}D -->|是| E[return]D -->|否| F[inorder(root.Left)]F --> G{prev != nil?}G -->|是| H{prev.Val > root.Val?}H -->|是| I{first == nil?}I -->|是| J[first = prev, second = root]I -->|否| K[second = root]H -->|否| L[更新prev]G -->|否| M[更新prev]J --> LK --> LM --> N[inorder(root.Right)]L --> NN --> O[交换first和second的值]
错误节点识别流程
Morris遍历流程
复杂度分析
时间复杂度详解
中序遍历算法:O(n)
- 需要遍历所有节点一次
- 每次访问进行常数时间检查
- 总时间:O(n)
Morris遍历算法:O(n)
- 每个节点最多被访问两次(建立线索和拆除线索)
- 每次访问进行常数时间检查
- 总时间:O(n)
空间复杂度详解
中序遍历递归算法:O(h)
- 递归调用栈深度为树高度
- 最坏情况(链状树):O(n)
- 最好情况(平衡树):O(log n)
中序遍历迭代算法:O(n)
- 需要显式栈存储节点
- 最坏情况栈大小为n
- 总空间:O(n)
Morris遍历算法:O(1)
- 只使用常数额外空间(几个指针变量)
- 通过修改树指针实现遍历
- 总空间:O(1)
关键优化技巧
技巧1:中序遍历递归 + 错误节点记录(推荐解法)
var prev, first, second *TreeNodefunc recoverTree(root *TreeNode) {prev, first, second = nil, nil, nilinorder(root)// 交换两个错误节点的值first.Val, second.Val = second.Val, first.Val
}func inorder(root *TreeNode) {if root == nil {return}inorder(root.Left)// 检查逆序if prev != nil && prev.Val > root.Val {if first == nil {// 第一个逆序对first = prev}// 第二个逆序对(可能不存在,如果相邻交换)second = root}prev = rootinorder(root.Right)
}
优势:
- 时间复杂度:O(n)
- 空间复杂度:O(h)
- 代码简洁,逻辑清晰
- 正确处理相邻和非相邻交换
技巧2:中序遍历迭代
func recoverTree(root *TreeNode) {var prev, first, second *TreeNodestack := []*TreeNode{}cur := rootfor cur != nil || len(stack) > 0 {// 一路向左for cur != nil {stack = append(stack, cur)cur = cur.Left}// 访问节点n := len(stack) - 1node := stack[n]stack = stack[:n]// 检查逆序if prev != nil && prev.Val > node.Val {if first == nil {first = prev}second = node}prev = node// 转向右子树cur = node.Right}// 交换first.Val, second.Val = second.Val, first.Val
}
特点:避免递归,适合深度很大的树
技巧3:中序遍历优化(只记录错误节点)
func recoverTree(root *TreeNode) {var prev, first, second *TreeNodevar dfs func(*TreeNode)dfs = func(node *TreeNode) {if node == nil {return}dfs(node.Left)if prev != nil && prev.Val > node.Val {if first == nil {first = prev}second = node}prev = nodedfs(node.Right)}dfs(root)first.Val, second.Val = second.Val, first.Val
}
特点:使用闭包,代码更简洁
技巧4:Morris遍历(O(1)空间,最优解法)
func recoverTree(root *TreeNode) {var prev, first, second *TreeNodecur := rootfor cur != nil {if cur.Left == nil {// 访问当前节点if prev != nil && prev.Val > cur.Val {if first == nil {first = prev}second = cur}prev = curcur = cur.Right} else {// 寻找前驱节点pre := cur.Leftfor pre.Right != nil && pre.Right != cur {pre = pre.Right}if pre.Right == nil {// 建立线索pre.Right = curcur = cur.Left} else {// 拆除线索并访问pre.Right = nilif prev != nil && prev.Val > cur.Val {if first == nil {first = prev}second = cur}prev = curcur = cur.Right}}}// 交换first.Val, second.Val = second.Val, first.Val
}
优势:
- 时间复杂度:O(n)
- 空间复杂度:O(1)
- 满足进阶要求
- 适合空间受限的场景
边界条件处理
边界情况1:只有两个节点
- 处理:两个节点值交换,中序遍历检测到一个逆序对
- 验证:first和second都正确设置,交换即可
边界情况2:相邻节点交换
- 处理:只有一个逆序对,first和second都指向这个逆序对的两个节点
- 验证:交换这两个节点即可
边界情况3:非相邻节点交换
- 处理:有两个逆序对,first指向第一个逆序对的前一个节点,second指向第二个逆序对的后一个节点
- 验证:交换first和second即可
边界情况4:根节点参与交换
- 处理:中序遍历从根节点开始,第一个逆序对可能包含根节点
- 验证:算法正确处理,first或second可能指向根节点
边界情况5:整数边界值
- 处理:节点值可能为-231或231-1
- 验证:使用标准比较操作,不受边界值影响
测试用例设计
基础测试用例
- 相邻交换:
[1,3,null,null,2]→[3,1,null,null,2](交换1和3) - 非相邻交换:
[3,1,4,null,null,2]→[2,1,4,null,null,3](交换3和2) - 两个节点:
[2,1]→[1,2](交换1和2) - 根节点交换:
[2,1,3]→[1,2,3](交换2和1)
进阶测试用例
- 完全BST相邻交换:
[4,2,6,1,5,5,7]→ 交换两个5 - 链状BST交换:
[3,2,null,1]→[1,2,null,3] - 深度不平衡交换:极端左偏或右偏的BST
- 大值交换:包含边界值的BST
- 复杂交换:多层级BST中的非相邻交换
- 对称交换:交换后BST结构对称的情况
常见错误和陷阱
错误1:只记录一个错误节点
// 错误写法
if prev != nil && prev.Val > root.Val {first = prev// 忘记设置second
}// 正确写法
if prev != nil && prev.Val > root.Val {if first == nil {first = prev}second = root // 必须更新second
}
原因:相邻交换时,second需要指向当前节点;非相邻交换时,second需要更新为第二个逆序对的节点
错误2:交换节点而不是交换值
// 错误写法:交换节点指针
first, second = second, first// 正确写法:交换节点值
first.Val, second.Val = second.Val, first.Val
原因:题目要求不改变树结构,只能交换值
错误3:Morris遍历后未恢复树结构
- 问题:Morris遍历会修改树指针,但遍历过程中会恢复
- 注意:如果在遍历中断,需要手动恢复所有线索
错误4:prev更新时机错误
// 错误写法:在检查逆序前更新prev
prev = root
if prev != nil && prev.Val > root.Val {// prev已经指向root,无法检测逆序
}// 正确写法:先检查逆序,再更新prev
if prev != nil && prev.Val > root.Val {// 检测逆序
}
prev = root // 检查后更新
实用技巧
- 优先使用中序遍历递归方法:代码简洁,逻辑清晰,易于理解和调试
- 理解错误节点识别:相邻交换vs非相邻交换的处理方式不同
- Morris遍历:适合空间受限的场景,但代码复杂度较高
- 测试各种情况:相邻交换、非相邻交换、根节点交换等
- 注意值交换:只能交换值,不能交换节点指针
- 边界值处理:确保算法在极端情况下也能正确工作
进阶扩展
扩展1:恢复多个错误节点
- 如果BST中有多个节点被错误交换,需要找出所有错误并修复
扩展2:恢复并返回错误信息
- 不仅恢复BST,还返回具体哪些节点被交换了
扩展3:验证恢复后的BST
- 恢复后验证BST是否有效
扩展4:最小交换次数
- 如果允许多次交换,找出恢复BST所需的最少交换次数
应用场景
- 数据库索引修复:修复B树/B+树索引中的错误
- 数据结构修复:修复BST相关代码中的bug导致的错误
- 算法调试:在BST构建算法中检测和修复错误
- 数据恢复:从损坏的BST数据中恢复正确结构
- 系统维护:定期检查和修复BST结构中的错误
总结
恢复BST是一个经典的树遍历 + 错误检测问题,核心在于:
- 利用BST中序遍历递增性质:找出违反递增关系的节点
- 正确识别错误节点:区分相邻和非相邻交换的情况
- 只交换值不交换结构:保持树结构不变
- 优化空间复杂度:使用Morris遍历实现O(1)空间
通过系统学习和练习,可以熟练掌握BST恢复的各种方法!
完整题解代码
package mainimport ("fmt"
)type TreeNode struct {Val intLeft *TreeNodeRight *TreeNode
}// =========================== 方法一:中序遍历递归 + 错误节点记录(推荐解法) ===========================
var prev1, first1, second1 *TreeNodefunc recoverTree1(root *TreeNode) {prev1, first1, second1 = nil, nil, nilinorder1(root)// 交换两个错误节点的值if first1 != nil && second1 != nil {first1.Val, second1.Val = second1.Val, first1.Val}
}func inorder1(root *TreeNode) {if root == nil {return}inorder1(root.Left)// 检查逆序if prev1 != nil && prev1.Val > root.Val {if first1 == nil {// 第一个逆序对first1 = prev1}// 第二个逆序对(可能不存在,如果相邻交换)second1 = root}prev1 = rootinorder1(root.Right)
}// =========================== 方法二:中序遍历迭代 ===========================
func recoverTree2(root *TreeNode) {var prev, first, second *TreeNodestack := []*TreeNode{}cur := rootfor cur != nil || len(stack) > 0 {// 一路向左for cur != nil {stack = append(stack, cur)cur = cur.Left}// 访问节点n := len(stack) - 1node := stack[n]stack = stack[:n]// 检查逆序if prev != nil && prev.Val > node.Val {if first == nil {first = prev}second = node}prev = node// 转向右子树cur = node.Right}// 交换if first != nil && second != nil {first.Val, second.Val = second.Val, first.Val}
}// =========================== 方法三:中序遍历优化(闭包) ===========================
func recoverTree3(root *TreeNode) {var prev, first, second *TreeNodevar dfs func(*TreeNode)dfs = func(node *TreeNode) {if node == nil {return}dfs(node.Left)if prev != nil && prev.Val > node.Val {if first == nil {first = prev}second = node}prev = nodedfs(node.Right)}dfs(root)if first != nil && second != nil {first.Val, second.Val = second.Val, first.Val}
}// =========================== 方法四:Morris遍历(O(1)空间,最优解法) ===========================
func recoverTree4(root *TreeNode) {var prev, first, second *TreeNodecur := rootfor cur != nil {if cur.Left == nil {// 访问当前节点if prev != nil && prev.Val > cur.Val {if first == nil {first = prev}second = cur}prev = curcur = cur.Right} else {// 寻找前驱节点(左子树的最右节点)pre := cur.Leftfor pre.Right != nil && pre.Right != cur {pre = pre.Right}if pre.Right == nil {// 建立线索pre.Right = curcur = cur.Left} else {// 拆除线索并访问当前节点pre.Right = nilif prev != nil && prev.Val > cur.Val {if first == nil {first = prev}second = cur}prev = curcur = cur.Right}}}// 交换if first != nil && second != nil {first.Val, second.Val = second.Val, first.Val}
}// =========================== 工具函数:构建二叉树 ===========================
func arrayToTreeLevelOrder(arr []interface{}) *TreeNode {if len(arr) == 0 {return nil}if arr[0] == nil {return nil}root := &TreeNode{Val: arr[0].(int)}queue := []*TreeNode{root}i := 1for i < len(arr) && len(queue) > 0 {node := queue[0]queue = queue[1:]// 左子节点if i < len(arr) && arr[i] != nil {left := &TreeNode{Val: arr[i].(int)}node.Left = leftqueue = append(queue, left)}i++// 右子节点if i < len(arr) && arr[i] != nil {right := &TreeNode{Val: arr[i].(int)}node.Right = rightqueue = append(queue, right)}i++}return root
}// =========================== 工具函数:中序遍历验证(用于测试) ===========================
func inorderTraversal(root *TreeNode) []int {var res []intvar dfs func(*TreeNode)dfs = func(node *TreeNode) {if node == nil {return}dfs(node.Left)res = append(res, node.Val)dfs(node.Right)}dfs(root)return res
}// =========================== 工具函数:验证BST ===========================
func isValidBST(root *TreeNode) bool {var prev *intvar dfs func(*TreeNode) booldfs = func(node *TreeNode) bool {if node == nil {return true}if !dfs(node.Left) {return false}if prev != nil && node.Val <= *prev {return false}prev = &node.Valreturn dfs(node.Right)}return dfs(root)
}// =========================== 工具函数:复制树(用于测试) ===========================
func copyTree(root *TreeNode) *TreeNode {if root == nil {return nil}newRoot := &TreeNode{Val: root.Val}newRoot.Left = copyTree(root.Left)newRoot.Right = copyTree(root.Right)return newRoot
}// =========================== 测试 ===========================
func main() {fmt.Println("=== LeetCode 99: 恢复二叉搜索树 ===\n")testCases := []struct {name stringroot *TreeNodeexpected []int // 恢复后的中序遍历结果expectedSwap []int // 被交换的两个值(用于验证)}{{name: "例1: [1,3,null,null,2] - 相邻交换",root: arrayToTreeLevelOrder([]interface{}{1, 3, nil, nil, 2}),expected: []int{1, 2, 3},expectedSwap: []int{1, 3},},{name: "例2: [3,1,4,null,null,2] - 非相邻交换",root: arrayToTreeLevelOrder([]interface{}{3, 1, 4, nil, nil, 2}),expected: []int{1, 2, 3, 4},expectedSwap: []int{3, 2},},{name: "两个节点: [2,1]",root: arrayToTreeLevelOrder([]interface{}{2, 1}),expected: []int{1, 2},expectedSwap: []int{1, 2},},{name: "根节点交换: [2,1,3]",root: arrayToTreeLevelOrder([]interface{}{2, 1, 3}),expected: []int{1, 2, 3},expectedSwap: []int{1, 2},},{name: "复杂交换: [5,3,9,1,4,7,10,null,null,2]",root: arrayToTreeLevelOrder([]interface{}{5, 3, 9, 1, 4, 7, 10, nil, nil, 2}),expected: []int{1, 2, 3, 4, 5, 7, 9, 10},expectedSwap: []int{1, 5}, // 交换1和5},{name: "链状交换: [3,2,null,1]",root: arrayToTreeLevelOrder([]interface{}{3, 2, nil, 1}),expected: []int{1, 2, 3},expectedSwap: []int{1, 3},},{name: "完全BST交换: [4,2,6,1,3,5,7] -> [4,2,6,1,5,3,7]",root: arrayToTreeLevelOrder([]interface{}{4, 2, 6, 1, 5, 3, 7}), // 交换3和5expected: []int{1, 2, 3, 4, 5, 6, 7},expectedSwap: []int{3, 5},},}methods := map[string]func(*TreeNode){"中序遍历递归": recoverTree1,"中序遍历迭代": recoverTree2,"中序遍历优化": recoverTree3,"Morris遍历": recoverTree4,}for methodName, methodFunc := range methods {fmt.Printf("方法:%s\n", methodName)pass := 0for i, tc := range testCases {// 复制树以避免修改原始测试用例testRoot := copyTree(tc.root)methodFunc(testRoot)// 验证恢复后的BSTgot := inorderTraversal(testRoot)isValid := isValidBST(testRoot)expectedMatch := trueif len(got) != len(tc.expected) {expectedMatch = false} else {for j := range got {if got[j] != tc.expected[j] {expectedMatch = falsebreak}}}ok := expectedMatch && isValidstatus := "✅"if !ok {status = "❌"}fmt.Printf(" 测试%d(%s): %s\n", i+1, tc.name, status)if !ok {fmt.Printf(" 输出中序遍历: %v\n 期望中序遍历: %v\n 是否有效BST: %v\n", got, tc.expected, isValid)} else {pass++}}fmt.Printf(" 通过: %d/%d\n\n", pass, len(testCases))}
}
