欢迎光临
我们一直在努力

【面试高频题】二叉树的重建:由前序+中序遍历和后序+中序遍历重建二叉树

文字目录

  • 一、问题背景与核心思想
    • 1.1 问题描述
    • 1.2 核心原理
  • 二、由前序 + 中序还原二叉树
    • 2.1 原理分析
    • 2.2 递归实现
    • 2.3 执行过程追踪
    • 2.4 非递归实现(迭代 + 栈)
    • 2.5 执行过程追踪
  • 三、由后序 + 中序还原二叉树
    • 3.1 原理分析
    • 3.2 递归实现
    • 3.3 执行过程追踪
    • 3.4 非递归实现(迭代 + 栈)
    • 3.5 执行过程追踪
  • 四、两种方法对比
    • 4.1 切割规则对比
    • 4.2 序列切割示意图
  • 五、算法复杂度分析
    • 5.1 时间复杂度
    • 5.2 空间复杂度
    • 5.3 递归 vs 非递归
  • 六、完整可运行代码

一、问题背景与核心思想

1.1 问题描述

给定二叉树的两种遍历序列,还原(重建)出唯一的二叉树结构。

本文讨论两种经典组合:

题目已知条件能否唯一确定二叉树
题目一 前序遍历 + 中序遍历 ✅ 能
题目二 后序遍历 + 中序遍历 ✅ 能
参考 前序遍历 + 后序遍历 ❌ 不能(无法确定左右子树边界)

⚠️ 前提条件:树中所有节点的值各不相同,否则无法唯一定位根节点在中序序列中的位置。

1.2 核心原理

三种遍历方式的特征:

前序遍历:[ 根 | 左子树序列 | 右子树序列 ]
中序遍历:[ 左子树序列 | 根 | 右子树序列 ]
后序遍历:[ 左子树序列 | 右子树序列 | 根 ]

关键结论:

  • 前序序列的第一个元素 = 当前树的根节点
  • 后序序列的最后一个元素 = 当前树的根节点
  • 根节点在中序序列中的位置 = 左子树与右子树的分割线

只要知道根节点,就能在中序序列中把左右子树分开;知道左右子树的节点数量,就能在前/后序序列中切出对应的子序列;然后递归重建。


二、由前序 + 中序还原二叉树

2.1 原理分析

以下面的例子为例:

前序遍历:[ 3, 9, 20, 15, 7 ]
中序遍历:[ 9, 3, 15, 20, 7 ]

第一步:确定根节点

前序序列首元素 = 3,即根节点为 3。

前序:[ 3 | 9, 20, 15, 7 ]

根节点

第二步:在中序序列中定位根节点

在中序序列中找到 3 的位置(下标 = 1):

中序:[ 9 | 3 | 15, 20, 7 ]

根节点
左子树长度=1 右子树长度=3

第三步:切割前序序列

已知左子树长度 = 1,右子树长度 = 3:

前序:[ 3 | 9 | 20, 15, 7 ]
←1→ ←—3—→
左子 右子树序列

第四步:递归重建

子问题前序序列中序序列
左子树 [9] [9]
右子树 [20, 15, 7] [15, 20, 7]

对右子树继续递归:

  • 前序 [20, 15, 7] → 根 = 20
  • 中序 [15, 20, 7] → 20 在位置 1 → 左子树 [15],右子树 [7]

最终还原结果:

3
/ \\
9 20
/ \\
15 7

2.2 递归实现

#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;

struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

