hot100 -- 二叉树(下)

👂 ▶ 心许百年 (163.com)

目录

🌼前序与中序遍历构造二叉树

AC  递归

🥤路径总和 III

AC  暴力DFS

AC  前缀和

解析

代码

🎂二叉树的最近公共祖先

AC  递归

🌼二叉树的最大路径和

AC  递归

解析

代码


🌼前序与中序遍历构造二叉树

105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)

AC  递归

1,递归出口,区间不存在    2,明确 pre, in 区间的左右端点( dfs()的参数 )

3,递归建树,令 root->left = dfs(), root->right = dfs(), return root

4,借助 unordered_map,找到中序 in,根节点位置

class Solution {
public:
    unordered_map<int, int> index; // 中序 <值, 位置>

    TreeNode* dfs(const vector<int>& pre, const vector<int>&in,
    int pre_left, int pre_right, int in_left, int in_right) {
        // 只需判断 pre 区间为空,因为 in 区间根据 得出
        if (pre_left > pre_right)   
            return nullptr; // 递归出口

        // in 区间,根节点位置
        int in_root = index[pre[pre_left]]; 

        int num = in_root - in_left; // 左子树节点数

        // 根据上述变量,明确 pre 和 in 区间的范围

        // 递归建树,注意 root->left / right = ...
        TreeNode* root = new TreeNode(pre[pre_left]); // 当前根节点
        root->left = dfs(pre, in, pre_left + 1, pre_left + num, in_left, in_left + num - 1);
        root->right = dfs(pre, in, pre_left + num + 1, pre_right, in_root + 1, in_right);

        return root;
    }

    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int n = preorder.size();

        for (int i = 0; i < n; ++i)
            index[inorder[i]] = i; // 中序 <值, 位置>

        return dfs(preorder, inorder, 0, n - 1, 0, n - 1);
    }
};

🥤路径总和 III

437. 路径总和 III - 力扣(LeetCode)

AC  暴力DFS

每个节点 10^9,10^3个节点,最大可达 10^12,将所有 int 改为 long long

时间 O(n^2):每个节点最大遍历节点数 n,共 n 个节点

空间 O(n):递归栈深度

class Solution {
public:
    // 当前节点出发的路径总数
    long long rootSum(TreeNode* root, long long targetSum) {
        if (!root) return 0; // 递归出口
        long long ret = 0;
        if (root->val == targetSum)
            ret++; // 当前路径/节点符合

        // 递归左右子树
        // 初始 ret += 的是 root 开始,整条路径上所有符合条件的点
        ret += rootSum(root->left, targetSum - root->val);
        ret += rootSum(root->right, targetSum - root->val);

        return ret;
    }

    // 整个二叉树的路径总数
    long long pathSum(TreeNode* root, long long targetSum) {
        if (!root) return 0; // 递归出口

        // 根节点路径数
        long long ret = rootSum(root, targetSum);

        // 递归左右子树
        ret += pathSum(root->left, targetSum);
        ret += pathSum(root->right, targetSum);

        return ret;
    }
};

AC  前缀和

解析

1,哈希表:得到路径和的时间,O(n) 降到 O(1)

unordered_map<long long, int> prefix,键 -- 前缀和,值 -- 该前缀和出现次数

这里的前缀和,和一维数组的前缀和一样,就是累积和,即“根节点出发的累积和”

举个例子:根节点出发,到节点 c 的前缀和为 10,且 prefix[10] == 3

表示 root节点 到 c节点 这条路径上,前缀和为 10 的不同路线有 3 段

2,逻辑:前缀和的差 == targetSum

当前节点 a 的前缀和 curr,目标路径和 targetSum

curr:root 到 a 的路径和,假设另一个节点 b 的路径和为 curr_b(b 在 root 和 a 之间)

那么当 curr - curr_b == targetSum 时,表示 a--->b 这条路线,满足要求

所以 prefix[curr]++;  表示 根节点 root 到 当前节点 a 之间,满足条件的路线 +1

而 b 是在遍历到 a 之前,就加入了哈希表 prefix 的,可以直接 prefix.count(...)

