首页 > 其他分享 >offer68题 Day2

offer68题 Day2

时间:2024-10-26 16:58:54浏览次数:4  
标签:遍历 TreeNode int 中序 Day2 offer68 inorder 节点

面试题 07. 重建二叉树

前中序构建

要根据二叉树的前序遍历中序遍历结果来构建二叉树,我们可以利用以下性质:

  1. 前序遍历的第一个元素总是当前树的根节点。
  2. 中序遍历中,根节点将二叉树分为左子树和右子树。

思路

  • 根据前序遍历的第一个元素确定根节点。
  • 在中序遍历中找到根节点位置,这样可以确定左子树和右子树的元素。
  • 递归地对左子树和右子树执行同样的操作,构建二叉树。

递归构建的步骤:

  1. 在前序遍历中,根节点是第一个元素。
  2. 找到这个根节点在中序遍历中的位置,左侧的所有元素属于左子树,右侧的所有元素属于右子树。
  3. 根据这个分割,递归构建左子树和右子树。

C++ 实现

#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 {
public:
    // 哈希表用于快速查找中序遍历中根节点的位置
    unordered_map<int, int> inorder_map;

    // 构建二叉树函数
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        // 将中序遍历的值和它们对应的下标存入哈希表,便于快速查找根节点位置
        for (int i = 0; i < inorder.size(); ++i) {
            inorder_map[inorder[i]] = i;
        }
        return build(preorder, 0, preorder.size() - 1, 0, inorder.size() - 1);
    }

private:
    TreeNode* build(const vector<int>& preorder, int preStart, int preEnd, int inStart, int inEnd) {
        // 如果已经没有节点需要构建,返回空
        if (preStart > preEnd || inStart > inEnd) {
            return nullptr;
        }

        // 前序遍历的第一个节点是当前树的根节点
        int rootVal = preorder[preStart];
        TreeNode* root = new TreeNode(rootVal);

        // 在中序遍历中找到根节点的位置
        int inRoot = inorder_map[rootVal];

        // 左子树的节点数目
        int leftTreeSize = inRoot - inStart;

        // 递归构建左子树和右子树
        root->left = build(preorder, preStart + 1, preStart + leftTreeSize, inStart, inRoot - 1);
        root->right = build(preorder, preStart + leftTreeSize + 1, preEnd, inRoot + 1, inEnd);

        return root;
    }
};

// 测试函数
void printTree(TreeNode* root) {
    if (root == nullptr) {
        return;
    }
    cout << root->val << " "; // 打印根节点
    printTree(root->left);    // 递归打印左子树
    printTree(root->right);   // 递归打印右子树
}

int main() {
    vector<int> preorder = {3, 9, 20, 15, 7}; // 前序遍历
    vector<int> inorder = {9, 3, 15, 20, 7};  // 中序遍历

    Solution sol;
    TreeNode* root = sol.buildTree(preorder, inorder);

    // 打印构建后的二叉树,前序遍历方式
    printTree(root); // 输出: 3 9 20 15 7

    return 0;
}

代码解析

  1. 哈希表优化:我们使用 unordered_map 将中序遍历的每个值与它在数组中的位置进行映射,以便快速查找根节点在中序遍历中的位置,时间复杂度从 O(n) 降到 O(1)。

  2. 递归函数 build

    • preStartpreEnd 分别表示前序遍历中当前子树的起始和结束位置。
    • inStartinEnd 表示中序遍历中当前子树的起始和结束位置。
    • 每次从前序遍历中获取根节点,然后在中序遍历中找到这个根节点的位置,从而将左右子树分割开。
    • 递归地构建左右子树。
  3. 时间复杂度:O(n),因为每个节点都需要访问一次。

  4. 空间复杂度:O(n),用于存储哈希表和递归调用栈。

测试输出

对于输入的前序遍历 [3, 9, 20, 15, 7] 和中序遍历 [9, 3, 15, 20, 7],构建出的二叉树结构如下:

      3
     / \
    9  20
       / \
      15  7

输出的前序遍历为:3 9 20 15 7,与原始前序遍历一致,说明二叉树构建正确。


中后序构建

使用中序遍历后序遍历结果构建二叉树的过程与前序和中序构建类似,不过需要注意:

  • 在后序遍历中,最后一个元素是当前树的根节点
  • 在中序遍历中,根节点将二叉树分为左子树和右子树。
  • 递归地对左子树和右子树执行同样的操作,构建二叉树。

构建步骤

  1. 后序遍历的最后一个元素即为当前树的根节点。
  2. 在中序遍历中找到根节点的位置,从而将左子树和右子树分开。
  3. 递归地构建左右子树。