class Solution_PreIn {
private:
// 存储中序序列中每个值的下标,查询 O(1)
unordered_map<int, int> inorderIndex;

/**
* 递归重建
* preorder : 前序序列
* preLeft : 当前子树在前序中的起始下标
* preRight : 当前子树在前序中的结束下标
* inLeft : 当前子树在中序中的起始下标
* inRight : 当前子树在中序中的结束下标
*/

TreeNode* build(const vector<int>& preorder,
int preLeft, int preRight,
int inLeft, int inRight) {
// 递归终止:子序列为空
if (preLeft > preRight) return nullptr;

// 1. 前序首元素即为当前根节点
int rootVal = preorder[preLeft];
TreeNode* root = new TreeNode(rootVal);

// 2. 在中序序列中定位根节点
int inRootIdx = inorderIndex[rootVal];

// 3. 计算左子树节点数量
int leftSize = inRootIdx inLeft;

// 4. 递归重建左子树
// 前序左子树范围:[preLeft+1, preLeft+leftSize]
// 中序左子树范围:[inLeft, inRootIdx-1]
root->left = build(preorder,
preLeft + 1, preLeft + leftSize,
inLeft, inRootIdx 1);

// 5. 递归重建右子树
// 前序右子树范围:[preLeft+leftSize+1, preRight]
// 中序右子树范围:[inRootIdx+1, inRight]
root->right = build(preorder,
preLeft + leftSize + 1, preRight,
inRootIdx + 1, inRight);

return root;
}

public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
int n = preorder.size();
// 预处理:建立中序值→下标的哈希映射
for (int i = 0; i < n; i++) {
inorderIndex[inorder[i]] = i;
}
return build(preorder, 0, n 1, 0, n 1);
}
};

2.3 执行过程追踪

以 preorder=[3,9,20,15,7],inorder=[9,3,15,20,7] 为例:

递归过程(树形展开):

build(pre[0..4], in[0..4])
rootVal=3, inRootIdx=1, leftSize=1
├─ 左子树 build(pre[1..1], in[0..0])
│ rootVal=9, inRootIdx=0, leftSize=0
│ ├─ 左子树 build(pre[2..1]) → nullptr
│ └─ 右子树 build(pre[2..1]) → nullptr
│ return Node(9)
└─ 右子树 build(pre[2..4], in[2..4])
rootVal=20, inRootIdx=3, leftSize=1
├─ 左子树 build(pre[3..3], in[2..2])
│ return Node(15)
└─ 右子树 build(pre[4..4], in[4..4])
return Node(7)
return Node(20)
return Node(3)

递归参数追踪表:

递归层级preLeftpreRightinLeftinRightrootValleftSize
第1层 0 4 0 4 3 1
第2层(左) 1 1 0 0 9 0
第2层(右) 2 4 2 4 20 1
第3层(右左) 3 3 2 2 15 0
第3层(右右) 4 4 4 4 7 0

2.4 非递归实现(迭代 + 栈)

TreeNode* buildTreeIterative_PreIn(vector<int>& preorder, vector<int>& inorder) {
if (preorder.empty()) return nullptr;

TreeNode* root = new TreeNode(preorder[0]);
stack<TreeNode*> stk;
stk.push(root);

int inIdx = 0; // 中序序列的指针

for (int i = 1; i < (int)preorder.size(); i++) {
TreeNode* node = stk.top();
TreeNode* child = new TreeNode(preorder[i]);

// 若栈顶节点值 ≠ 中序当前值,说明还在向左走
if (node->val != inorder[inIdx]) {
node->left = child; // 作为左子节点
} else {
// 回溯:弹出所有已访问完左子树的节点,直到找到右子节点的父节点
while (!stk.empty() && stk.top()->val == inorder[inIdx]) {
node = stk.top();
stk.pop();
inIdx++;
}
node->right = child; // 作为右子节点
}
stk.push(child);
}
return root;
}

2.5 执行过程追踪

以 preorder=[3,9,20,15,7],inorder=[9,3,15,20,7] 为例:

非递归迭代过程追踪:

迭代算法核心逻辑:前序顺序 = 根→左→右,遇到新节点先尝试接到栈顶的左边;若栈顶值等于中序当前值,说明左子树已完成,回溯找到右子节点的父节点。

前序:[ 3, 9, 20, 15, 7 ] 索引 i = 0,1,2,3,4
中序:[ 9, 3, 15, 20, 7 ] 指针 inIdx 从 0 开始

完整追踪(含回溯细节):

