考 408 的同学大概率都有过这种体验:一道选择题,四个选项看起来都对,或者看起来都错,最后蒙了一个,答案还真的就是那个“看起来最像答案”的。2011 年统考数据结构第 5 题就是这种题。题干很简短——给出二叉树的先序遍历序列,要求判断哪个中序遍历序列不可能出现。很多人第一次做这道题时会很困惑:先序和中序不是能唯一确定一棵二叉树吗?那为什么还会出现“不可能”的选项?难道任意给出一个中序序列,都能构造出一棵满足先序的树?
这里先给出一个明确判断:这道题的核心不是“背遍历规则”,而是“理解遍历序列之间的约束关系”。先序序列确定根,中序序列确定左右子树的划分,两者放在一起,本质上是对一棵二叉树做“双重描述”。如果两个序列互相矛盾,就无法还原出任何一棵树。2011 年第 5 题考的就是这个矛盾的检测能力。
读完这篇文章,你会得到三样东西:第一,一个 30 秒内手算排除错误选项的三步法;第二,一份可以直接运行的 C/Python 代码,用来判断任意“先序+中序”组合是否合法;第三,在合法前提下重建二叉树、输出后序序列的完整实现。这套能力不仅对这道真题有效,对以后遇到“后序+中序求先序”“层次+中序恢复二叉树”这一类题同样适用。
1. 2011年第5题到底在考什么
这道题出现在 2011 年 408 统考数据结构部分,题目本身并不长,但错误率一直不低。原因是很多同学对二叉树遍历的理解停留在“能写出递归代码”这个层面,却没有理解一个更根本的问题:当两个遍历序列同时给出时,它们之间有哪些必须满足的约束?
先回顾一下三种遍历的定义。先序遍历的顺序是“根、左、右”,中序遍历的顺序是“左、根、右”,后序遍历的顺序是“左、右、根”。注意,这里的“根”指的是当前子树的根,不是整棵树的根。对于一棵树中的任意一个子树,这三个顺序都成立。
| 遍历方式 | 访问顺序 | 核心作用 |
|---|---|---|
| 先序遍历 | 根、左、右 | 第一个元素必为整棵树的根 |
| 中序遍历 | 左、根、右 | 根的位置把左右子树结点分开 |
| 后序遍历 | 左、右、根 | 最后一个元素必为整棵树的根 |
| 层次遍历 | 从上到下、从左到右 | 可配合中序恢复二叉树 |
2011 年第 5 题的考法,就是给定先序序列,让考生在四个中序序列中找出“不可能”的那个。为什么会有“不可能”?因为先序序列限定了根的位置,而中序序列限定了左右子树的结点集合,两者一旦冲突,就没有任何一棵二叉树能同时满足这两个序列。
从 408 的命题风格来看,这道题真正想考查的是“根据两种遍历序列恢复二叉树”的逆向能力。先序+中序可以唯一确定一棵二叉树,这是教材结论,但这个结论成立的前提是“两个序列来自同一棵树”。题目就是把这个前提拿掉,让学生判断“给定的两个序列是否真的来自同一棵树”。换句话说,它把正向的“给树求序列”变成了逆向的“给序列验树”。
这里要特别强调一个判断:刷这道题时,如果只记住某个选项是答案,那这道题就白刷了。因为 408 对遍历序列的考查方式一直在变化,但核心逻辑不变。真正要掌握的是“先序定根、中序分割、递归验证”这套通用方法。掌握了它,不管选项怎么变,你都能在几十秒内完成判断。
2. 核心原理:先序+中序为什么能唯一确定一棵二叉树
要理解“哪个中序不可能”,首先要理解“先序+中序为什么能确定一棵二叉树”。这两个序列之间的关系可以用三个关键观察来概括。
第一个观察:先序序列的第一个元素一定是整棵树的根结点。因为先序遍历最先访问的就是根,这是定义,没有任何例外。
第二个观察:得到根结点之后,去中序序列里找到这个根,根左边的所有结点一定属于左子树,根右边的所有结点一定属于右子树。中序遍历的顺序是“左、根、右”,所以根天然地把结点分成左右两个部分。
第三个观察也是最重要的:左子树和右子树的结点,在先序序列中也必须是连续的两段。先序遍历的顺序是“根、左、右”,根访问完之后,紧接着访问整棵左子树,左子树访问完才访问右子树。因此,如果中序序列告诉我们左子树有 L 个结点,那么先序序列中紧随根之后的 L 个结点,必须全部属于左子树集合,再往后的结点必须全部属于右子树集合。
这三个观察合在一起,就构成了“先序+中序唯一确定二叉树”的证明过程,同时也是判断“序列是否合法”的判定规则。用一个小例子演示这个过程。
假设先序序列是 ABDCE,中序序列是 DBAEC。先取先序第一个元素 A 作为根,在中序 DBAEC 中找到 A,位置在中间偏右。A 左边是 DB,说明左子树集合是 {D, B};A 右边是 EC,说明右子树集合是 {E, C}。
再看先序序列 ABDCE。根 A 之后是 B、D、C、E 这 4 个结点。左子树有 2 个结点,所以先序中 A 之后的 2 个结点 B、D 必须属于左子树集合;确实 {B, D} = {D, B}。剩下 C、E 属于右子树集合;确实 {C, E} = {E, C}。第一层验证通过。
接下来递归验证左子树。左子树的先序片段是 BD,中序片段是 DB。根是 B,中序中 B 的左边是 D,说明左子树的左子树是 {D},右子树为空。再看左子树先序 BD,根 B 之后是 D,正好属于左子树集合。验证通过。右子树同理,先序 CE,中序 EC,根 C,左子树 E,验证通过。因此这两个序列确实来自同一棵树。
这里的关键在于:每一次分割后,都要用“左子树结点数”去切割先序序列。左子树有多少个结点,根后面的前多少个位置就必须是左子树的先序片段。这就是“约束”二字的真正含义。
如果某个中序序列给出的左子树集合和先序序列中紧随根后的结点集合对不上,那就说明这个中序序列不可能是该先序序列对应的任何一棵二叉树的中序序列。这就是 2011 年第 5 题所有“不可能”选项的根源。
3. 手算解法:30秒排除不可能的“三步法”
选择题不需要真的把整棵树完整还原。绝大多数选项,在第一层验证时就能排除。这里给出一个适合考场上使用的手算三步法。
3.1 三步法总览
第一步,确定根。先序序列的第一个元素就是当前子树的根。
第二步,在中序序列中找到根的位置。根左边有多少个结点,就说明左子树有多少个结点;根右边有多少个结点,就说明右子树有多少个结点。
第三步,用左子树结点数去切割先序序列。先序序列中,根后面的前 L 个结点必须全部来自中序中根左边的集合,后 n-L-1 个结点必须全部来自中序中根右边的集合。如果匹配,对左子树和右子树继续递归验证;如果不匹配,直接判定不可能。
这个方法的本质,就是把“先序+中序唯一确定二叉树”的证明过程反着用。每一层只需要做一件事:检查“先序中紧跟根的那一段”和“中序中根左右两段”是否集合一致。
3.2 手算实例:先序 ABCDEF,中序 CABDEF
用先序 ABCDEF 和中序 CABDEF 走一遍完整流程。
先序第一个元素是 A,A 就是整棵树的根。在中序 CABDEF 中找到 A,位置在第 2 个(下标从 1 开始)。A 的左边只有一个结点 C,说明左子树集合是 {C},左子树大小为 1。A 的右边是 B、D、E、F,说明右子树集合是 {B, D, E, F}。
现在用左子树大小 1 去切割先序。先序是 A、B、C、D、E、F,根 A 之后应该是 1 个左子树结点,也就是 B。可是 B 并不在左子树集合 {C} 中,而是属于右子树集合。这就矛盾了:如果 B 是 A 的左孩子,它应该出现在中序 A 的左边,但中序 A 的左边只有 C;如果 B 不是左孩子,那左子树大小就不是 1,而先序中 A 后紧接着就是 B,这与“先序先遍历左子树”矛盾。
所以 CABDEF 不可能成为先序 ABCDEF 对应二叉树的中序序列。整个过程只需要十几秒,不需要画树。
3.3 容易忽略的递归验证
三步法中,第三步看起来只是集合判断,但要注意:集合判断只是必要条件,不是充分条件。第一层通过后,左右子树内部仍然可能存在矛盾。
举例来说,先序 ABCDEF,中序 CBADEF。第一层检查:根 A,中序中 A 左边是 CB,左子树集合 {C, B},左子树大小 2;先序 A 后是 B、C,恰好都属于左子树集合;右边 D、E、F 都属于右子树集合。第一层通过。
但如果就此判定合法,是不够严谨的。还要继续看左子树:左子树先序片段是 BC,中序片段是 CB。根 B,中序中 B 的左边是 C,左子树集合 {C},左子树大小 1;再看先序 BC,根 B 后是 C,C 确实在左子树集合中,验证通过。右子树先序 DEF,中序 DEF,根 D,右子树 EF,同样通过。因此 CBADEF 确实合法。
在选择题里,出题人经常会把干扰项设计成“第一层看起来没问题,但子树内部有问题”的形式。所以手算时至少要做到第二层、第三层的递归验证,不能只验证根这一层就下结论。
4. 完整示例:先序 ABCDEF 的四种中序,谁不可能
下面用一组典型的选项设计来演示完整判断过程。给定先序序列 ABCDEF,有四个中序序列候选:
- A. A B C D E F
- B. B A C D E F
- C. C B A D E F
- D. C A B D E F
用三步法逐个验证。
先看选项 A,中序 ABCDEF。根是 A,中序中 A 在最左边,说明左子树为空,右子树结点集合是 {B, C, D, E, F}。先序中 A 后面的 B C D E F 全部属于右子树集合,长度也正确。进一步检查右子树,先序 BCDEF,中序 BCDEF,根 B,同样结构。这说明整棵树是一条只向右延伸的链:A 的右孩子是 B,B 的右孩子是 C,以此类推。选项 A 合法。
再看选项 B,中序 BACDEF。根 A 在中序中位于第 2 位,左边只有 B,左子树集合 {B},右子树集合 {C, D, E, F}。先序中 A 后第一个是 B,确实属于左子树集合,且左子树大小为 1;剩下的 C D E F 属于右子树集合。验证通过。对应树的结构是:A 的左孩子是 B,A 的右子树是 C-D-E-F 这条右链。选项 B 合法。
接着看选项 C,中序 CBADEF。根 A 左边是 CB,左子树集合 {C, B},大小为 2;右边是 DEF,右子树集合 {D, E, F}。先序中 A 后是 B、C,这两个正好属于左子树集合,长度 2;后面的 D、E、F 属于右子树集合。第一层通过。再看左子树,先序 BC,中序 CB,根 B,左子树为 C,可以匹配。选项 C 合法。
最后看选项 D,中序 CABDEF。根 A 左边只有一个 C,左子树集合是 {C},左子树大小为 1;右边是 B、D、E、F。先序中 A 后第一个结点是 B,但 B 并不在左子树集合 {C} 中,而是属于右子树集合。和 3.2 节的分析一样,B 的位置无法安放。因此选项 D 是中序序列不可能出现的情况。
这道题选 D。
选项 D 是很好的干扰项,因为它看起来“很有中序的感觉”:A 在中间,左右各有一堆结点。但正是这种表面上的“合理”让人忽略了先序和中序之间的连续段约束。再换一个角度理解:如果把先序序列看成入栈顺序,把中序序列看成出栈顺序,那么 CABDEF 这个出栈顺序在栈模拟中会在第二个位置卡住。A 入栈后,B 入栈,此时 B 压在 A 上面;如果第一个出栈的是 C,那么必须先把 A、B 都压进去,C 出栈后栈顶是 B,但中序第二个元素是 A,而 A 被 B 压住,无法立即出栈。矛盾由此产生。
这个栈的视角非常重要,后面写代码时,最简洁的合法性判断就是基于这个思路实现的。
5. 代码实现:用栈模拟判断中序序列是否合法
选择题用手算三步法已经足够。但如果想彻底检验自己的理解,或者为复试机试做准备,就应该把判断逻辑写成代码。
5.1 算法思路:先序是入栈序,中序是出栈序
非递归中序遍历二叉树时会用到栈。中序遍历的访问顺序,正好对应着一种“按先序入栈、按中序出栈”的模拟过程:先序序列决定结点什么时候入栈,中序序列决定结点什么时候出栈。
算法过程如下:
- 维护一个栈,初始为空。
- 用指针 i 指向先序序列,表示下一个待入栈的结点。
- 从左到右扫描中序序列的每个字符 c:
- 当栈为空,或者栈顶元素不等于 c 时,不断从先序序列中取元素入栈,直到栈顶元素等于 c,或者先序序列已经全部入栈。
- 如果先序序列已经用尽,栈顶仍然不等于 c,说明这个中序序列不可能出现,返回 false。
- 如果栈顶等于 c,弹出栈顶,继续处理中序下一个字符。
- 如果中序序列扫描完,说明所有字符都正确匹配,返回 true。
每个字符最多入栈一次、出栈一次,所以时间复杂度是 O(n),其中 n 是序列长度。
5.2 C 语言完整实现
#include <stdio.h> #include <string.h> #include <stdbool.h> #define MAXN 100 // 判断中序序列 in 是否可能是先序序列 pre 对应二叉树的中序遍历 bool isValidInorder(const char *pre, const char *in, int n) { char stack[MAXN]; int top = -1; int i = 0; // pre 序列的指针 for (int j = 0; j < n; j++) { // 栈空或栈顶不等于当前中序字符时,继续入栈 while (top == -1 || stack[top] != in[j]) { if (i >= n) { return false; // 先序元素全部入栈仍无法匹配 } stack[++top] = pre[i++]; } // 匹配成功,出栈 top--; } return true; } int main() { char pre[MAXN], in[MAXN]; printf("请输入先序遍历序列: "); scanf("%s", pre); printf("请输入中序遍历序列: "); scanf("%s", in); int n = strlen(pre); if (strlen(in) != n) { printf("两个序列长度不一致,输入错误\n"); return 0; } if (isValidInorder(pre, in, n)) { printf("该中序序列合法,对应二叉树存在\n"); } else { printf("该中序序列不可能由该先序序列对应的二叉树产生\n"); } return 0; }核心逻辑集中在isValidInorder中。stack[++top] = pre[i++]这一步把先序序列中的元素依次压栈,直到栈顶能匹配当前要处理的中序字符。如果先序序列已经全部压入栈中还没有匹配成功,说明栈里剩余元素的顺序和中序序列冲突,该中序序列不合法。
编译和运行命令如下:
gcc btree_inorder_check.c -o btree_inorder_check ./btree_inorder_check输入一组合法序列,例如先序 ABCDEF、中序 CBADEF,程序输出:
请输入先序遍历序列: ABCDEF 请输入中序遍历序列: CBADEF 该中序序列合法,对应二叉树存在输入一组非法序列,例如先序 ABCDEF、中序 CABDEF,程序输出:
请输入先序遍历序列: ABCDEF 请输入中序遍历序列: CABDEF 该中序序列不可能由该先序序列对应的二叉树产生5.3 Python 精简版
如果平时用 Python 刷题,可以用下面这个更精简的版本:
def is_valid_inorder(pre: str, in_order: str) -> bool: stack = [] i = 0 for ch in in_order: while not stack or stack[-1] != ch: if i >= len(pre): return False stack.append(pre[i]) i += 1 stack.pop() return True if __name__ == "__main__": pre = input("请输入先序遍历序列: ").strip() in_order = input("请输入中序遍历序列: ").strip() if len(pre) != len(in_order): print("两个序列长度不一致") elif is_valid_inorder(pre, in_order): print("该中序序列合法,对应二叉树存在") else: print("该中序序列不可能由该先序序列对应的二叉树产生")Python 版本的逻辑和 C 版本完全一致。这里要注意一个容易出错的点:while not stack or stack[-1] != ch这个条件中,必须先判断栈是否为空,再判断栈顶,顺序不能写反,否则空栈访问stack[-1]会报 IndexError。
机试时,这个栈模拟函数可以作为判断“遍历序列是否配对”的通用工具,也可以进一步用于重建二叉树。
6. 进阶:合法时重建二叉树并验证后序
判断合法性只是第一步。408 的大题和复试机试里经常要求“给定先序和中序,重建二叉树并输出后序”。这个需求可以在合法性的基础上直接扩展。
6.1 递归重建的核心逻辑
构建过程和前面手算三步法一模一样:
- 先序片段的第一个元素是根。
- 在中序片段中找到根的位置 pos。
- 中序片段中,pos 左边是左子树中序,pos 右边是右子树中序。
- 左子树结点数 leftLen = pos - inL,用它把先序片段切成三部分:根、左子树先序、右子树先序。
- 递归构建左右子树。
边界条件很简单:当先序片段的左边界大于等于右边界时,说明没有结点,返回 NULL。
6.2 C 语言重建并输出后序完整代码
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdbool.h> #define MAXN 100 typedef struct TreeNode { char val; struct TreeNode *left; struct TreeNode *right; } TreeNode; // 判断中序序列是否可能 bool isValidInorder(const char *pre, const char *in, int n) { char stack[MAXN]; int top = -1; int i = 0; for (int j = 0; j < n; j++) { while (top == -1 || stack[top] != in[j]) { if (i >= n) return false; stack[++top] = pre[i++]; } top--; } return true; } // 根据先序和中序重建二叉树 TreeNode *buildTree(const char *pre, int preL, int preR, const char *in, int inL, int inR) { if (preL >= preR) { return NULL; } TreeNode *node = (TreeNode *)malloc(sizeof(TreeNode)); node->val = pre[preL]; // 在中序片段中查找根的位置 int pos = inL; while (pos < inR && in[pos] != pre[preL]) { pos++; } int leftLen = pos - inL; // 左子树结点数 // 左子树:先序 [preL+1, preL+1+leftLen),中序 [inL, pos) node->left = buildTree(pre, preL + 1, preL + 1 + leftLen, in, inL, pos); // 右子树:先序 [preL+1+leftLen, preR),中序 [pos+1, inR) node->right = buildTree(pre, preL + 1 + leftLen, preR, in, pos + 1, inR); return node; } void postorder(TreeNode *root) { if (root == NULL) return; postorder(root->left); postorder(root->right); printf("%c", root->val); } int main() { char pre[MAXN], in[MAXN]; printf("请输入先序遍历序列: "); scanf("%s", pre); printf("请输入中序遍历序列: "); scanf("%s", in); int n = strlen(pre); if (strlen(in) != n) { printf("两个序列长度不一致,输入错误\n"); return 0; } if (!isValidInorder(pre, in, n)) { printf("该中序序列不可能由该先序序列对应的二叉树产生\n"); } else { printf("该中序序列合法\n"); TreeNode *root = buildTree(pre, 0, n, in, 0, n); printf("重建成功,后序遍历序列为: "); postorder(root); printf("\n"); } return 0; }这段代码把“判断”和“重建”放在了一起。运行示例:
请输入先序遍历序列: ABCDEF 请输入中序遍历序列: CBADEF 该中序序列合法 重建成功,后序遍历序列为: CBFEDA可以用前面的手算过程验证这个后序结果。先序 ABCDEF、中序 CBADEF 对应这棵树:
- A 是根。
- 左子树先序 BC,中序 CB,根 B,左孩子 C。
- 右子树先序 DEF,中序 DEF,根 D,右孩子 E,E 的右孩子 F。
后序遍历顺序是“左、右、根”:C(左子树最左叶子)、B(左子树根)、F(右子树最右叶子)、E、D、A,也就是 CBFEDA。和程序输出一致。
如果 n 很大,递归中用循环找根的位置是 O(n) 的,整体会退化到 O(n^2)。面试或机试中更稳妥的做法是先用哈希表记录中序序列中每个字符的下标,把查找根的位置优化到 O(1)。哈希优化版本需要注意一个前提:二叉树中所有结点值互不相同。如果存在重复值,哈希表会丢失信息,这也是 408 题目默认的前提条件。
7. 常见错误与排查思路
代码和手算都容易踩坑。下面把出现频率较高的几个问题整理出来。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 手算认为合法,程序判断非法 | 两个序列的参数顺序传反,或输入时有空格 | 打印 pre 和 in,确认谁是谁 | 统一按“先序在前、中序在后”调用 |
| 元素集合相同但长度一致,仍判断非法 | 序列中存在重复字符 | 逐字符检查,重复值会导致栈模拟歧义 | 408 默认结点值互异;如果确实重复,需要带编号处理 |
| 递归重建时栈溢出 | 二叉树退化成一条链,递归深度等于结点数 | 输出树高或检查输入序列是否来自极端树形 | 机试可改用迭代栈模拟;笔试直接说明递归思路 |
| 只验证根一层就下结论 | 左子树或右子树内部仍然存在矛盾 | 对左右子树递归执行三步法 | 至少递归到第二层,必要时画树验证 |
| 哈希优化后结果错误 | 中序存在重复字符,哈希表覆盖了位置信息 | 打印哈希表内容检查 | 有重复值时不能直接用普通哈希表 |
其中最常见的问题是把“集合相等”当作“序列合法”。集合相等只是必要条件,递归结构也必须一致。比如先序 ABC、中序 CAB,根 A 左边是 C,右边是 B,左子树集合 {C};但先序中 A 后第一个是 B,B 不在左子树集合中,所以非法。这里的矛盾发生在第一层,很容易看出来。但有些题目会把矛盾藏在子树内部,第一层集合完全匹配,到第二层才暴露。做题时如果遇到两个选项第一层都通过,必须继续往下验证子树,这就是递归思想的实际应用。
另一个高发问题是递归重建时边界写错。buildTree函数里,左子树的先序范围是[preL+1, preL+1+leftLen),右子树的范围是[preL+1+leftLen, preR)。很多同学会把右子树的起点写成preL+leftLen,少加一个 1,导致跳过根结点。判断边界时,可以打印每次递归的参数来核对。这个错误在代码中很难一眼发现,但一旦输出后序结果完全错乱,基本就是边界问题。
8. 408复习建议与扩展考点
2011 年第 5 题属于“遍历序列恢复二叉树”这一大考点。这类题目在 408 中反复出现,而且换汤不换药。下面把这些考点串起来复习,效率会高很多。
8.1 不同序列组合能否唯一确定二叉树
| 给定序列 | 能否唯一确定二叉树 | 说明 |
|---|---|---|
| 先序 + 中序 | 能 | 先序定根,中序分左右 |
| 后序 + 中序 | 能 | 后序定根,中序分左右 |
| 层次 + 中序 | 能 | 层次序定根顺序,中序分左右 |
| 先序 + 后序 | 不能 | 单孩子结点的左右方向无法区分 |
| 先序 + 空指针标记 | 能 | 空指针标记补全了左右子树信息 |
“先序 + 后序不能唯一确定”这个结论,可以用最简反例说明:两个结点的树,根 A 的左孩子是 B,和根 A 的右孩子是 B,这两种树的先序序列都是 AB,后序序列都是 BA,但中序一个是 BA,一个是 AB,是两棵不同的二叉树。所以考试中只要出现“先序 + 后序还原二叉树”的说法,可以直接判断为错误或需要额外条件。
8.2 与本题相似的考法
第一类:给后序 + 中序,求先序。做法和先序 + 中序完全对称,只是要把“先序第一个元素是根”换成“后序最后一个元素是根”,然后同样用中序切割左右子树。
第二类:给先序 + 后序,问中序有多少种可能。这种题考查的是对“单孩子结点”的理解。每当一个结点只有一个孩子时,这个孩子在先序和后序中的相对位置体现不出左右,对应中序就多一种可能。
第三类:在二叉排序树背景下考查遍历。二叉排序树的中序遍历一定是递增序列,所以“二叉排序树先序 + 中序”的组合里,中序其实已经隐含了结点之间的顺序关系,这类题通常和查找、插入结合。
第四类:把遍历和栈结合。中序遍历的非递归实现依赖栈,所以“先序为入栈顺序、中序为出栈顺序”这个模型本身就是考点。掌握了栈模拟判断法,这类题基本是送分题。
8.3 复习建议
给四个可落地的建议:
第一,先把三种遍历的手算过关,包括递归版本和非递归版本。不要只背代码,要能在纸上快速写出任意一棵树的先序、中序、后序。
第二,做“遍历序列恢复二叉树”的专项练习,每天手算三题。题目不用很难,重点是训练“先序定根、中序分割、左子树长度切先序”这三个动作,直到形成条件反射。
第三,机试必练三件事:栈模拟判断合法性、递归重建二叉树、输出另一种遍历验证结果。这三个能力可以互相验证,也是复试机试的高频考点。
第四,错题本里不要只抄原题答案。把“通用解法”写下来,比如 2011 年第 5 题的通用解法就是“三步法 + 递归验证”,下次遇到同类题直接调用这套思路,而不是回忆上次选的是 A 还是 D。
9. 总结与动手练习
这篇文章把 2011 年第 5 题背后的逻辑拆成了三层:原理层,理解先序 + 中序为什么能唯一确定一棵二叉树;手算层,用“三步法”快速判断中序序列是否合法;代码层,用栈模拟判断合法性,用递归重建二叉树并输出后序。
其中最关键的一个认知是:两个序列来自同一棵树时,先序序列中紧随根之后的左子树结点段,长度必须等于中序序列中根左边结点的数量,且这两个集合必须一致。这个约束条件既是证明“唯一确定”的依据,也是判断“不可能”的武器。
下面留三道练习题,建议先手算,再用代码验证,最后在评论区交流答案。
- 已知先序序列为 ABCDEF,中序序列为 CDBAEF,这个中序序列是否合法?如果合法,后序序列是什么?
- 已知后序序列为 DEBFCA,中序序列为 DBEAFC,求先序序列。
- 为什么“先序 + 后序”不能唯一确定一棵二叉树?请画出一个具体反例。
把 2011 年第 5 题吃透之后,你会明显感觉到,408 数据结构中对二叉树遍历的考查,表面上是选择题,本质上是让你在脑子里维护一棵树。当你面对任意一组先序和中序序列,能在几十秒内判断出“这棵树到底存不存在”时,这一类题就已经真正通了。