C++ 实现

#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 {
public:
    unordered_map<int, int> inorder_map; // 哈希表存储中序遍历的索引,方便查找

    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        // 存储中序遍历值对应的索引
        for (int i = 0; i < inorder.size(); ++i) {
            inorder_map[inorder[i]] = i;
        }
        return build(inorder, postorder, 0, inorder.size() - 1, 0, postorder.size() - 1);
    }

private:
    TreeNode* build(const vector<int>& inorder, const vector<int>& postorder, int inStart, int inEnd, int postStart, int postEnd) {
        // 递归终止条件
        if (inStart > inEnd || postStart > postEnd) {
            return nullptr;
        }

        // 后序遍历的最后一个元素是根节点
        int rootVal = postorder[postEnd];
        TreeNode* root = new TreeNode(rootVal);

        // 在中序遍历中找到根节点的位置
        int inRoot = inorder_map[rootVal];
        int leftTreeSize = inRoot - inStart; // 左子树的节点数目

        // 递归构建左子树和右子树
        root->left = build(inorder, postorder, inStart, inRoot - 1, postStart, postStart + leftTreeSize - 1);
        root->right = build(inorder, postorder, inRoot + 1, inEnd, postStart + leftTreeSize, postEnd - 1);

        return root;
    }
};

// 打印树的前序遍历
void printPreOrder(TreeNode* root) {
    if (root == nullptr) return;
    cout << root->val << " ";
    printPreOrder(root->left);
    printPreOrder(root->right);
}

int main() {
    vector<int> inorder = {9, 3, 15, 20, 7};    // 中序遍历
    vector<int> postorder = {9, 15, 7, 20, 3};  // 后序遍历

    Solution sol;
    TreeNode* root = sol.buildTree(inorder, postorder);

    // 输出前序遍历的结果,以检查树是否构建正确
    printPreOrder(root); // 输出: 3 9 20 15 7
    return 0;
}

代码解析

  1. 构建哈希表

    • 使用 unordered_map 存储中序遍历中每个值的索引,方便快速找到根节点在中序遍历中的位置。
  2. 递归构建函数 build

    • inStartinEnd 表示当前子树在中序遍历中的范围。
    • postStartpostEnd 表示当前子树在后序遍历中的范围。
    • 从后序遍历的最后一个元素获取根节点,并在中序遍历中找到该节点的位置,进而划分左右子树。
    • 递归调用 build 分别构建左右子树。
  3. 测试

    • 构建的树结构:
            3
          /   \
         9    20
              /  \
            15    7
      
    • 使用前序遍历打印输出:3 9 20 15 7,验证构建的树结构正确。

复杂度分析

  • 时间复杂度:O(n),遍历所有节点,并在哈希表中查找根节点索引的操作是 O(1)。
  • 空间复杂度:O(n),用于存储哈希表和递归调用栈。

层序编列

  1
 / \
2   3
 \ / \
  4 5 6

面试题 09. 用两个栈实现队列

这个问题可以用两个栈来实现,用一个栈保存归还的书籍(push 操作),而借出书籍时(pop 操作)可以通过另一个栈来保证借出的是最早归还的书。具体思路如下:

  1. 使用两个栈 stack1stack2stack1 用于存放归还的书籍,stack2 用于按顺序借出书籍。
  2. push 操作:直接将书籍编号压入 stack1
  3. pop 操作:如果 stack2 为空,则将 stack1 中的所有元素依次弹出并压入 stack2,这样最早归还的书会在 stack2 的顶部。然后从 stack2 弹出顶部的元素,返回该书籍编号。

在实现中,这种方式保证了最早归还的书优先借出。

以下是 C++ 代码:

#include <stack>

class BookQueue {
private:
    std::stack<int> stack1; // 用于归还书籍
    std::stack<int> stack2; // 用于借出书籍

public:
    BookQueue() {}

    void push(int bookID) {
        stack1.push(bookID);
    }

    int pop() {
        if (stack2.empty()) {
            while (!stack1.empty()) {
                stack2.push(stack1.top());
                stack1.pop();
            }
        }
        if (stack2.empty()) {
            return -1; // 没有书可以借出
        }
        int bookID = stack2.top();
        stack2.pop();
        return bookID;
    }
};

示例

BookQueue bookQueue;
bookQueue.push(1); // 书队列为 [1]
bookQueue.push(2); // 书队列为 [1, 2]
std::cout << bookQueue.pop(); // 返回 1,书队列变为 [2]