3,回溯

当前节点处理完后,需要从哈希表 prefix 移除这一前缀和,再进入下一分支路径

即,左右子树递归完后,需要回溯,来保证,当前路径的前缀和,不会作为另一路径的前提

而本题,“路径方向必须是向下的(只能从父节点到子节点)”,只能算一条往下的路线,而不能中途分叉,限制了访问的方向,分叉会影响结果,需要多次经过同一个节点

所以需要回溯,需要 prefex[curr]--; 来取消标记

4,初始化

prefix[0] = 1;   

后续匹配的前提,当 curr - targetSum == 0,表示当前节点前缀和 == targetSum

prefix.count(curr - targetSum) >= 1

此时 ret = prefix[0],即 ret = 1,然后才是 prefix[0]++

防止包含根节点时找不到

代码

时间 O(n),空间 O(n)

class Solution {
public:

    unordered_map<long long, int> prefix; // <前缀和, 相同前缀和的路径数>

    int dfs(TreeNode* root, long long curr, int targetSum) {

        if (!root) return 0; // 递归出口

        curr += root->val; // 当前节点 前缀和
        int ret = 0;

        // 节点 a, b 间的路径和 满足要求
        if (prefix.count(curr - targetSum))
            ret = prefix[curr - targetSum];

        // 标记
        prefix[curr]++; // 路径数 +1

        // 递归
        ret += dfs(root->left, curr, targetSum);
        ret += dfs(root->right, curr, targetSum);

        // 取消标记(回溯)
        prefix[curr]--;

        return ret; // 返回累加结果
    }

    int pathSum(TreeNode* root, int targetSum) {
        prefix[0] = 1; // 初始化:包含根节点的路径
        return dfs(root, 0, targetSum);
    }
};

🎂二叉树的最近公共祖先

236. 二叉树的最近公共祖先 - 力扣(LeetCode)

AC  递归

灵神讲的太好了👇

时间 O(n),空间 O(n)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        // 递归出口,返回当前节点
        if (!root || root == p || root == q)
            return root;
        // 递归
        TreeNode* left = lowestCommonAncestor(root->left, p, q);
        TreeNode* right = lowestCommonAncestor(root->right, p, q);

        // 1) 分别在左右子树找到 p, q
        if (left && right) 
            return root; // 返回当前节点
        // 2) 只有左子树找到 p, q
        if (left)
            return left;
        // 3) 只有右子树找到 p, q
        if (right)
            return right;
        // 左右子树都没有 p, q
        return nullptr; // 返回空节点
    }
};

🌼二叉树的最大路径和

124. 二叉树中的最大路径和 - 力扣(LeetCode)

AC  递归

解析

题目中 “一条路径” 的意思是,不能分叉,只能一次从头走到尾

所有树的题目,都可以将大的树,想象成一棵小树:根,左,右

按本题,一棵只有 3 个节点的树,只有以下 6 种情况:

不用向上累加

case 1: 根 左 右

case 2: 左

case 3: 右

向上累加

case 4: 根 左

case 5: 根 右

case 6: 根

是否向上累加,是因为,题目要求 “一条路径”

最后将 不用向上累加向上累加 取最大值

代码

时间 O(n),空间 O(n)

class Solution {
public:
    int ans = -1000; // case 1,2,3(不用向上累加) 最大值

    int dfs(TreeNode* root, int& acc) {
        // 递归出口
        if (!root) return -1000; // 递归出口
        // 递归
        int left = dfs(root->left, acc);
        int right = dfs(root->right, acc);
        
        // case 1,2,3 (根左右,左,右)
        int temp = max(root->val + left + right, max(left, right));
        ans = max(ans, temp);

        // case 4,5,6(根左,根右,根)
        acc = root->val + max(left, right);
        return max(acc, root->val);
    }

    int maxPathSum(TreeNode* root) {
        int acc = dfs(root, acc); // case 4,5,6(向上累加)最大值
        return max(acc, ans); // 返回两种情况最大值
    }
};
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千帐灯无此声

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值