步骤i新节点值inIdxinorder[inIdx]栈顶值条件判断执行动作栈内容(底→顶)
初始 0 3(根) 0 9 创建 Node(3),push(3) [3]
1 1 9 0 9 3 3 ≠ 9 → 左走 9 接到 3.left,push(9) [3, 9]
2 2 20 0 9 9 9 == 9 → 回溯开始:pop(9),inIdx→1;栈顶=3,3==inorder[1]=3? 是,pop(3),inIdx→2;栈空,停止;最后弹出的是 3 20 接到 3.right,push(20) [20]
3 3 15 2 15 20 20 ≠ 15 → 左走 15 接到 20.left,push(15) [20, 15]
4 4 7 2 15 15 15 == 15 → 回溯开始:pop(15),inIdx→3;栈顶=20,20==inorder[3]=20? 是,pop(20),inIdx→4;栈空,停止;最后弹出的是 20 7 接到 20.right,push(7) [7]
结束 4 前序遍历完毕 []

构建结果验证:

3
/ \\
9 20
/ \\
15 7

节点挂载关系汇总:

父节点子节点方向
3 9
3 20
20 15
20 7

三、由后序 + 中序还原二叉树

3.1 原理分析

以下面的例子为例:

后序遍历:[ 9, 15, 7, 20, 3 ]
中序遍历:[ 9, 3, 15, 20, 7 ]

第一步:确定根节点

后序序列末元素 = 3,即根节点为 3。

后序:[ 9, 15, 7, 20 | 3 ]

根节点

第二步:在中序序列中定位根节点

在中序序列中找到 3 的位置(下标 = 1):

中序:[ 9 | 3 | 15, 20, 7 ]

根节点
左子树长度=1 右子树长度=3

第三步:切割后序序列

已知左子树长度 = 1,右子树长度 = 3:

后序:[ 9 | 15, 7, 20 | 3 ]
←1→ ←—3—→ 根
左子 右子树序列

第四步:递归重建

子问题后序序列中序序列
左子树 [9] [9]
右子树 [15, 7, 20] [15, 20, 7]

对右子树继续递归:

  • 后序 [15, 7, 20] → 根 = 20
  • 中序 [15, 20, 7] → 20 在位置 1 → 左子树 [15],右子树 [7]

最终还原结果:

3
/ \\
9 20
/ \\
15 7

与前序+中序还原结果完全一致 ✅

3.2 递归实现

class Solution_PostIn {
private:
unordered_map<int, int> inorderIndex;

/**
* 递归重建
* postorder : 后序序列
* postLeft : 当前子树在后序中的起始下标
* postRight : 当前子树在后序中的结束下标
* inLeft : 当前子树在中序中的起始下标
* inRight : 当前子树在中序中的结束下标
*/

TreeNode* build(const vector<int>& postorder,
int postLeft, int postRight,
int inLeft, int inRight) {
// 递归终止:子序列为空
if (postLeft > postRight) return nullptr;

// 1. 后序末元素即为当前根节点
int rootVal = postorder[postRight];
TreeNode* root = new TreeNode(rootVal);

// 2. 在中序序列中定位根节点
int inRootIdx = inorderIndex[rootVal];

// 3. 计算左子树节点数量
int leftSize = inRootIdx inLeft;

// 4. 递归重建左子树
// 后序左子树范围:[postLeft, postLeft+leftSize-1]
// 中序左子树范围:[inLeft, inRootIdx-1]
root->left = build(postorder,
postLeft, postLeft + leftSize 1,
inLeft, inRootIdx 1);

// 5. 递归重建右子树
// 后序右子树范围:[postLeft+leftSize, postRight-1]
// 中序右子树范围:[inRootIdx+1, inRight]
root->right = build(postorder,
postLeft + leftSize, postRight 1,
inRootIdx + 1, inRight);

return root;
}

public:
TreeNode* buildTree(vector<int>& postorder, vector<int>& inorder) {
int n = postorder.size();
for (int i = 0; i < n; i++) {
inorderIndex[inorder[i]] = i;
}
return build(postorder, 0, n 1, 0, n 1);
}
};

3.3 执行过程追踪

递归过程(树形展开):

