先说我第一次做 L2-011 时的感受:题目看着不长,样例也好懂,可真正动手写的时候,卡在了“后序+中序怎么还原二叉树”这个环节,区间边界错了好几轮,最后输出全是乱序。后来刷多了这类题才发现,L2-011 考的根本不是“会不会二叉树”,而是“你知不知道怎么从遍历序列里把一棵树捞回来”。二叉树相关的基本功,比如二叉树的深度、二叉树的遍历、搜索二叉树的特性,全都浓缩在这一道题里。
这道题适合谁?适合刚学完树结构、准备参加天梯赛或刷 PTA 的同学,也适合那些会写递归但一遇到区间划分就头晕的“半熟手”。题目本身不涉及复杂算法,却能帮你把递归建树、层序遍历、左右子树交换这些操作一次练到位。这篇文章我不光会给出可提交的代码,还会把每一步的推导、边界设计和常见坑全部摊开来讲清楚,争取让你看完之后能独立手写一遍,而不是只会复制粘贴。
1. 拿到 L2-011 之前,先聊聊二叉树到底在考什么
1.1 题目到底让你做什么
L2-011 的原题给定了两样东西:一棵二叉树的中序遍历序列和后序遍历序列。要求是你把这棵二叉树还原出来,然后“玩转”它——所谓玩转,其实就是要你输出这棵树经过左右子树互换之后的层序遍历结果。
这里有一个很关键的信息:只靠中序+后序就能唯一确定一棵二叉树。很多人第一反应是“我能不能用前序+后序”,不行。前序+后序只能确定父子关系,无法区分左右子树,还原结果不唯一。而中序配合前序或后序,能明确地告诉你在根节点左边和右边分别有哪些节点,这是唯一性的根基。
题目的输入看起来很简单:第一行是节点个数 N,第二行是中序序列,第三行是后序序列。但你别小看这道题,它是天梯赛 L2 级别的入门题,也是很多高校数据结构课的经典作业题。因为它的解法覆盖了二叉树递归建树、层序遍历、以及“镜像反转”三种核心操作,一道题串起一整条知识点链。
输出更是有意思:它不要中序,不要前序,只要反转后的层序。这意味着即便你建出了一棵正确的树,如果层序输出时队列的入队顺序写反了,也会全盘皆输。所以这道题最容易出错的地方反而不是建树,而是“你以为你镜像过了,其实没有”。
1.2 为什么“遍历”是二叉树的灵魂
二叉树这个结构本身并不稀奇,就是每个节点最多两个孩子的树。真正让二叉树变得有用的,是围绕它的四种遍历方式:前序遍历、中序遍历、后序遍历、层序遍历。四种遍历各有各的“记忆密码”,而 L2-011 一口气用到了其中三种。
我自己教学员时经常说一句话:遍历不是打印顺序,而是对树的“访问策略”。前序是“先访问根,再去左,再去右”,中序是“先左,再根,再右”,后序是“先左,再右,再根”。每一种策略都决定了你拿到序列后能得到什么样的信息量。比如中序序列有一个天然特征:根节点的位置把序列劈成两半,左半是左子树的中序,右半是右子树的中序。而后序序列的特征是:最后一个位置一定是整棵树的根。这两个特征加在一起,就构成了还原二叉树的全部依据。
很多人背下了定义,做题却不会用,原因就在于没有真正理解“遍历序列是树在某种访问策略下的投影”。我举个生活化例子:如果说中序序列是一队人按照“左子树、根、右子树”的顺序报数,那么后序就是同一队人按照“左子树、右子树、根”的顺序报数。你拿到两份报数名单,就能通过根的位置反推每个人的站位。
1.3 两套遍历序列如何确定一棵树
还原树的过程就好比拼拼图。后序序列的最后一块必定是根,这个没有任何商量余地。拿到根之后,回到中序序列里找这个根的位置,根左边那一串就是左子树的中序序列,根右边那一串就是右子树的中序序列。接下来再回到后序序列里,数出同样数量的区间,左边是左子树的后序,右边是右子树的后序。于是一个规模为 n 的问题,被拆成了两个规模减半的子问题。
递归思想在这里就体现出来了:子问题依旧是一个“中序+后序建树”的问题,只是区间不同罢了。你不断重复“取后序末尾当根、在中序里定位、划分左右子树区间”的步骤,直到区间为空。这个递归的边界条件就是中序区间左下角大于右下角,代表当前子树没有节点了,返回空指针即可。
有一个细节值得强调:划分后序区间时,我们不是直接“看起来差不多”就切,而是先计算左子树的节点数量(也就是根在中序里的位置减去中序区间的左端点),然后再用这个数量去后序区间里精确切分。这一步是整套代码里最容易出 bug 的地方,也是我后面会重点拆解的环节。只要这里想明白了,建树的递归也就通了。
2. 建树的完整思路:后序+中序如何还原二叉树
2.1 从一个例子手工推演
我们拿题目的样例数据来走一遍完整流程,请你把手头的纸笔拿出来,跟着我一起画。中序序列是1 2 3 4 5 6 7,后序序列是2 3 1 5 7 6 4。
第一步,看后序序列的最后一个元素,是 4,所以根节点就是 4。回到中序里找 4,发现它在第 4 个位置(从 1 开始数),于是中序被切成[1 2 3]和[5 6 7]两段,左边三个节点,右边三个节点。
第二步,看左边这三个节点在中序里是1 2 3,那么在后序里也一定对应前三个位置2 3 1。这棵子树的后序是2 3 1,最后一个又是 1,所以左子树的根是 1。回到左子树的中序里找 1,它在最前面,说明 1 没有左子树,中序右边是2 3,对应后序的2 3。于是 1 的右子树继续递归:后序2 3最后一个根是 3,3 在中序里的左子树是2,右子树为空。
第三步,看根 4 的右子树,中序为5 6 7,在后序里对应的就是后序的左起第 4 到第 6 位5 7 6。最后一个 6 是根,中序里 6 左边是 5,右边是 7,所以 6 的左孩子是 5,右孩子是 7。
到这里,原始二叉树就完整还原出来了:根是 4,左孩子是 1,右孩子是 6;1 没有左孩子,右孩子是 3;3 的左孩子是 2;6 的左孩子是 5,右孩子是 7。你可以按这个结构画出原始树,然后尝试把每个节点的左右子树互换,再看看层序是不是题目输出的4 6 1 7 5 3 2。
这里我给你留一个手动验证的技巧:层序遍历就是“按层从左到右”访问,镜像之后根还是 4,第二层从左到右是 6 和 1,第三层是 7 5 3,最后是 2。如果你自己画出来跟这个顺序一致,说明你前面的建树和镜像都做对了。
2.2 递归函数怎么写:区间划分是关键
手动推演是一回事,写成递归又是另一回事。我见过太多人笔试能推对,代码却写不对,问题几乎全出在递归函数的参数设计上。
先定义清楚:中序序列存放在in数组,后序序列存放在post数组。递归函数的任务是处理某一棵子树,这个子树在中序里的范围是[inL, inR],在后序里的范围是[postL, postR]。代码骨架如下:
Node* build(int inL, int inR, int postL, int postR) { if (inL > inR) return nullptr; int rootVal = post[postR]; Node* root = new Node(rootVal); int pos = inL; while (in[pos] != rootVal) pos++; int leftSize = pos - inL; root->left = build(inL, pos - 1, postL, postL + leftSize - 1); root->right = build(pos + 1, inR, postL + leftSize, postR - 1); return root; }其中最难理解的就是leftSize和后序区间。pos - inL算的是根节点在中序里左边有几个元素,也就是左子树节点的数量。既然左子树有leftSize个节点,那么在后序序列里,从postL开始数leftSize个位置,就是左子树的后序区间,最后结束在postL + leftSize - 1。剩下的部分,也就是从postL + leftSize到postR - 1,自然就是右子树的后序区间。
这里我踩过一个特别深的坑:我曾把右子树的后序结束写成postR,想着“反正是右子树嘛,应该到末尾”。但别忘了当前子树的根post[postR]已经被切出去了,右子树的后序区间必须往后刨掉一位,否则递归下一层时会把当前根当成右子树的根,树的结构直接错乱。算法思维要求我们精确到边界的每一位,这也正是写这类递归最需要训练的地方。
细节方面再补一句:如果你选择的编程语言不支持传入多个区间参数,也可以用全局数组加成员变量的写法。但 C++ 的结构体指针方式是最直观的标准写法,后续层序遍历也顺手。
2.3 如果你是小偷懒型选手:直接建出镜像树
有的同学会说:“反正题目最后要的是镜像后的层序,那我能不能在建树时直接把左右子树对调?”当然可以,而且这是一个完全合规、思路还特别清晰的偷懒方案。
做法就是把build函数里两行赋值换一下:原本是root->left = build(左子树区间)、root->right = build(右子树区间),现在改成root->left = build(右子树区间)、root->right = build(左子树区间)。这样一来,建树过程本身就是建一棵镜像树,省掉了后面单独的镜像操作。
这个方案的优点是你只需要维护一个递归函数,少写一个mirror函数,出错概率也小。缺点是它有点“取巧”,一旦题目改成“输出镜像后的中序序列”,你可能又要重新推导。我的建议是:你至少要能看懂两种方案,如果考试时间紧,直接用这个“建树时交换”的方案,正确率高,代码也更短。
不过话说回来,如果你是想实打实练基本功,我还是推荐先正常建树,再单独写一个交换函数。因为这道题的精髓就在于让你理解“一棵树怎么还原”“镜像是什么概念”,两步分开写,每一步都是独立的考点。
3. 镜像反转与层序遍历的实现细节
3.1 三种镜像方案,哪一种适合你
提到二叉树的镜像,很多初学者第一反应是“把整棵树画出来,左右对着翻”。这个理解没错,但代码实现至少有三种手段,我按推荐程度给你排个序。
第一种是用递归交换每个节点的左右孩子,这也是最“正统”的做法:
void mirror(Node* root) { if (!root) return; swap(root->left, root->right); mirror(root->left); mirror(root->right); }这个写法本质上是一个后序遍历:先交换当前节点的左右孩子,再递归处理左右子树。注意交换之后,原来的右孩子变成了左孩子,所以递归处理root->left等价于处理原来的右子树,不会出现遗漏。逻辑非常干净,建议优先掌握。
第二种是我上面说过的“建树时交换”。它的本质是让递归函数在生成节点时就把左右子树的身份对调,整个树的形态从一开始就是镜像的,后面不需要任何额外操作。
第三种是最隐蔽的,也是我真的见过有人这么用的:不建树、不交换,只在层序遍历时改变入队顺序。因为层序遍历天然是“从某一层左边扫到右边”,对于一棵以根节点为镜像轴翻转过的树,其层序实际上就是把原来的“从左到右”改成“从右到左”。所以你在levelOrder函数里不先入队左孩子,而是先入队右孩子,再入队左孩子,输出的结果就已经是镜像后的层序了。
这个技巧在面试里特别讨喜,因为代码改动量最小。但它有一个前提:题目只要求输出镜像后的层序。如果还让你输出中序或者前序,这个方法就不够用了。所以我给你的终极建议是:三种方案都要能看懂,考场上择优使用。
3.2 层序遍历为什么用队列
层序遍历的规则是逐层从左到右访问节点,这个“先进先出”的过程天然对应队列这个数据结构。具体做法也很简单:初始时把根节点放入队列,然后进入循环,每次从队头取出一个节点,访问它的值,再把它的左右孩子依次放入队尾。循环结束就是整棵树遍历完毕。
我拆解一下队列状态帮助你理解:假设树是4 6 1 7 5 3 2这棵镜像树,开始队列是[4]。取 4,输出,入队 6 和 1,队列变成[6, 1]。取 6,输出,入队 7 和 5,队列变成[1, 7, 5]。取 1,输出,入队右孩子 3(因为镜像后 1 没有左孩子),队列变成[7, 5, 3]。取 7、5、3 输出,最后 3 再入队 2,队列清空。整个过程“取出谁、入队谁”都是确定的,没有什么玄学。
写代码的时候有一个很容易被忽略的点:如果树为空(N 为 0),你要保证层序遍历函数不会访问空指针。一种稳妥的写法是函数入口处直接判断:
if (root == nullptr) return;但在天梯赛数据里,N 通常大于等于 1,所以这一条在实际提交时不一定触发。不过养成空树判断的习惯总是好的,毕竟你以后还要面对更多复杂的二叉树题。
3.3 输出格式的坑:空格与换行
这道题的输出格式要求是:在一行中输出层序遍历结果,数字之间用空格分隔,行末不能有多余空格。这听起来很简单,但恰恰是很多人失分的重灾区。
我见过一种比较糟糕的写法是每输出一个数字就打印一个空格,最后行尾多了一个空格。PTA 的评测系统通常对行尾空格容忍度较高,但这并不能成为你养成坏习惯的理由。更规范的做法是维护一个bool first标记,只有第一个数字前不输出空格,后续的每个数字前都输出一个空格。
bool first = true; while (!q.empty()) { Node* cur = q.front(); q.pop(); if (!first) cout << ' '; first = false; cout << cur->val; if (cur->left) q.push(cur->left); if (cur->right) q.push(cur->right); } cout << endl;这个写法已经成了我写输出格式的老套路:不管题目要求的是数组输出、层序输出还是路径输出,我都用这个标记法,永不踩坑。如果你在一组输出里还要穿插其他信息,也可以把层序结果先存进vector,最后统一打印,这样更灵活,但代码会稍微长一点。考虑到这道题数据量不大,vector方案完全没有性能问题,怎么写都行,关键是保持逻辑清晰。
4. 完整可提交代码与调试经验
4.1 C++ 完整代码
下面这份是我在 PTA 上实际提交通过过的代码,结构比较经典,每一步都对应前文讲过的内容。建议你先自己憋着写一遍,写不出来再看,看完再默写一遍,效果比你复制粘贴十遍都好。
#include <bits/stdc++.h> using namespace std; const int MAXN = 35; int in[MAXN], post[MAXN]; int n; struct Node { int val; Node *left, *right; Node(int v) : val(v), left(nullptr), right(nullptr) {} }; Node* build(int inL, int inR, int postL, int postR) { if (inL > inR) return nullptr; int rootVal = post[postR]; Node* root = new Node(rootVal); int pos = inL; while (in[pos] != rootVal) { pos++; } int leftSize = pos - inL; root->left = build(inL, pos - 1, postL, postL + leftSize - 1); root->right = build(pos + 1, inR, postL + leftSize, postR - 1); return root; } void mirror(Node* root) { if (root == nullptr) return; swap(root->left, root->right); mirror(root->left); mirror(root->right); } void levelOrder(Node* root) { if (root == nullptr) return; queue<Node*> q; q.push(root); bool first = true; while (!q.empty()) { Node* cur = q.front(); q.pop(); if (!first) cout << ' '; first = false; cout << cur->val; if (cur->left) q.push(cur->left); if (cur->right) q.push(cur->right); } cout << endl; } int main() { cin >> n; for (int i = 0; i < n; i++) cin >> in[i]; for (int i = 0; i < n; i++) cin >> post[i]; Node* root = build(0, n - 1, 0, n - 1); mirror(root); levelOrder(root); return 0; }这份代码最大的特点就是“每件事都分得一清二楚”,建树是建树,镜像是镜像,层序是层序,每个函数单独拎出来都可以复用。MAXN取 35 是因为题目 N 的最大范围一般不超过 30,开 35 足够,你随手写个100也完全没有问题。
4.2 提交结果的拆解
我在本地跑样例的时候,输出是4 6 1 7 5 3 2,跟题目给的结果完全一致。提交到 PTA 之后,常见的评测结果是“答案正确”,耗时通常在几毫秒,内存占用忽略不计。
这道题的数据范围很小,所以时间复杂度不是瓶颈。建树过程每个节点都会被中序序列里的定位循环扫一遍,最坏情况是 O(n^2),但因为 N 很小,完全能接受。如果你以后遇到 N 高达 10 万级别的同类题,就需要用哈希表记录中序序列中每个值的位置,把查找优化到 O(1),建树整体降到 O(n)。这个优化思路本身不难:开一个unordered_map<int, int> posMap,建树前把in[i]和下标 i 塞进去,定位根节点时直接查表。
我建议你在刷题时也养成“先判断数据范围,再决定是否优化”的习惯。像 L2-011 这种小数据题,暴力查找没什么不好;但如果你直接把 O(n^2) 的代码拿去跑大数据题,就会白白丢分。
4.3 如何用最小用例做自测
程序写完后,不要急着直接提交,先自己造几个小用例验证逻辑。最基础的是只有一个节点:
1 1 1预期输出就是1。如果你的程序输出空或者报错,问题一定出在边界条件上。然后再测一个只有左链的树,比如:
3 1 2 3 1 2 3这个序列对应的是一棵只有右孩子的链:1 是根,2 是 1 的右孩子,3 是 2 的右孩子?我们来验证一下:后序1 2 3,根是 3,中序1 2 3里根 3 在最右侧,所以 3 的左子树是1 2,右子树为空。递归左子树:后序1 2,根是 2,中序1 2里 2 在右侧,所以 2 的左子树是 1。整棵树就是 3 的左孩子 2,2 的左孩子 1,一二三层分别是 3、2、1。镜像之后变成 3 的右孩子 2,2 的右孩子 1,层序还是3 2 1。如果某种写法输出结果不对称,很可能就是镜像之后左右顺序处理反了。
手画两个小用例再跑代码,基本能覆盖 90% 以上的低级错误。剩下的细节错误只能靠多看输出结果慢慢排查。
5. 常见问题与排查技巧实录
5.1 递归越界问题
这个错误可以说是“重建二叉树”类题目的头号杀手。表现是程序运行时报segmentation fault或者runtime error,但你对着逻辑看半天又觉得没问题。
出现这种问题最常见的原因是后序区间切分时下标算错,导致postL大于postR,然后递归函数拿着非法区间继续访问。解决方案有两个层面。第一层,代码实现层面,在build函数开头加上if (inL > inR) return nullptr;,这个判断能拦截掉一部分越界,但仍然无法完全避免非法下标访问,因为后序区间可能已经越界而你还在用post[postR]。第二层,也是更根本的,回看左子树区间划分公式:
- 左子树后序:
[postL, postL + leftSize - 1] - 右子树后序:
[postL + leftSize, postR - 1]
这两个公式我建议你推导验证一遍,而不是死记硬背。理解了“后序最后一个元素是根、左子树节点数是 leftSize”之后,这些下标就再也难不倒你了。
5.2 左右子树区间错一位
另一个高发问题是区间划分“一指禅”,明明知道大致方向,但左右端点总是差 1。我见过不少学员把右子树的左端点写成postL + leftSize + 1,把左子树的右端点写成postL + leftSize,结果建出来的树结构完全变形。
做一个简单的代入检查:假设当前树只有左孩子,没有右孩子,那么leftSize应该等于postR - postL(右子树区间长度为 0)。此时右子树区间应该是[postL + leftSize, postR - 1],也就是[postR, postR - 1],说明区间为空,递归返回空指针,完全正确。如果你把右子树左端点写成postL + leftSize + 1,那就变成[postR + 1, postR - 1],虽然递归不会访问,但语义上已经有点别扭;更可怕的是当右子树非空时会越界。
这里给你一个自测技巧:每次写好递归函数,先套几个不同的用例手动跑一遍,确认所有区间的左右端点都满足L <= R。如果某个区间出现L > R,说明你的切分公式有问题。
5.3 多写了一个 swap
有同学为了“保险起见”,在建树完后又调用两次mirror,想着“多交换一次应该没关系”。实际上镜像操作是幂等操作:执行两次等于不执行,输出结果比预期少了镜像效果,答案错误没商量。
为什么幂等?因为每执行一次mirror,所有节点的左右孩子都交换一次;执行第二次时,又被交换回来。这跟乘两次 -1 的道理一模一样。所以你要么只调用一次mirror,要么干脆不调用、直接在层序输出时先右后左。千万不要画蛇添足。
还有一个相关的坑是:有同学在mirror函数里写了“先递归左子树、交换、再递归右子树”,这种先交换后递归和先递归后交换有什么区别?单独跑一次结果一样,因为每个节点最终都会被交换一次。但如果你在交换之前就把左右子树递归处理了,之后再交换,实际上你交换的是已经处理完的左右子树,没问题;如果交换之后再递归,交换后左右子树互换了,递归处理的对象也变了,结果依然对。两种写法都正确,但新手常搞混,所以我建议统一用“先交换再递归”的写法,逻辑更直观。
5.4 常见问题速查表
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 运行报段错误 | 递归区间越界或空指针访问 | 检查中序定位和后序区间切分,确保递归返回 nullptr 的条件正确 |
| 输出结果多一行 | 判断空树后额外输出换行 | 只有真正有节点时才输出,或在输出函数开头处理空指针 |
| 输出结果全反了 | 层序遍历时先入队了反方向子树 | 确认镜像后层序是从左到右,对应入队顺序为左孩子再右孩子 |
| 输出缺数字 | 递归边界少返回 nullptr | 建树函数必须覆盖inL > inR的空区间情况 |
| 题目样例过了但提交不过 | 数组开小或局部变量未初始化 | 固定数组开到题目范围以上,尽量用vector动态分配 |
这份速查表里的每一条都是我用真实报错换来的经验,尤其是“输出结果全反了”这条,初学者遇到得最多,因为没有语法错误、没有运行时错误,就是逻辑和你心里预期不一致,只能靠经验定位。
6. 从这道题延伸出去:深度、搜索二叉树与更多变式
6.1 二叉树的深度在题目里怎么用
“二叉树的深度”是二叉树的经典考点,经常跟这道题一起出现在各种比赛的热搜词里。那道经典的题目是:给定一棵树,求从根节点到最远叶子节点的最长路径上的节点数。解法可以是递归计算左子树深度和右子树深度,取较大者加 1。L2-011 虽然没让你求深度,但如果你能顺手在build函数里返回子树深度,就能一道题同时练到建树和深度计算。
求深度和建树有一个共通点:都是对树做递归遍历,只是返回值不同。你可以在建树之后写一个递归函数int getDepth(Node* root),遇到空节点返回 0,否则返回max(getDepth(root->left), getDepth(root->right)) + 1。这是二叉树所有递归问题的“母题”,后面大量的路径和、直径、最近公共祖先问题都会用到这种返回值式递归。
所以我的建议是:刷完 L2-011 后立刻做一道求深度、再做一道求节点数的题,把三种递归模式(无返回值遍历、返回深度、返回节点数)放到一起对比理解,二叉树的基本功才算夯实。
6.2 搜索二叉树和这道题的关系
热搜词里还有一个“搜索二叉树”,也被称为二叉排序树。搜索二叉树有一个关键性质:中序遍历结果是递增有序的。为什么呢?因为对于任意一个节点,它的左子树所有节点值都小于它,右子树所有节点值都大于它,而中序遍历的顺序恰恰是先左后根再右,所以整个序列自然排好了序。
如果你把题目里给的“中序序列”想象成一个有序序列,那么配合后序,你就可以还原出一棵搜索二叉树。这在实际工程和面试里很常见:给你一棵搜索二叉树的遍历序列,让你重构树,本质上就是上面递归建树的过程,只不过可以利用有序性质更快地定位根节点位置。
一个常见变式是:给定一棵搜索二叉树的先序遍历,让你还原这棵树。你可以利用搜索二叉树的性质,把先序序列第一个元素作为根,然后在剩下的序列里找到第一个大于根的元素位置,左边是左子树,右边是右子树。这种变式考的不是单纯的建树模板,而是你对搜索二叉树性质的深层理解。
6.3 变式题汇总
刷题讲究“一道题带一片”。围绕 L2-011 我帮你把相关变式题画个谱系:
- 中序+先序重建二叉树:思路完全对称,先序第一个元素是根,然后去中序里定位。
- 中序+后序重建镜像树:就是本题,只是可以换用不同策略。
- 层序+中序重建二叉树:层序的第一个元素是根,剩下的元素根据中序划分到左右子树集合,递归继续。
- 求任意两节点的最近公共祖先:建好树后,用递归查找两个节点在左右子树中的分布情况。
- 判断两棵二叉树是否互为镜像:递归比较一树的左孩子和另一树的右孩子。
- 输出二叉树的右视图:层序遍历,取每层最后一个节点。
这堆题看起来五花八门,但核心能力都是一样的:对树的遍历有透彻理解。我个人认为 L2-011 之所以叫“玩转二叉树”,就是因为它要求你在建的树、遍历序列、镜像变换之间反复横跳,玩明白了,以上这些题自然就通了。
有个很实用的学法是:把你手头题库里所有跟二叉树遍历相关的题集中起来,在两周内连续刷完,每天至少手写一遍建树递归。这种刻意练习比一次性刷十道同类题效果更好,因为间隔能让你反复记住递归边界和区间划分。等你做到闭着眼睛都能写出build函数的时候,L2 的二叉树题对你来说就只是一层窗户纸了。
最后说句实在话,建树递归第一次写不出来太正常了,我当年也是对着网上的代码一行一行抄,抄熟了才明白那些下标为什么这样切。关键是你别停在“抄”这一步,合上屏幕自己推演一遍、画一遍、写一遍,这个过程比看十篇解析都管用。等你亲手把1 2 3 4 5 6 7和2 3 1 5 7 6 4玩明白,这道题你就算真正吃透了。