Bootstrap

每日一题:通过前序与中序遍历、中序与后序遍历构造二叉树

引言

本文将带你深入探讨如何从前序与中序遍历、中序与后序遍历构造二叉树,并详细分析解题思路、代码实现以及涉及的语法知识。通过本文的学习,你将掌握递归、哈希表等关键技巧,并能够灵活运用这些知识解决类似的二叉树问题。让我们一起开始这段探索之旅吧!


一、基础知识

二叉树的遍历是解决二叉树问题的核心基础。常见的遍历方式有三种:

  1. 前序遍历:根节点 -> 左子树 -> 右子树。
  2. 中序遍历:左子树 -> 根节点 -> 右子树。
  3. 后序遍历:左子树 -> 右子树 -> 根节点。

关于二叉树三种遍历,更多内容请参考:C++二叉树三种遍历全解析及其进阶使用:前序遍历的前是什么前

在这里插入图片描述


二、题目介绍

  1. 从前序与中序遍历构造二叉树
    给定一棵二叉树的前序遍历和中序遍历,构造这棵二叉树。
    注意:树中不存在重复的节点。
示例:
输入:[1,2,3],[2,3,1]
输出:{1,2,#,#,3}

    1
   / 
  2
   \
    3
  1. 从中序与后序遍历构造二叉树
    给定一棵二叉树的中序遍历和后序遍历,构造这棵二叉树。
    注意:树中不存在重复的节点。
示例:
输入:[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]
代码执行过程
  1. 初始化
    • 创建哈希表 inorder_map,存储中序遍历的值和索引:
      inorder_map = {2: 0, 3: 1, 1: 2}
      
  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);
      
  3. 左子树递归调用
    • 根节点值为 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);
      
  4. 右子树递归调用
    • 根节点值为 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]
代码执行过程
  1. 初始化
    • 创建哈希表 inorder_map,存储中序遍历的值和索引:
      inorder_map = {2: 0, 1: 1, 3: 2}
      
  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);
      
  3. 左子树递归调用
    • 根节点值为 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
      
  4. 右子树递归调用
    • 根节点值为 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++ 中,使用 newdelete 进行动态内存分配和释放。

动态内存分配示例
#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)
前序 + 后序无法唯一确定仅适用于满二叉树--

通过本文,你应该掌握了如何从前序与中序遍历、中序与后序遍历构造二叉树,并理解了哈希表、递归等核心知识。如果有任何问题,欢迎在评论区讨论!


点个赞吧
图片名称

;