build(post[0..4], in[0..4])
rootVal=3, inRootIdx=1, leftSize=1
├─ 左子树 build(post[0..0], in[0..0])
│ rootVal=9
│ return Node(9)
└─ 右子树 build(post[1..3], in[2..4])
rootVal=20, inRootIdx=3, leftSize=1
├─ 左子树 build(post[1..1], in[2..2])
│ return Node(15)
└─ 右子树 build(post[2..2], in[4..4])
return Node(7)
return Node(20)
return Node(3)

递归参数追踪表:

递归层级postLeftpostRightinLeftinRightrootValleftSize
第1层 0 4 0 4 3 1
第2层(左) 0 0 0 0 9 0
第2层(右) 1 3 2 4 20 1
第3层(右左) 1 1 2 2 15 0
第3层(右右) 2 2 4 4 7 0

3.4 非递归实现(迭代 + 栈)

后序+中序的迭代方案:将后序序列逆序处理,转化为"根→右→左"的类前序问题。

TreeNode* buildTreeIterative_PostIn(vector<int>& postorder, vector<int>& inorder) {
if (postorder.empty()) return nullptr;

int n = postorder.size();
// 后序末尾为根节点,逆序遍历后序 = 根→右→左
TreeNode* root = new TreeNode(postorder[n 1]);
stack<TreeNode*> stk;
stk.push(root);

int inIdx = n 1; // 中序指针从末尾开始

for (int i = n 2; i >= 0; i) {
TreeNode* node = stk.top();
TreeNode* child = new TreeNode(postorder[i]);

// 若栈顶节点值 ≠ 中序当前值,说明还在向右走
if (node->val != inorder[inIdx]) {
node->right = child; // 作为右子节点
} else {
// 回溯:找到左子节点应挂载的父节点
while (!stk.empty() && stk.top()->val == inorder[inIdx]) {
node = stk.top();
stk.pop();
inIdx;
}
node->left = child; // 作为左子节点
}
stk.push(child);
}
return root;
}

3.5 执行过程追踪

以 postorder=[9,15,7,20,3],inorder=[9,3,15,20,7] 为例:

非递归迭代过程追踪:

迭代算法核心逻辑:逆序遍历后序序列(等价于"根→右→左"),遇到新节点先尝试接到栈顶的右边;若栈顶值等于中序当前值(从末尾往前),说明右子树已完成,回溯找到左子节点的父节点。

后序逆序:[ 3, 20, 7, 15, 9 ] i 从 n-2=3 递减到 0
中序: [ 9, 3, 15, 20, 7 ] inIdx 从 4 开始递减

步骤i新节点值inIdxinorder[inIdx]栈顶值条件判断执行动作栈内容(底→顶)
初始 3(末) 3(根) 4 7 创建 Node(3),push(3) [3]
1 3 20 4 7 3 3 ≠ 7 → 右走 20 接到 3.right,push(20) [3, 20]
2 2 7 4 7 20 20 ≠ 7 → 右走 7 接到 20.right,push(7) [3, 20, 7]
3 1 15 4 7 7 7 == 7 → 回溯开始:pop(7),inIdx→3;栈顶=20,20inorder[3]=20? 是,pop(20),inIdx→2;栈顶=3,3inorder[2]=15? 否,停止;最后弹出的是 20 15 接到 20.left,push(15) [3, 15]
4 0 9 2 15 15 15 == 15 → 回溯开始:pop(15),inIdx→1;栈顶=3,3==inorder[1]=3? 是,pop(3),inIdx→0;栈空,停止;最后弹出的是 3 9 接到 3.left,push(9) [9]
结束 0 后序逆序遍历完毕 []

构建结果验证:

3
/ \\
9 20
/ \\
15 7

节点挂载关系汇总:

父节点子节点方向
3 20
20 7
20 15
3 9

四、两种方法对比

4.1 切割规则对比

对比项前序 + 中序后序 + 中序
根节点位置 前序序列首元素 后序序列末元素
左子树前/后序范围 [preLeft+1, preLeft+leftSize] [postLeft, postLeft+leftSize-1]
右子树前/后序范围 [preLeft+leftSize+1, preRight] [postLeft+leftSize, postRight-1]
左子树中序范围 [inLeft, inRootIdx-1] [inLeft, inRootIdx-1]
右子树中序范围 [inRootIdx+1, inRight] [inRootIdx+1, inRight]

