引言
本文将带你深入探讨如何从前序与中序遍历、中序与后序遍历构造二叉树,并详细分析解题思路、代码实现以及涉及的语法知识。通过本文的学习,你将掌握递归、哈希表等关键技巧,并能够灵活运用这些知识解决类似的二叉树问题。让我们一起开始这段探索之旅吧!
一、基础知识
二叉树的遍历是解决二叉树问题的核心基础。常见的遍历方式有三种:
- 前序遍历:根节点 -> 左子树 -> 右子树。
- 中序遍历:左子树 -> 根节点 -> 右子树。
- 后序遍历:左子树 -> 右子树 -> 根节点。
关于二叉树三种遍历,更多内容请参考:C++二叉树三种遍历全解析及其进阶使用:前序遍历的前是什么前
二、题目介绍
- 从前序与中序遍历构造二叉树
给定一棵二叉树的前序遍历和中序遍历,构造这棵二叉树。
注意:树中不存在重复的节点。
示例:
输入:[1,2,3],[2,3,1]
输出:{1,2,#,#,3}
1
/
2
\
3
- 从中序与后序遍历构造二叉树
给定一棵二叉树的中序遍历和后序遍历,构造这棵二叉树。
注意:树中不存在重复的节点。
示例:
输入:[2,1,3],[2,3,1]
输出:{1,2,3}
1
/ \
2 3
三、解题思路
3.1 从前序与中序遍历构造二叉树
- 前序遍历的第一个元素是根节点。
// 前序遍历的第一个元素是根节点
int root_val = preorder[pre_start];
TreeNode* root = new TreeNode(root_val);
- 在中序遍历中找到根节点的位置,根节点左边是左子树的中序遍历结果,右边是右子树的中序遍历结果。
// 找到根节点在中序遍历中的位置
int mid_idx = inorder_map[root_val];
int left_size = mid_idx - in_start; // 左子树的节点数量
- 根据左子树的节点数量,在前序遍历中划分左子树和右子树的前序遍历结果。
build(preorder, pre_start + 1, pre_start + left_size, inorder, in_start, mid_idx - 1, inorder_map);
- 递归构造左子树和右子树。
// 递归构造左子树
root->left = build(preorder, pre_start + 1, pre_start + left_size, inorder, in_start, mid_idx - 1, inorder_map);
// 递归构造右子树
root->right = build(preorder, pre_start + left_size + 1, pre_end, inorder, mid_idx + 1, in_end, inorder_map);
输入:
preorder = [1, 2, 3]
inorder = [2, 3, 1]
输出:
{1, 2, #, #, 3}
3.2 从前序与中序遍历构造二叉树
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if (preorder.empty() || inorder.empty()) return nullptr;
// 哈希表存储中序遍历的值和索引
unordered_map<int, int> inorder_map;
for (int i = 0; i < inorder.size(); i++) {
inorder_map[inorder[i]] = i;
}
return build(preorder, 0, preorder.size() - 1,
inorder, 0, inorder.size() - 1,
inorder_map);
}
private:
TreeNode* build(vector<int>& preorder, int pre_start, int pre_end,
vector<int>& inorder, int in_start, int in_end,
unordered_map<int, int>& inorder_map) {
if (pre_start > pre_end || in_start > in_end) return nullptr;
// 前序遍历的第一个元素是根节点
int root_val = preorder[pre_start];
TreeNode* root = new TreeNode(root_val);
// 找到根节点在中序遍历中的位置
int mid_idx = inorder_map[root_val];
int left_size = mid_idx - in_start; // 左子树的节点数量
// 递归构造左子树
root->left = build(preorder, pre_start + 1, pre_start + left_size,
inorder, in_start, mid_idx - 1, inorder_map);
// 递归构造右子树
root->right = build(preorder, pre_start + left_size + 1, pre_end,
inorder, mid_idx + 1, in_end, inorder_map);
return root;
}
};
3.3 从中序与后序遍历构造二叉树
- 后序遍历的最后一个元素是根节点。
// 后序遍历的最后一个元素是根节点
int root_val = postorder[post_end];
TreeNode* root = new TreeNode(root_val);
- 在中序遍历中找到根节点的位置,根节点左边是左子树的中序遍历结果,右边是右子树的中序遍历结果。
// 找到根节点在中序遍历中的位置
int mid_idx = inorder_map[root_val];
int left_size = mid_idx - in_start; // 左子树的节点数量
- 根据左子树的节点数量,在后序遍历中划分左子树和右子树的后序遍历结果。
build(inorder, in_start, mid_idx - 1, postorder, post_start, post_start + left_size - 1, inorder_map);
- 递归构造左子树和右子树。
// 递归构造左子树
root->left = build(inorder, in_start, mid_idx - 1, postorder,
post_start, post_start + left_size - 1, inorder_map);
// 递归构造右子树
root->right = build(inorder, mid_idx + 1, in_end, postorder,
post_start + left_size, post_end - 1, inorder_map);
输入:
inorder = [2, 1, 3]
postorder = [2, 3, 1]
输出:
{1, 2, #, #, 3}
3.4 从中序与后序遍历构造二叉树
class Solution {
public:
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
if (inorder.empty() || postorder.empty()) return nullptr;
// 哈希表存储中序遍历的值和索引
unordered_map<int, int> inorder_map;
for (int i = 0; i < inorder.size(); i++) {
inorder_map[inorder[i]] = i;
}
return build(inorder, 0, inorder.size() - 1,
postorder, 0, postorder.size() - 1,
inorder_map);
}
private:
TreeNode* build(vector<int>& inorder, int in_start, int in_end,
vector<int>& postorder, int post_start, int post_end,
unordered_map<int, int>& inorder_map) {
if (in_start > in_end || post_start > post_end) return nullptr;
// 后序遍历的最后一个元素是根节点
int root_val = postorder[post_end];
TreeNode* root = new TreeNode(root_val);
// 找到根节点在中序遍历中的位置
int mid_idx = inorder_map[root_val];
int left_size = mid_idx - in_start; // 左子树的节点数量
// 递归构造左子树
root->left = build(inorder, in_start, mid_idx - 1,
postorder, post_start, post_start + left_size - 1,
inorder_map);
// 递归构造右子树
root->right = build(inorder, mid_idx + 1, in_end,
postorder, post_start + left_size, post_end - 1,
inorder_map);
return root;
}
};
四、代码实现过程详解
1. 从前序与中序遍历构造二叉树
示例输入
preorder = [1, 2, 3]
inorder = [2, 3, 1]
代码执行过程
- 初始化:
- 创建哈希表
inorder_map
,存储中序遍历的值和索引:inorder_map = {2: 0, 3: 1, 1: 2}
- 创建哈希表
- 第一次递归调用:
- 根节点值为
preorder[0] = 1
。 - 在中序遍历中找到
1
的索引mid_idx = 2
。 - 左子树的节点数量
left_size = mid_idx - in_start = 2 - 0 = 2
。 - 递归构造左子树:
build(preorder, 1, 2, inorder, 0, 1, inorder_map);
- 递归构造右子树:
build(preorder, 3, 2, inorder, 3, 2, inorder_map);
- 根节点值为
- 左子树递归调用:
- 根节点值为
preorder[1] = 2
。 - 在中序遍历中找到
2
的索引mid_idx = 0
。 - 左子树的节点数量
left_size = 0 - 0 = 0
。 - 递归构造左子树:
build(preorder, 2, 1, inorder, 0, -1, inorder_map); // 返回 nullptr
- 递归构造右子树:
build(preorder, 2, 2, inorder, 1, 1, inorder_map);
- 根节点值为
- 右子树递归调用:
- 根节点值为
preorder[2] = 3
。 - 在中序遍历中找到
3
的索引mid_idx = 1
。 - 左子树的节点数量
left_size = 1 - 1 = 0
。 - 递归构造左子树:
build(preorder, 3, 2, inorder, 1, 0, inorder_map); // 返回 nullptr
- 递归构造右子树:
build(preorder, 3, 2, inorder, 2, 1, inorder_map); // 返回 nullptr
- 根节点值为
最终结果
{1, 2, #, #, 3}
2. 从中序与后序遍历构造二叉树
示例输入
inorder = [2, 1, 3]
postorder = [2, 3, 1]
代码执行过程
- 初始化:
- 创建哈希表
inorder_map
,存储中序遍历的值和索引:inorder_map = {2: 0, 1: 1, 3: 2}
- 创建哈希表
- 第一次递归调用:
- 根节点值为
postorder[2] = 1
。 - 在中序遍历中找到
1
的索引mid_idx = 1
。 - 左子树的节点数量
left_size = mid_idx - in_start = 1 - 0 = 1
。 - 递归构造左子树:
build(inorder, 0, 0, postorder, 0, 0, inorder_map);
- 递归构造右子树:
build(inorder, 2, 2, postorder, 1, 1, inorder_map);
- 根节点值为
- 左子树递归调用:
- 根节点值为
postorder[0] = 2
。 - 在中序遍历中找到
2
的索引mid_idx = 0
。 - 左子树的节点数量
left_size = 0 - 0 = 0
。 - 递归构造左子树:
build(inorder, 0, -1, postorder, 0, -1, inorder_map); // 返回 nullptr
- 递归构造右子树:
build(inorder, 1, 0, postorder, 0, -1, inorder_map); // 返回 nullptr
- 根节点值为
- 右子树递归调用:
- 根节点值为
postorder[1] = 3
。 - 在中序遍历中找到
3
的索引mid_idx = 2
。 - 左子树的节点数量
left_size = 2 - 2 = 0
。 - 递归构造左子树:
build(inorder, 2, 1, postorder, 1, 0, inorder_map); // 返回 nullptr
- 递归构造右子树:
build(inorder, 3, 2, postorder, 1, 0, inorder_map); // 返回 nullptr
- 根节点值为
最终结果
{1, 2, #, #, 3}
五、语法总结
1. 哈希表的使用
哈希表(unordered_map
)是一种基于键值对的数据结构,能够快速查找、插入和删除数据。在二叉树构造问题中,哈希表用于快速查找中序遍历中根节点的位置。
哈希表使用示例
#include <unordered_map>
#include <iostream>
int main() {
// 创建一个哈希表
std::unordered_map<int, int> map;
// 插入键值对
map[10] = 1;
map[20] = 2;
map[30] = 3;
// 查找键对应的值
std::cout << "Value for key 20: " << map[20] << std::endl;
// 检查键是否存在
if (map.find(40) != map.end()) {
std::cout << "Key 40 exists!" << std::endl;
} else {
std::cout << "Key 40 does not exist!" << std::endl;
}
return 0;
}
哈希表的特点
- 时间复杂度:查找、插入、删除的平均时间复杂度为
O(1)
。 - 空间复杂度:需要额外的空间存储哈希表。
- 适用场景:需要快速查找的场景,如中序遍历中根节点的位置查找。
2. 动态数组(vector
)
vector
是 C++ 标准库中的动态数组,支持动态扩容和随机访问。
动态数组使用示例
#include <vector>
#include <iostream>
int main() {
// 创建一个动态数组
std::vector<int> vec = {1, 2, 3};
// 添加元素
vec.push_back(4);
vec.push_back(5);
// 访问元素
std::cout << "Element at index 2: " << vec[2] << std::endl;
// 遍历数组
for (int i = 0; i < vec.size(); i++) {
std::cout << vec[i] << " ";
}
std::cout << std::endl;
return 0;
}
动态数组的特点
- 时间复杂度:
- 随机访问:
O(1)
。 - 尾部插入:平均
O(1)
。 - 中间插入或删除:
O(n)
。
- 随机访问:
- 空间复杂度:需要连续的内存空间。
- 适用场景:需要动态扩容和随机访问的场景。
3. 递归
递归是一种通过函数调用自身来解决问题的编程技巧。在二叉树构造问题中,递归用于划分左右子树并构造二叉树。
递归示例
#include <iostream>
int factorial(int n) {
if (n == 0) return 1; // 递归终止条件
return n * factorial(n - 1); // 递归调用
}
int main() {
std::cout << "Factorial of 5: " << factorial(5) << std::endl;
return 0;
}
递归的特点
- 终止条件:必须有明确的递归终止条件,否则会导致无限递归。
- 适用场景:问题可以分解为相同类型的子问题,如二叉树的构造。
4. 动态内存分配
在 C++ 中,使用 new
和 delete
进行动态内存分配和释放。
动态内存分配示例
#include <iostream>
int main() {
// 动态分配一个整数
int* p = new int;
*p = 10;
std::cout << "Value: " << *p << std::endl;
// 释放内存
delete p;
return 0;
}
动态内存分配的特点
- 手动管理:需要手动释放内存,否则会导致内存泄漏。
- 适用场景:需要动态创建对象的场景,如二叉树的节点创建。
六、拓展
1. 从前序与后序遍历构造二叉树
- 前序和后序遍历无法唯一确定一棵二叉树,除非树是满二叉树。
2. 遍历的应用
- 前序遍历:用于复制二叉树。
- 中序遍历:用于获取二叉搜索树的有序序列。
- 后序遍历:用于删除二叉树。
七、总结
遍历方式 | 根节点位置 | 构造方法 | 时间复杂度 | 空间复杂度 |
---|---|---|---|---|
前序 + 中序 | 前序第一个元素 | 递归划分左右子树 | O(n) | O(n) |
中序 + 后序 | 后序最后一个元素 | 递归划分左右子树 | O(n) | O(n) |
前序 + 后序 | 无法唯一确定 | 仅适用于满二叉树 | - | - |
通过本文,你应该掌握了如何从前序与中序遍历、中序与后序遍历构造二叉树,并理解了哈希表、递归等核心知识。如果有任何问题,欢迎在评论区讨论!
点个赞吧