恢复二叉搜索树 图解题解
这道题到底在问什么
- 输入
- 中序应 10,20,30,40,50,60,70,实际 10,60,30,40,50,20,70
- 输出
- 把 60 与 20 换回
- 输入
- 相邻两节点被换
- 输出
- 只有一处逆序,交换这一对
最优解:一步一步想明白
- 3记住「中序升序被破坏 → 抓逆序对 → first 是第一处的大者、second 是最后一处的小者 → 交换两个值」,下面逐帧套它。
- 4中序 = 升序先找感觉:一棵合法 BST(根 40、左 20、右 60),中序遍历是 20、40、60,严格升序。我们要恢复的那棵,中序却乱了序,顺着这个破绽就能揪出放错的两个节点。
- 5中序乱序这就是待恢复的树。光看树形不好看出哪里错,但只要走一遍中序,升序一被打破就露馅。下面开始中序遍历,一个节点一个节点地比。
- 6从最左下开始中序遍历的顺序是「左子树 → 根 → 右子树」,所以第一个访问到的是整棵树最左下角的节点。prev、first、second 现在都为空,开始逐个走、逐个比。
- 7到达 值10中序走到节点 值10(紫色)。它是中序的第一个,前面没有 prev 可比,直接收下。
- 8记下 值10值10 成为目前中序的开头,把 prev 更新成它,继续往后走。
- 9到达 值60中序走到节点 值60(紫色)。要拿它和 prev = 值10 比一比,看是不是还在升序。
- 10仍升序值60 不小于前一个,序列仍然升序,没有逆序。把 prev 更新成 值60,接着走。
- 11到达 值30中序走到节点 值30(紫色)。要拿它和 prev = 值60 比一比,看是不是还在升序。
- 12值60 > 值30逆序出现!prev = 值60 竟然大于当前 值30(标红)。这是第一处逆序,把较大的 prev = 值60 记为 first;两种情况都把较小的当前节点 值30 记为 second。
- 13到达 值40中序走到节点 值40(紫色)。要拿它和 prev = 值30 比一比,看是不是还在升序。
- 14仍升序值40 不小于前一个,序列仍然升序,没有逆序。把 prev 更新成 值40,接着走。
- 15到达 值50中序走到节点 值50(紫色)。要拿它和 prev = 值40 比一比,看是不是还在升序。
- 16仍升序值50 不小于前一个,序列仍然升序,没有逆序。把 prev 更新成 值50,接着走。
- 17到达 值20中序走到节点 值20(紫色)。要拿它和 prev = 值50 比一比,看是不是还在升序。
- 18值50 > 值20逆序出现!prev = 值50 竟然大于当前 值20(标红)。这是第二处逆序,first 保持不动;两种情况都把较小的当前节点 值20 记为 second。
- 19到达 值70中序走到节点 值70(紫色)。要拿它和 prev = 值20 比一比,看是不是还在升序。
- 20仍升序值70 不小于前一个,序列仍然升序,没有逆序。把 prev 更新成 值70,接着走。
- 21first 值60 · second 值20遍历结束,两处逆序把放错的两个节点都揪了出来(标红):first = 值60、second = 值20。它们正是当初被对调的那一对,只要把两个节点的值换回来就恢复了。
- 22值60 ↔ 值20只交换这两个节点里的值(绿色),树的结构一点没动。原来 first 处的 值60 和 second 处的 值20 互换,现在它们归位了。
- 23中序重新升序再走一遍中序:10、20、30、40、50、60、70,严格升序,逆序对全没了,BST 恢复成功。整个过程只比较、不挪结构,空间用在递归栈上。
⚠️ 容易写错的地方
✗ 错:first 也跟着每次逆序更新
✓ 对:first 只在「第一次」逆序时记 prev
非相邻交换有两处逆序,first 必须停在第一处的较大者;若每次都更新,first 会被第二处覆盖、最终交换错节点
✗ 错:second 只在第一次逆序记
✓ 对:second 每次逆序都更新成当前节点
相邻交换时只有一处逆序,second 就是这一对的小者;非相邻时要取到第二处的小者,所以每次都更新
✗ 错:交换的是节点指针/结构
✓ 对:只交换两个节点的值
题目要求结构不变,改指针会破坏树形;直接换 val 最简单也最稳
完整代码(Python / C++ / Java)
Python
from typing import List, Optional
class TreeNode:
def __init__(self,val=0,left=None,right=None):
self.val=val; self.left=left; self.right=right
class Solution:
def recoverTree(self, root: Optional[TreeNode]) -> None:
first = second = prev = None
def dfs(node):
nonlocal first, second, prev
if not node: return
dfs(node.left)
if prev and prev.val > node.val:
if not first: first = prev
second = node
prev = node
dfs(node.right)
dfs(root)
if first and second: first.val, second.val = second.val, first.valC++
#include <algorithm>
#include <functional>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
using namespace std;
struct TreeNode{int val;TreeNode* left;TreeNode* right;TreeNode():val(0),left(nullptr),right(nullptr){} TreeNode(int x):val(x),left(nullptr),right(nullptr){} TreeNode(int x,TreeNode*l,TreeNode*r):val(x),left(l),right(r){}};
class Solution{TreeNode* first=nullptr;TreeNode* second=nullptr;TreeNode* prev=nullptr; public: void recoverTree(TreeNode* root){ first=second=prev=nullptr; dfs(root); if(first&&second)swap(first->val,second->val);} void dfs(TreeNode*n){ if(!n)return; dfs(n->left); if(prev&&prev->val>n->val){ if(!first)first=prev; second=n;} prev=n; dfs(n->right);} };Java
import java.util.*;
class TreeNode{int val;TreeNode left,right;TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val,TreeNode left,TreeNode right){this.val=val;this.left=left;this.right=right;}}
class Solution{ TreeNode first,second,prev; public void recoverTree(TreeNode root){ first=second=prev=null; dfs(root); if(first!=null&&second!=null){ int t=first.val; first.val=second.val; second.val=t; } } void dfs(TreeNode n){ if(n==null)return; dfs(n.left); if(prev!=null&&prev.val>n.val){ if(first==null)first=prev; second=n; } prev=n; dfs(n.right);} }复杂度
时间
O(n)
中序遍历每个节点恰好访问一次,n 是节点数;末尾交换是 O(1)
空间
O(h)
递归栈深度等于树高 h,最坏退化成链时 O(n);用 Morris 中序可降到 O(1)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 恢复二叉搜索树 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
能不能做到 O(1) 额外空间?+
可以,用 Morris 中序遍历。它在遍历时临时建立、随后又恢复「线索」,从而不用递归栈或显式栈走完中序,空间降到 O(1);代价是代码更复杂、且遍历中会临时改动指针再恢复。面试里能说出「常规 O(h),Morris 可到 O(1)」就到位。
为什么只交换值、不交换节点?+
题目要求保持树的结构不变。两个被换的节点位置(以及它们的左右孩子)都不该动,真正错的只是「值放错了格子」,所以把两个节点的 val 对调即可,既满足结构不变、又最简单不易错。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 恢复二叉搜索树 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。