中序范围的切割规则完全相同,两种方法的本质差异只在于如何从前/后序序列中定位根节点。

4.2 序列切割示意图

前序 + 中序:

前序:[ root | <– leftSize –> | <– rightSize –> ]
preLeft+1 preLeft+leftSize+1
中序:[ <– leftSize –> | root | <– rightSize –> ]
inLeft inRootIdx+1

后序 + 中序:

后序:[ <– leftSize –> | <– rightSize –> | root ]
postLeft postLeft+leftSize postRight
中序:[ <– leftSize –> | root | <– rightSize –> ]
inLeft inRootIdx+1


五、算法复杂度分析

5.1 时间复杂度

操作复杂度说明
哈希表构建 O(n) 遍历中序序列一次
递归重建 O(n) 每个节点恰好被创建一次
哈希查找(每次) O(1) 均摊
总体 O(n)

⚠️ 若不使用哈希表,每次在中序中线性查找根节点,时间复杂度退化为 O(n²)。

5.2 空间复杂度

空间来源复杂度说明
哈希表 O(n) 存储 n 个键值对
递归调用栈 O(h) h 为树高,最坏 O(n),平衡树 O(log n)
输出二叉树 O(n) n 个节点
总体 O(n)

5.3 递归 vs 非递归

维度递归非递归(迭代)
代码简洁性 ✅ 直观清晰 ❌ 较复杂
栈溢出风险 ❌ 深树有风险 ✅ 无风险
空间效率 O(h) 系统栈 O(h) 显式栈
推荐场景 学习理解、平衡树 生产环境、不确定树高

六、完整可运行代码

#include <iostream>
#include <vector>
#include <stack>
#include <queue>
#include <unordered_map>
using namespace std;

// ==================== 节点定义 ====================
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

// ==================== 前序 + 中序 → 递归 ====================
class BuildFromPreIn {
unordered_map<int, int> idxMap;

TreeNode* build(const vector<int>& pre, int pL, int pR, int iL, int iR) {
if (pL > pR) return nullptr;
int rootVal = pre[pL];
int inRoot = idxMap[rootVal];
int leftSize = inRoot iL;
TreeNode* root = new TreeNode(rootVal);
root->left = build(pre, pL + 1, pL + leftSize, iL, inRoot 1);
root->right = build(pre, pL + leftSize + 1, pR, inRoot + 1, iR);
return root;
}
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
for (int i = 0; i < (int)inorder.size(); i++)
idxMap[inorder[i]] = i;
return build(preorder, 0, preorder.size()1, 0, inorder.size()1);
}
};

// ==================== 前序 + 中序 → 迭代 ====================
TreeNode* buildPreIn_Iter(vector<int>& preorder, vector<int>& inorder) {
if (preorder.empty()) return nullptr;
TreeNode* root = new TreeNode(preorder[0]);
stack<TreeNode*> stk;
stk.push(root);
int inIdx = 0;
for (int i = 1; i < (int)preorder.size(); i++) {
TreeNode* node = stk.top();
TreeNode* child = new TreeNode(preorder[i]);
if (node->val != inorder[inIdx]) {
node->left = child;
} else {
while (!stk.empty() && stk.top()->val == inorder[inIdx]) {
node = stk.top(); stk.pop(); inIdx++;
}
node->right = child;
}
stk.push(child);
}
return root;
}

// ==================== 后序 + 中序 → 递归 ====================
class BuildFromPostIn {
unordered_map<int, int> idxMap;

TreeNode* build(const vector<int>& post, int pL, int pR, int iL, int iR) {
if (pL > pR) return nullptr;
int rootVal = post[pR];
int inRoot = idxMap[rootVal];
int leftSize = inRoot iL;
TreeNode* root = new TreeNode(rootVal);
root->left = build(post, pL, pL + leftSize 1, iL, inRoot 1);
root->right = build(post, pL + leftSize, pR 1, inRoot + 1, iR);
return root;
}
public:
TreeNode* buildTree(vector<int>& postorder, vector<int>& inorder) {
for (int i = 0; i < (int)inorder.size(); i++)
idxMap[inorder[i]] = i;
return build(postorder, 0, postorder.size()1, 0, inorder.size()1);
}
};

