翻转二叉树以匹配先序遍历 图解题解
这道题到底在问什么
- 输入
- root=[50,30,80,20,60], voyage=[50,80,30,20,60]
- 输出
- [50]
- 输入
- root=[50,30], voyage=[30,50]
- 输出
- [-1]
最优解:为什么这么做
一句话答案:LeetCode 971 翻转二叉树以匹配先序遍历:一趟前序 DFS 贪心,左孩子接不上目标就翻转当前节点、改先走右子树,每步动作被 voyage 唯一逼定所以翻得最少,时间 O(n)。
先序要变成 voyage,能翻哪些节点
先序遍历,也就是先访问自己、再左子树、再右子树。翻转一个节点就是把它左右子树对调,先序里这两棵子树的先后随之互换。题目要用最少翻转让整棵树的先序等于目标序列 voyage,做不到返回 [-1]。题面例子 root=[50,30,80,20,60]、voyage=[50,80,30,20,60] 返回 [50];root=[50,30]、voyage=[30,50] 返回 [-1]。
把每种翻法都试一遍,为什么撑不住
每个节点都能翻或不翻,n 个节点就有 2 的 n 次方种翻法。逐一翻出整棵树再算先序去对 voyage,节点稍多就是天文数字,还要挑翻得最少的,稍大的树根本跑不完。
每个节点翻不翻,voyage 早就定死了
先序永远先落在根上。某节点匹配完自己、指针挪一格后,先序接下来要的 voyage[i] 就是紧跟的那棵子树开头。若该节点左孩子值正好等于 voyage[i],左子树本就该排前面、不能翻;若对不上,只能翻转让右子树先走去接。两种各只有一个动作、没有可翻可不翻的余地,照做次数天然最少;左右孩子都接不上就判无解,主线走前序 DFS。
一趟前序 DFS,每到一个节点做三件事
用全局指针 i 指着 voyage,从 0 起、只增不减。进入节点先拿它的值和 voyage[i] 比:不等就把 ok 标失败、返回,就此无解。相等则 i 加一,再看左孩子——左孩子为空、或值等于此刻的 voyage[i],就先递归左子树、再递归右;否则把节点值追加进答案数组 ans,改成先递归右、再递归左。走完 ok 仍为真返回 ans,否则返回 [-1]。
拿题面这棵五节点的树,逐个节点定翻不翻
root=[50,30,80,20,60]:根 50 挂左 30、右 80,30 再挂左 20、右 60;voyage=[50,80,30,20,60]。i 从 0 进 50,50 对上 voyage[0],i 变 1。50 的左孩子 30 不等于 voyage[1]=80,翻转:50 记进 ans,改先走右子树。到 80 对上 voyage[1],i 变 2,叶子返回。回到 30 对上 voyage[2],i 变 3,左孩子 20 等于 voyage[3]=20,不翻走左。到 20 对上 voyage[3],i 变 4。再到 60 对上 voyage[4],i 变 5 到末尾。只翻 50 一次,返回 [50]。
单节点、根就对不上,各返回什么
每个节点在 DFS 里只访问一次,节点内比较、记录都是常数功夫,时间 O(n)。空间只花在递归栈上,层数跟着树高 h 走,越均衡越近 O(log n)、歪成链时到 O(n)。边界上,只有一个节点时根一匹配就返回空数组 [];根值和 voyage[0] 一开始就对不上就返回 [-1],例二 root=[50,30]、voyage=[30,50] 正是根 50 接不住 30。两处暗坑:判左孩子比的是 i 自增后的 voyage[i],写成 voyage[i+1] 会挪错一格;翻转时只记答案却忘把递归改成先右后左,等于白记。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 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],说明这棵树怎么翻都排不出目标先序,直接无解返回 [-1]。
能不能不用递归,改成迭代写?+
可以,用一个显式栈模拟先序即可:栈里压待处理的节点,弹出时做同样的事——比当前值和 voyage[i]、看左孩子决定翻不翻、再按顺序把孩子压回栈。注意翻转时压栈的顺序要反过来,让右子树后压、先出。迭代版省掉了系统递归栈,逻辑和递归版一一对应,时间还是 O(n)。
指针 i 会不会往回退?翻转会不会改变某个位置该是谁?+
都不会。i 全程只增不减,因为先序本身就是一条从左到右的线性序列,指针单向推进才对得上;翻转只改变一个节点先走左还是先走右,也就是调整子树进入先序的先后次序,并不会改变某个位置本该出现哪个值。所以当某节点的值和 voyage[i] 对不上时,那是真正的死局信号,翻转救不回来,只能判 [-1]。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 翻转二叉树以匹配先序遍历 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。