Week 12. 第235-258题

235. 二叉搜索树的最近公共祖先

分析

分情况, 3种情况
如果p, q在两边, 那么lca就是根节点
如果p, q在左边, 那么递归到左子树, 那么递归到左子树(因为变成了相同的问题)
如果p, q在右边, 那么递归到右边

每次递归只会递归一个分支, 因此递归最大深度就是树的高度, 时间复杂度O(h), h是树的高度

提高班LCA

正规的lca O(logn)
离线做法O(1)

code

/**
 * 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 (p->val > q->val) swap(p, q); // 考虑p < q的情况
        if (p->val <= root->val && q->val >= root->val) return root; // 如果在两边 或者有一个等于root 直接返回
        if (q->val < root->val) return  lowestCommonAncestor(root->left, p, q); // 两个中的最大值都 < root, 则递归左子树
        return lowestCommonAncestor(root->right, p, q); // 递归右子树
    }
};

236. 二叉树的最近公共祖先

分析

对子树进行递归分析

如果p == root || q == root || root == NULL的话, 那么直接返回root就可以了

否则, 递归左子树/右子树

然后判断下左子树的结果, 如果空, 那么必定在右子树

因为lca不是根节点, 也不在左子树, 那么只有可能在右子树

同理判断下 右子树的结果为空的, 返回左子树的结果

如果都为空, 直接返回root

code

/**
 * 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 == NULL || p == root || q == root) return root; 
        auto left = lowestCommonAncestor(root->left, p, q);
        auto right = lowestCommonAncestor(root->right, p, q);
        if (left == NULL) return right;
        if (right == NULL) return left;
        return root;
    }
};

237. 删除链表中的节点

分析

发现题目并没有给头节点, 只给了要删除的点

4 -> 5 -> 1 -> 9 -> NULL

取巧的办法, 将5 --> 9

在这里插入图片描述
但是实际上要删的是5, 而不是删掉1

然后怎么办呢, 把5变成1(哈哈哈, 有点搞笑的)

code

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    void deleteNode(ListNode* node) {
        node->val = node->next->val; // 将5变成1
        node->next = node->next->next;  // 然后将后面的1略过  
    }
};

分析2

其实node结构里只有两个变量, val, node*, 那么只需要将下一个节点的node赋值给当前的就可以了

注意结构体赋值语法*node = *node->next

code2

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    void deleteNode(ListNode* node) {
        *node = *node->next;
    }
};

238. 除自身以外数组的乘积

分析

返回除了自身以外的其他所有数的乘积
在这里插入图片描述
发现可以这样做
在这里插入图片描述
但是题目要求不能用除法

先预先处理前缀乘积, 再预处理后缀乘积

在这里插入图片描述
但是题目要求, 只能开1个数组

前缀处理出来后, 开后缀的时候, 就不能额外开一个新的数组

对于后缀的话, 就不开数组了, 用变量s(表示b[i]后面所有数乘积)
b[i] = p[i] * s
s也可以递推, 每次i往后挪一格子, s也往后乘一个数

然后我们的b数组和p数组也可以合并成一个数组, 这样就能保证实现一个数组

code

class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) {
        int n = nums.size();

        vector<int> p(n, 1);
        for (int i = 1; i < n; i ++ ) p[i] = p[i - 1] * nums[i - 1]; // 前缀乘积
        for (int i = n - 1, s = 1; i >= 0; i -- ){ // s表示后缀乘积
            p[i] *= s; 
            s *= nums[i]; // 处理完后, 后缀*当前数
        }

        return p;
    }
};

239. 滑动窗口最大值

分析

单调队列模板题
每次都是将头部删掉, 最后一个数加进来, 因此需要一个先进先出的数据结构

发现每次要求队列里的最值, 如果暴力求的话, 要遍历队列, 这样时间复杂度会变成O(nk)

优化方案, 可以发现有些数是没有意义的

每次要求的是队列里最大的值
只要有5的存在, -3不可能成为最大值, 而且-3还会在5之前被淘汰掉
在这里插入图片描述
总结下: 只要在队列里, 右边的数>=左边的数, 那么左边的数就没有意义, 就可以把左边的数删掉

把所有不会成为答案的数删掉后, 必然会成为单调下降的队列

因为如果不是单调下降的话, 必然会存在两个相邻的, 左边大, 右边小, 那么前一个应该被删掉, 就矛盾了

因此删完后, 一定是单调的
在这里插入图片描述
单调队列求最大值的话, 必然是队尾
在这里插入图片描述
每次插入元素的时候, 每次看下新来的员工, 比队头大的话, 就弹出队头(队头没有存在的必要了)

在这里插入图片描述
时间复杂度:
有个while循环, 为什么不是O(n^2)呢, 因为每个元素只会进队列1次, while每执行1次, 就会让一个元素出队, while总共只会执行n次, while和i循环是绑定的, 不是独立的

code

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        int n = nums.size();
        deque<int> q;
        vector<int> res;

        for (int i = 0; i < nums.size(); i ++ ){
            if (q.size() && i - k + 1 > q.front())  q.pop_front(); // 因为当前位置为i, 窗口k, 那么左端点i - k + 1滑出窗口了
            while (q.size() && nums[i] >= nums[q.back()]) q.pop_back(); // 队尾元素处理
            q.push_back(i); // 下标进队列
            if (i >= k - 1) res.push_back(nums[q.front()]); // 因为下标从0开始, 所以>= k - 1
        }
        return res;
    }
};

240. 搜索二维矩阵 II

分析

矩阵没有全局单调性, 只有在每行每列局部有单调性

要么找到了, 要么去掉一行或者一列
所以总的时间复杂度O(n + m)

在这里插入图片描述

code

class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        if (matrix.empty() || matrix[0].empty()) return false;
        int n = matrix.size(), m = matrix[0].size();
        int i = 0, j = m - 1;
        while (i < n && j >= 0){
            if (target == matrix[i][j]) return true;
            else if (matrix[i][j] > target) j --;
            else i ++;
        }

        return false;
    }
};

241. 为运算表达式设计优先级

分析

背景知识

所有中缀表达式都可以转化为表达式树
表达式树里是没有括号的
在这里插入图片描述
所以发现, 所有运算加括号都可以转化成表达式树 , 转化的方式就是从里往外看

((a + b) + c)
比如+要先算, 然后左右是a, b, 第2个是c前面的加法, 那就把前面计算的值与c相加
在这里插入图片描述


任意表达式树都可以转化为中缀表达式
首先对于根节点来说
a + 右子树的值
右子树的值, 递归去处理 b + c

这里面有个限制, 转化的时候, 只能加括号, 不能调整顺序

虽然形态各异, 但是中序遍历都是一样的
因为对于原来的表达式, 不能改变顺序, 所以中序遍历是一样的
在这里插入图片描述

本题

搜索的时候, 首先枚举下树的根节点是哪个, 根节点就表示整个区间最后一个运算操作符

递归左边, 右边,
然后任取左边, 任取右边, 左边+ 右边都可以成为全新的表达式

所以递归完左边和右边, 当前所有不同的表达式种类, 只要从左边任取一个, 右边任取一个 , 然后和当前的操作符拼起来就可以了

在这里插入图片描述

联动题

leetcode 95

code

class Solution {
public:
    vector<string> expr;

    vector<int> diffWaysToCompute(string s) {
        // 将表达式中所有 数字 符号扣出来放到vector里方便递归处理
        for (int i = 0; i < s.size(); i ++ ) {
            if (isdigit(s[i])){
                int x = 0, j = i;
                while (j < s.size() && isdigit(s[j])) x = x * 10 + (s[j ++] - '0');
                expr.push_back(to_string(x));
                i = j - 1;
            }else expr.push_back(s.substr(i, 1));
        }

        return dfs(0, expr.size() - 1);
    }

    vector<int> dfs(int l, int r){
        if (l == r) return {stoi(expr[l])};
        vector<int> res;
        for (int i = l + 1; i < r; i += 2){ // 因为开头结尾一定是数字, 所以从[l + 1, r - 1], 每次跳2格, 因为是数字 符号 数字 符号 排列
            auto left = dfs(l, i - 1), right = dfs(i + 1, r);
            for (auto x : left)
                for (auto y : right){
                    int z = 0;
                    if (expr[i] == "+") z = x + y;
                    else if (expr[i] == "-") z = x - y;
                    else  z = x * y;
                    res.push_back(z);
                }
        }
        return res;
    }
};

242. 有效的字母异位词

分析

进阶要求: 如果有Unicode字符, 因为Unicode两个字节表示一个字符

本来hash的是一个char类型的变量, 如果Unicode, 那么要hash两个字符串变量, 改成unordered_map<string, int>hash

code

class Solution {
public:
    bool isAnagram(string s, string t) {
        unordered_map<char, int> a, b;
        for (auto c : s) a[c] ++;
        for (auto c : t) b[c] ++;
        return a == b; // 容器支持比较运算
    }
};

257. 二叉树的所有路径

分析

从根节点开始遍历, 维护下当前点到根节点的路径
如果当前点是叶节点的话, 就把路径存下来

遍历是O(n)
时间复杂度O(n^2), 因为要记录答案
在这里插入图片描述

code

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<int> path; // 维护递归的路径上的值
    vector<string> ans;
    vector<string> binaryTreePaths(TreeNode* root) {
        if (root) dfs(root);
        return ans;
    }

    void dfs(TreeNode* root){
        path.push_back(root->val);
        if (!root->left && !root->right) {
            string line = to_string(path[0]);
            for (int i = 1; i < path.size(); i ++ )
                line += "->" + to_string(path[i]);
            ans.push_back(line);
        }else {
            if (root->left) dfs(root->left);
            if (root->right) dfs(root->right);
        }
        path.pop_back();
    }
};

258. 各位相加

分析(模拟)

两层循环, 没想出来, 好恶心这题

code

class Solution {
public:
    int addDigits(int num) {
        while (num >= 10){
            int tot = 0;
            for (; num > 0; num /= 10)
                tot += num % 10;
            num = tot;
        }
        return num;
    }
};

分析2

f表示题目要求的函数: 对所有位上数字相加
我们发现X = a1a2a3…an = f(X) (mod9)
因此不管怎么操作最后得到的数 一定是X mod 9

如果X % 9 = 1 ~ 8, 那么答案就一定1~ 8

需要特别判断下0, 9, 因为俩数%9 == 0
如果X 刚开始不等于0, 表示a1, … a_{n - 1} 至少有一个不是0, 不管怎么操作X 剩下个位数的时候, 一定不会是0, 比如81—>9

只有X==0 的时候,才会是0

在这里插入图片描述

code

class Solution {
public:
    int addDigits(int num) {
        if (!num) return 0;
        if (num % 9) return num % 9;
        return 9;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值