// ==================== 后序 + 中序 → 迭代 ====================
TreeNode* buildPostIn_Iter(vector<int>& postorder, vector<int>& inorder) {
if (postorder.empty()) return nullptr;
int n = postorder.size();
TreeNode* root = new TreeNode(postorder[n 1]);
stack<TreeNode*> stk;
stk.push(root);
int inIdx = n 1;
for (int i = n 2; i >= 0; i) {
TreeNode* node = stk.top();
TreeNode* child = new TreeNode(postorder[i]);
if (node->val != inorder[inIdx]) {
node->right = child;
} else {
while (!stk.empty() && stk.top()->val == inorder[inIdx]) {
node = stk.top(); stk.pop(); inIdx;
}
node->left = child;
}
stk.push(child);
}
return root;
}

// ==================== 验证:层序打印 ====================
void levelPrint(TreeNode* root, const string& label) {
cout << label << ": [";
if (!root) { cout << "]\\n"; return; }
queue<TreeNode*> q;
q.push(root);
bool first = true;
while (!q.empty()) {
TreeNode* node = q.front(); q.pop();
if (!first) cout << ", ";
cout << node->val;
first = false;
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
cout << "]\\n";
}

// ==================== 验证:中序打印 ====================
void inPrint(TreeNode* root, const string& label) {
cout << label << ": [";
stack<TreeNode*> stk;
TreeNode* curr = root;
bool first = true;
while (curr || !stk.empty()) {
while (curr) { stk.push(curr); curr = curr->left; }
curr = stk.top(); stk.pop();
if (!first) cout << ", ";
cout << curr->val;
first = false;
curr = curr->right;
}
cout << "]\\n";
}

int main() {
cout << "====== 测试一:前序 + 中序 ======" << endl;
vector<int> pre1 = {3, 9, 20, 15, 7};
vector<int> in1 = {9, 3, 15, 20, 7};

BuildFromPreIn solverA;
TreeNode* treeA_rec = solverA.buildTree(pre1, in1);
levelPrint(treeA_rec, "递归还原(层序验证)");
inPrint (treeA_rec, "递归还原(中序验证)");

TreeNode* treeA_itr = buildPreIn_Iter(pre1, in1);
levelPrint(treeA_itr, "迭代还原(层序验证)");
inPrint (treeA_itr, "迭代还原(中序验证)");

cout << "\\n====== 测试二:后序 + 中序 ======" << endl;
vector<int> post2 = {9, 15, 7, 20, 3};
vector<int> in2 = {9, 3, 15, 20, 7};

BuildFromPostIn solverB;
TreeNode* treeB_rec = solverB.buildTree(post2, in2);
levelPrint(treeB_rec, "递归还原(层序验证)");
inPrint (treeB_rec, "递归还原(中序验证)");

TreeNode* treeB_itr = buildPostIn_Iter(post2, in2);
levelPrint(treeB_itr, "迭代还原(层序验证)");
inPrint (treeB_itr, "迭代还原(中序验证)");

return 0;
}

预期输出:

====== 测试一:前序 + 中序 ======
递归还原(层序验证): [3, 9, 20, 15, 7]
递归还原(中序验证): [9, 3, 15, 20, 7]
迭代还原(层序验证): [3, 9, 20, 15, 7]
迭代还原(中序验证): [9, 3, 15, 20, 7]

====== 测试二:后序 + 中序 ======
递归还原(层序验证): [3, 9, 20, 15, 7]
递归还原(中序验证): [9, 3, 15, 20, 7]
迭代还原(层序验证): [3, 9, 20, 15, 7]
迭代还原(中序验证): [9, 3, 15, 20, 7]

验证方式:对还原后的二叉树做中序遍历,若结果与输入的中序序列完全吻合,则还原正确。

赞(0)
未经允许不得转载:171主机测评 » 【面试高频题】二叉树的重建:由前序+中序遍历和后序+中序遍历重建二叉树
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址