解释

  • push 操作只涉及 stack1,时间复杂度为 (O(1))。
  • pop 操作在 stack2 不为空时为 (O(1));如果 stack2 为空时,则需要将 stack1 的元素移动到 stack2 中,平均时间复杂度仍为 (O(1))。

这样,我们就可以实现按照顺序的借书和还书功能了。

面试题 10- I. 斐波那契数列

标签:遍历,TreeNode,int,中序,Day2,offer68,inorder,节点
From: https://www.cnblogs.com/albertmak/p/18504194

相关文章

  • LeetCode|3180. 执行操作可获得的最大总奖励 I(day23)
    作者:MJ昊博客:掘金、CSDN等公众号:程序猿的编程之路今天是昊的算法之路第23天,今天分享的是LeetCode第3180题执行操作可获得的最大总奖励I的解题思路。这是一道中等难度的题目,要求我们在给定的奖励值数组中,通过某些操作尽可能获取最大总奖励。题目描述简要回顾题目要......
  • LeetCode|384. 打乱数组(day22)
    作者:MJ昊博客:掘金、CSDN等公众号:程序猿的编程之路今天是昊的算法之路第22天,今天分享的是LeetCode第384题打乱数组的解题思路。这是一道中等难度的题目,要求我们实现一个算法,使得数组支持随机打乱和重置为初始顺序的功能,并且每种排列出现的概率应当相等。题目描述简要......
  • 代码随想录算法训练营day25| 491.递增子序列 46.全排列 47.全排列2
    学习资料:https://programmercarl.com/0491.递增子序列.html#算法公开课排列与组合的区别,不用startIndex,而每个树层都从0开始,但是要跳过已经用过的数(用used判断)学习记录:491.递增子序列(添加一个数组used(hash表),来保持数组每个位置上的数的使用情况,没用过为0,用过变成1)点击查看代......
  • Day23--数组的使用
    Day23--数组的使用数组的使用:1.For-Each循环2.数组做方法入参3.数组做返回值四个小的例子packagecom.liu.www.array;publicclassArrayDemo03{publicstaticvoidmain(String[]args){int[]arrays={1,2,3,4,5};//打印全部数组元素f......
  • 代码随想录算法训练营第二十四天|Day24 回溯算法
    93.复原IP地址题目链接/文章讲解:https://programmercarl.com/0093.%E5%A4%8D%E5%8E%9FIP%E5%9C%B0%E5%9D%80.html视频讲解:https://www.bilibili.com/video/BV1XP4y1U73i/思路char**result;intresultTop;intsegments[3];intisValid(char*s,intstart,intend){......
  • LeetCode|3185. 构成整天的下标对数目 II(day21)
    作者:MJ昊博客:掘金、CSDN等公众号:程序猿的编程之路今天是昊的算法之路第21天,今天分享的是LeetCode第3185题构成整天的下标对数目II的解题思路。这是一道中等难度的题目,主要考察如何高效地统计两个元素之和为24的倍数的下标对,通过优化的算法减少时间复杂度。题目描......
  • LeetCode|3184. 构成整天的下标对数目 I(day20)
    作者:MJ昊博客:掘金、CSDN等公众号:程序猿的编程之路今天是昊的算法之路第20天,今天分享的是LeetCode第3184题构成整天的下标对数目I的解题思路。这是一道简单难度的题目,考察的是数组元素之间的组合与模运算。题目描述简要回顾给定一个整数数组hours,求满足(hours[i]+......
  • 代码随想录算法训练营day22和day23 | 77. 组合 216.组合总和III 17.电话号码的字母
    学习资料:https://programmercarl.com/回溯算法理论基础.html回溯法backtracking:for循环控制递归数量,暴力搜索:组合、切割、子集、排列、棋盘今天学了组合和切割可以画个N叉树的图来帮助理解回溯过程组合又包括1.单个数组(要加startIndex参数)或多个数组;2.数组内有无重复元素;3.数......
  • Day22--下标越界及小结
    Day22--下标越界及小结数组的四个基本特点:长度是确定的,一旦被创建,大小不可改变。元素必须是相同类型,不允许混合类型。元素可以是任何数据类型,包括基本类型和引用类型。在Java中,数组对象在堆中。数组边界数组边界特点如下:下标的合法区间为[0,length-1],如果越界就......
  • Day22--内存分析
    Day22--内存分析Java内存分析:1.堆:存放new的对象和数组;可以被所有的线程共享:不会存放别的对象引用2.栈存放基本变量类型(会包含这个基本类型的具体数值)引用对象的变量(会存放这个引用在堆里面的具体地址)3.方法区可以被所有的线程共享包含了所有的class和static变量......