翻转二叉树以匹配先序遍历 图解题解
这道题到底在问什么
- 输入
- root=[50,30,80,20,60], voyage=[50,80,30,20,60]
- 输出
- [50]
- 输入
- root=[50,30], voyage=[30,50]
- 输出
- [-1]
最优解:一步一步想明白
- 3记住这句话:先比当前值,再看左孩子对不对得上下一个 voyage,不对就翻转换右子树先走。下面每一帧都在套它。
- 4原先序 50 30 20 60 80先把局面看清。左边这棵树有 5 个节点:根是 50,它的左孩子 30、右孩子 80;30 下面又挂着左孩子 20、右孩子 60。原树的先序(根左右)是 50 30 20 60 80。可目标 voyage 是 50 80 30 20 60,明显对不上,得靠翻转来纠。
- 5i=0 从根开始我们用一个指针 i 在 voyage 上从左往右走,初始 i=0,指向第一个值 50。再从根节点开始做先序 DFS:每到一个节点,就拿它的值和 voyage[i] 对一对。指针 i 永远只往前走、不回头。现在出发,先看根 50。
- 6比较 50 与 voyage[i]=50走到节点 50(紫色)。当前指针 i 指向 voyage 里的 50。先序的第一步永远是看根,所以先判断:节点 50 是不是正好等于 voyage[0]=50?一看就相等,匹配上了。
- 750 匹配,i→1节点 50 正好等于刚才的 voyage 值,匹配成功,它变绿。指针 i 往后走一格,现在 i=1,指向 voyage 里的下一个值 80。接下来这一格,决定了 50 要不要翻转。
- 8左孩子 30 ? voyage[i]=80节点 50 有左孩子 30(红框)。先序里,匹配完根之后紧跟的应该是某棵子树的开头,而 voyage[1]=80。关键一问:左孩子 30 等于 80 吗?等于就让左子树先走,不等就得翻转让右子树先走。
- 9记入答案,改先右后左左孩子 30 并不等于 voyage 要的 80。原样走左子树会立刻对不上,唯一的补救就是翻转节点 50:把它记进答案,并改成「先走右子树、再走左子树」。50 标成翻转色。这样右子树的开头就有机会接上 80 了。
- 10比较 80 与 voyage[i]=80走到节点 80(紫色)。当前指针 i 指向 voyage 里的 80。先序的第一步永远是看根,所以先判断:节点 80 是不是正好等于 voyage[1]=80?一看就相等,匹配上了。
- 1180 匹配,i→2节点 80 正好等于刚才的 voyage 值,匹配成功,它变绿。指针 i 往后走一格,现在 i=2,指向 voyage 里的下一个值 30。接下来这一格,决定了 80 要不要翻转。
- 1280 是叶子,本枝走完节点 80 没有左孩子,是个叶子,没有子树需要排序,自然不用翻转。这一枝到此匹配完毕,回到上一层,继续处理还没走的兄弟子树。
- 13比较 30 与 voyage[i]=30走到节点 30(紫色)。当前指针 i 指向 voyage 里的 30。先序的第一步永远是看根,所以先判断:节点 30 是不是正好等于 voyage[2]=30?一看就相等,匹配上了。
- 1430 匹配,i→3节点 30 正好等于刚才的 voyage 值,匹配成功,它变绿。指针 i 往后走一格,现在 i=3,指向 voyage 里的下一个值 20。接下来这一格,决定了 30 要不要翻转。
- 15左孩子 20 ? voyage[i]=20节点 30 有左孩子 20(红框)。先序里,匹配完根之后紧跟的应该是某棵子树的开头,而 voyage[3]=20。关键一问:左孩子 20 等于 20 吗?等于就让左子树先走,不等就得翻转让右子树先走。
- 16左孩子对得上,先左后右左孩子 20 正好等于 voyage 当前要的 20,说明左子树本来就该排在前面,不用翻转 30。按先序原样「先左子树、再右子树」继续往下走。
- 17比较 20 与 voyage[i]=20走到节点 20(紫色)。当前指针 i 指向 voyage 里的 20。先序的第一步永远是看根,所以先判断:节点 20 是不是正好等于 voyage[3]=20?一看就相等,匹配上了。
- 1820 匹配,i→4节点 20 正好等于刚才的 voyage 值,匹配成功,它变绿。指针 i 往后走一格,现在 i=4,指向 voyage 里的下一个值 60。接下来这一格,决定了 20 要不要翻转。
- 1920 是叶子,本枝走完节点 20 没有左孩子,是个叶子,没有子树需要排序,自然不用翻转。这一枝到此匹配完毕,回到上一层,继续处理还没走的兄弟子树。
- 20比较 60 与 voyage[i]=60走到节点 60(紫色)。当前指针 i 指向 voyage 里的 60。先序的第一步永远是看根,所以先判断:节点 60 是不是正好等于 voyage[4]=60?一看就相等,匹配上了。
- 2160 匹配,i→5节点 60 正好等于刚才的 voyage 值,匹配成功,它变绿。指针 i 往后走一格,现在 i=5,已经走到 voyage 末尾了。
- 2260 是叶子,本枝走完节点 60 没有左孩子,是个叶子,没有子树需要排序,自然不用翻转。这一枝到此匹配完毕,回到上一层,继续处理还没走的兄弟子树。
- 23答案 = [50]voyage 指针 i 走到了末尾,途中每个节点都和 voyage 对上了,先序成功对齐。整趟只在节点 50 翻转了一次(因为它的左孩子 30 接不上 voyage 要的 80),节点 30 的左孩子 20 恰好对得上所以没翻。最少翻转点就是 [50],这就是答案。
- 2450 翻转,30 不翻回放一遍这条主线:根 50 匹配后,左孩子 30 对不上目标的 80,翻转 50,先去右边的 80;80 是叶子;回来走 30,30 匹配后它的左孩子 20 正好对上目标的 20,不翻转,依次走 20、60,全部命中。整套就是「值不对立刻判无解,左孩子接不上就翻转」,每步都被 voyage 唯一逼着走,所以翻得最少。
⚠️ 容易写错的地方
✗ 错:判左孩子时用错下标 voyage[i+1]
✓ 对:匹配完根 i 已自增,左孩子要和「当前 voyage[i]」比
下标错位会把翻转判断整体挪一格,结果全乱
✗ 错:翻转了却忘改递归顺序
✓ 对:翻转就要改成先递归右子树、再递归左子树
只记答案不改顺序,先序仍按原左右走,等于没翻
✗ 错:当前值对不上还继续往下走
✓ 对:一旦 root.val ≠ voyage[i] 立刻置失败、返回
继续走只会越对越乱,且 i 越界
✗ 错:以为指针 i 会回退
✓ 对:i 全程只增不减,翻转只改访问顺序
先序是一条线性序列,指针单向推进才对应得上
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def flipMatchVoyage(self, root: Optional[TreeNode], voyage: List[int]) -> List[int]:
def dfs(root):
nonlocal i, ok
if root is None or not ok:
return
if root.val != voyage[i]:
ok = False
return
i += 1
if root.left is None or root.left.val == voyage[i]:
dfs(root.left)
dfs(root.right)
else:
ans.append(root.val)
dfs(root.right)
dfs(root.left)
ans = []
i = 0
ok = True
dfs(root)
return ans if ok else [-1]C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#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 *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
/**
* Definition for a binary tree node.
* 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 *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> flipMatchVoyage(TreeNode* root, vector<int>& voyage) {
bool ok = true;
int i = 0;
vector<int> ans;
function<void(TreeNode*)> dfs = [&](TreeNode* root) {
if (!root || !ok) {
return;
}
if (root->val != voyage[i]) {
ok = false;
return;
}
++i;
if (!root->left || root->left->val == voyage[i]) {
dfs(root->left);
dfs(root->right);
} else {
ans.push_back(root->val);
dfs(root->right);
dfs(root->left);
}
};
dfs(root);
return ok ? ans : vector<int>{-1};
}
};Java
import java.util.*;
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode 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 TreeNode { int val; TreeNode left; TreeNode 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 ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
private int i;
private boolean ok;
private int[] voyage;
private List<Integer> ans = new ArrayList<>();
public List<Integer> flipMatchVoyage(TreeNode root, int[] voyage) {
this.voyage = voyage;
ok = true;
dfs(root);
return ok ? ans : Arrays.asList(-1);
}
private void dfs(TreeNode root) {
if (root == null || !ok) {
return;
}
if (root.val != voyage[i]) {
ok = false;
return;
}
++i;
if (root.left == null || root.left.val == voyage[i]) {
dfs(root.left);
dfs(root.right);
} else {
ans.add(root.val);
dfs(root.right);
dfs(root.left);
}
}
}复杂度
时间
O(n)
每个节点只在 DFS 里被访问一次,节点内的比较、判断、记录都是 O(1),n 个节点共 O(n)
空间
O(h)
只用递归栈,深度等于树高 h;平衡树 O(log n),链状极端树最坏 O(n)。答案数组不计入,最多 n 个翻转点也在 O(n) 内
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 翻转二叉树以匹配先序遍历 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么这种「左孩子接不上就立刻翻」的贪心是对的、而且翻得最少?+
因为每个节点的决策被 voyage 唯一逼定,没有选择余地。匹配完一个节点后,先序的下一个值 voyage[i] 要么等于左孩子(只能左子树先走、不能翻,翻了反而错),要么等于右孩子(只能翻转让右先走)。两种情况各对应唯一动作,既不存在「可翻可不翻」的自由,也就不存在更省的方案,所以贪心结果就是最少翻转。如果左右孩子都接不上 voyage[i],那就是无解。
能不能不用递归、改成迭代?+
可以,用一个显式栈模拟先序即可:栈里压待处理的节点,弹出时做同样的「比值、看左孩子决定翻不翻、按顺序压孩子」逻辑。注意翻转时压栈顺序要反过来,让右子树后压先出。迭代版省掉系统递归栈,逻辑和递归版一一对应,复杂度不变。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 翻转二叉树以匹配先序遍历 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。