Week 11. 第215-224题

215. 数组中的第K个最大元素

分析

快速选择模板题
回顾一下快速选择

确定一个基准值x
将小于等于x的数 放到左边, 大于等于x的数放到右边
然后递归排左边, 递归排右边

这样左右两边递归完以后, 整个数组就排序完了

快排期望是O(nlogn), 因为每次内部调整是O(n)的, 每次除以2, 期望会递归O(logn)层, 所以总的时间复杂度是O(nlogn)

快速选择基于快排

如果第k大元素在左边, 那么只递归左边就可以了

如果k第k大元素在右边, 那么只递归右边就可以了

也就是在递归的时候只递归一边
第一层O(n), 第二层O(n / 2) , … 第三层O(n / 4)

当然c++ nth_element(), 面试的时候不能用

code (自己的写法)

因为要求第k大元素, 所以将do i++; while ( ); do j-- while() 符号翻转

class Solution {
public:
    int quick_sort(vector<int>& nums, int l, int r, int k){
        if (l >= r) return nums[l];
        int i = l - 1, j = r + 1, x = nums[l + r >> 1];
        while (i < j){
            do i ++; while (nums[i] > x);
            do j --; while (nums[j] < x);
            if (i < j) swap(nums[i], nums[j]);
        }
        int s = j - l + 1;
        if (s >= k) return quick_sort(nums, l, j, k);
        else return quick_sort(nums, j + 1, r, k - (j - l + 1));
    }

    int findKthLargest(vector<int>& nums, int k) {
        return quick_sort(nums, 0, nums.size() - 1, k);
    }
};

y老师模板分析

yxc老师的写法, 不需要计算, 元素个数, 因为当前快排完, [0, j]的位置已经排好, 所以k - 1传入quick_sort后, k转化为0开始的第0大的数开始计算, 只要比j小, 因为j也是从0开始, 那么就递归前面, 否则就递归后面

其实就是, 递归中是拿下标k和下标j进行比较, 如果下标k <= j, 就递归前面, 否则, 就递归后面

code(yxc老师的写法)

class Solution {
public:
    int quick_sort(vector<int>& nums, int l, int r, int k){
        if (l >= r) return nums[l];
        int i = l - 1, j = r + 1, x = nums[l + r >> 1];
        while (i < j){
            do i ++; while (nums[i] > x);
            do j --; while (nums[j] < x);
            if (i < j) swap(nums[i], nums[j]);
        }
        if (k <= j) return quick_sort(nums, l, j, k);
        else return quick_sort(nums, j + 1, r, k);
    }

    int findKthLargest(vector<int>& nums, int k) {
        return quick_sort(nums, 0, nums.size() - 1, k - 1);
    }
};

216. 组合总和 III

分析

搜索组合数的时候, 为了避免搜索重复方案, 就要人为定义一个顺序

定义顺序, 我们只能从前往后选, 这样的话就不会出现选了1 2 3, 又选出来1 3 2的情况

所以在搜索组合数的时候, 一定要记录一个东西start , 表示当前数最小可以从哪个数开始搜
另外还需要记录当前一共搜索了多少个数
还需要记录当前的总和是多少

举个例子:
start = 4, 那么从4开始搜, 选4递归下, 选5递归下, 选6递归下, …
在这里插入图片描述

联动

求1~n的全排列
从n个数中选k个数
这俩都是模板题, 需要背的

code

class Solution {
public:
    vector<vector<int>> ans;
    vector<int> path;
    vector<vector<int>> combinationSum3(int k, int n) {
        dfs(1, n, k);
        return ans;
    }

    void dfs(int start, int n, int k){ 
        // start表示要从至少为start的数开始选, 用来避免重复. n表示总和, 当n减到0, 并且k减到0, 表示选满了
        if (!n){ 
            if (!k) ans.push_back(path);
        }else if (k){ // 如果n还有, 并且k还剩余, 枚举递归
            for (int i = start; i <= 9; i ++ )
                if (n >= i){
                    path.push_back(i);
                    dfs(i + 1, n - i, k - 1);
                    path.pop_back();
                }
        }
    }
};

217. 存在重复元素

分析

给了一个数组, 判断是否存在重复元素
其实考察的是hash表的操作
用hash表维护每个数是否出现过
每次从前往后枚举, 对于当前这个数x, 先判断下hash表里有没有这个数
如果有, 说明存在重复元素, 返回true
如果没有的话, 把x加到这个集合里

hash表增删改查都是O(1)的, 所以整个时间复杂度是O(n)的

code

class Solution {
public:
    bool containsDuplicate(vector<int>& nums) {
        unordered_set<int> hash;
        for (auto x : nums){
            if (hash.count(x)) return true;
            hash.insert(x);
        }
        return false;
    }
};

218. 天际线问题

分析

将矩形的轮廓输出出来, 这其实是一类问题, 统称为“扫描线问题”
扫描线问题是这样一类问题: 平面上有很多矩形, 矩形有重叠啊, 包含啊, 相交啊之类的关系, 当然矩形是横平竖直的, 两条边平行于x轴, y轴

那么对于这样的图形, 求下总周长, 总覆盖面积, 或者求下轮廓

有一类通用的解法, 通过扫描线, 切割成1条1条

不规则图形, 发现求面积, 周长特别难求

解法: 类似于积分法, 我们把他们切割成若干长条, 每个长条内部是没有竖边的
其实就是把所有的竖边 延长

在这里插入图片描述
切割完之后, 发现每个长条内部都是一段一段的长方形, 规则的图形

对于滑阴影部门, 宽度都是一样的, 不管算面积, 还是周长, 都非常容易

在这里插入图片描述
题目要求输出以下点
在这里插入图片描述
首先还是按照刚才的思路, 切割成若干的扫描线(关于每条竖线做一条垂线)
我们需要统计下, 每个长条最上面那条边, 把最上面这条边找出来, 就是我们要找的轮廓线

算法步骤:

首先需要将整个图形分成若干个长条, 分成长条其实就是把所有端点拿出来, 按照横坐标排序, 排完序后, 相邻两个点之间就是长条

需要统计下长条内部最上面那条边

排序的时候, 每个长条内部有2种点, 入点, 出点
在这里插入图片描述
所有入点, 出点扔到vector里, 按照横坐标排序
然后枚举下所有相邻两个点之间的长条, 然后枚举下两个长条内部, 最上面那条边在什么地方

然后在去做的时候要考虑细节问题,
情况1: 当前点是入点, 考虑什么时候当前点所在的边是最上面那条边, 要求当前边的高度是当前长条内最高的一个, 所以发现需要维护当前长条内线段的高度

所以需要用一个数据结构 维护下当前长条内部的所有高度是什么, 需要支持插入, 删除, 找最值, 用multiset去存

维护的时候怎么维护呢, 当前是左端点的话, 表示当前长条里会加入一条边, 就把这个点的高度加入到multiset

如果枚举的右端点, 说明当前枚举的长条里就没有这条边(指带右端点的边), 那么我们就把这条边从multiset里删除

情况1:
当维护的时候, 需要判断, 当前插入的是否是长条中最高的一个, 只需要看下, 入点高度是否 > multiset里所有高度都要大; 如果>, 说明当前点是最高的边, 当前的边就需要加到答案里, 说明是一条轮廓线

情况2:
如果是出点的话, 出点下面的点可能放到轮廓里, 那么下面这个点需要满足什么要求呢, 不算当前点的话, 下面这个点应该是长条里最高点, 才能放到轮廓里
那么意味着h删完之后, h的高度仍然大于multiset里最大值, 才能将multiset最大值加入到答案中

如果h删完之后, h高度< multiset最大值怎么办, 那么这个点不应该在当前加入答案, 而应该在枚举左端点的时候加入, 如下图所示
相等时候也不应该加, 因为相等时候是一条边, 如下面第2章图
在这里插入图片描述

在这里插入图片描述

左端点相同

如果有两条边的话, 先加上面这个点,
在这里插入图片描述
因为如果先加第2个点的话, 第2个点有可能就被当作答案加进去了, 但是实际情况不对, 我们应该把更高的1点加进去
因此如果两个都是左端点的话, 而且横坐标一样的话, 优先枚举高度大的点
同理,

右端点相同

两个点都是右端点的话, 应该先把较小的值删除
如果先删除较高的, 旁边有个这样的矩形, 那么就会将较低的轮廓(即1加进来)加进来,
但实际情况, 我们不需要低的轮廓
在这里插入图片描述

在这里插入图片描述

如果一个左端点=右端点

如果先枚举图中这个点的话, 因为是右端点, 红圈的点还没加进来, 可能会将下面的点加进来
在这里插入图片描述
所以先枚举红圈的点, 那么右端点去枚举的时候就会算正确, 不会将红圈下面的点加进来
正确顺序如下:
在这里插入图片描述

排序的时候, 可以用pair来排序
对于左端点的来说, 应该将较高的排前面, 右端点应该较低的排前面, 左右重合先排左端点, 再排右端点
怎么实现呢, 如果是左端点存-h, 右端点存h, 这样的话, 就可以保证, 对于左边的先大后小, 对于右边的数, 先小后大, 左右重合的话, 先左后右

联动题

  1. 亚特兰蒂斯(提高班)

code

class Solution {
public:
    vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
        vector<vector<int>> res;
        vector<pair<int, int>> points;
        multiset<int> heights;
        for (auto& b : buildings) {
            points.push_back({b[0], -b[2]});
            points.push_back({b[1], b[2]});

        }
        sort(points.begin(), points.end());
        heights.insert(0);

        for (auto& p : points){
            int x = p.first, h = abs(p.second);
            if (p.second < 0){ // 左端点
                if (h > *heights.rbegin()) 
                    res.push_back({x, h});
                heights.insert(h);
            }else {
                heights.erase(heights.find(h));
                if (h > *heights.rbegin())
                    res.push_back({x, *heights.begin()}); // 纵坐标是高度的最大值
                
            }
        }
        return res;
    }
};

219. 存在重复元素 II

分析

前面有很多等于a[i]的数, 我们只关系距离a[i]最近的一个
直接用map来做, 保存距离a[i]最近的数的下标

code

class Solution {
public:
    bool containsNearbyDuplicate(vector<int>& nums, int k) {
        unordered_map<int, int> hash;
        for (int i = 0; i < nums.size(); i ++ ){
            int x = nums[i];
            if (hash.count(x) && i - hash[x] <= k) return true; // 不能直接return i - hash[x] <= k, 因为i - hash[x] <= k结果可能为false, 但后面可能还有数
            hash[x] = i;
        }
        return false;
    }
};

220. 存在重复元素 III

分析

需要判断的是a[i]前面是否存在一个数, 使得与a[i]的距离不超过k, 并且值和a[i]差不超过t
距离不超过k, 就是一个滑动窗口, 那么实际上看的是包括a[i]的窗口k内, 是否存在一个数和a[i]的差不超过t
动态的维护滑动窗口, 判断窗口内是否存在一个和当前数差不超过t, 其实是在窗口内找到与a[i]最接近的数

找到>= x的最小值和< x的最大数, 从这里面找最接近的数
>= x的最小数直接调用lower_bound就可以了, 而< x的数最大数, 就是这个点的前面一个数
这样的话, 就可以发现用set来维护, 因为set支持lower_bound操作, 而且set支持插入, 删除操作, 因为要维护滑动窗口

当然区间内存在相同的数, 所以要用multiset

在这里插入图片描述
时间复杂度 每次log(n), 常数次log(n), 然后总共n次,所以总时间复杂度nlogn

code

为了防止找不到的情况, 加了两个哨兵(正无穷, 负无穷), 这样在调用lower_boundup_bound的时候, 不会返回空

class Solution {
public:
    bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int t) {
        typedef long long LL;
        multiset<LL> S;
        S.insert(1e18), S.insert(-1e18); // LL最大值
        for (int i = 0, j = 0; i < nums.size(); i ++ ){
            if (i - j > k) S.erase(S.find(nums[j ++ ])); // 当前j已经不在窗口内, 删除nums[j], 并让j ++
            int x = nums[i];
            auto it = S.lower_bound(x); // 在S中返回 >= x的最小的数
            if (*it - x <= t) return true;
            -- it;
            if (x - *it <= t) return true;
            S.insert(x);
        }
        return false;
    }
};

221. 最大正方形

分析

f[i][j]: 以(i, j)为右下角的最大正方形(全是1构成的)边长
如果整个f[i][j]都是1的话, (红色框也必定是1), 因此红色边框变长起码是f[i][j] - 1
所以f[i - 1][j] >= f[i][j] - 1

在这里插入图片描述
同理也会有, f[i - 1, j - 1] >= f[i][j] - 1
也会有f[i, j - 1] >= f[i][j] - 1
所以, 整理下有
f[i][j] <= min(f[i - 1][j - 1], f[i - 1][j], f[i][j - 1]) +1

那么能不能证明f[i][j] >= min(f[i - 1][j], f[i - 1][j - 1], f[i][j - 1]) +1

假设式子不成立
f[i][j] < min(f[i - 1][j - 1], f[i - 1][j], f[i][j - 1]) +1
因此有f[i - 1][j] + 1 > f[i][j]
f[i - 1][j] > f[i][j] -1

f[i - 1][j] >= f[i][j],
因此会有红色框都是1
在这里插入图片描述
同理有
f[i][j - 1] >= f[i][j]
在这里插入图片描述
再同理, 有

在这里插入图片描述
所以发现更大的矩形里都是1, 所以跟蓝色的矩形是最大矩形,矛盾

所以假设不成立, 因此f[i][j] >= min(f[i - 1][j], f[i - 1][j - 1], f[i][j - 1]) +1

综合前面结果
f[i][j] = min(f[i - 1][j], f[i - 1][j - 1], f[i][j - 1]) +1

code

class Solution {
public:
    int maximalSquare(vector<vector<char>>& matrix) {
        if (matrix.empty() || matrix[0].empty()) return 0;
        int n = matrix.size(), m = matrix[0].size();

        vector<vector<int>> f(n + 1, vector<int>(m + 1));
        int res = 0;
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= m; j ++ ){
                if (matrix[i - 1][j - 1] == '1'){
                    f[i][j] = min(f[i - 1][j], min(f[i][j - 1], f[i - 1][j - 1])) + 1; // 取min
                    res = max(res, f[i][j]);
                }
            }

        return res * res;
                
    }
};

222. 完全二叉树的节点个数

分析

是一个二分, 但是不容易看出是一个二分

满二叉树: 每层都满的, n层的, 2^n - 1

满二叉树的话, 很好算
完全二叉树的话, 因为不知道最后一层分界点在什么地方

不能遍历, 因为遍历的话是O(n), 题目要求更快的算法

首先我们需要判断下, 一个节点是否是满二叉树, 只需要判断下最左侧点和最右侧点的深度是否一样, 只要左右两个端点深度一样, 就是一颗满二叉树
满二叉树的直接算, 返回
不是满二叉树的话

  1. 左边满二叉树, 右边满二叉树, 左边比右边层数多1

这种情况直接递归, 左儿子直接算, 右儿子直接算, 然后返回
在这里插入图片描述

2.有且只有1颗不是满二叉树
因为最后一层只有一个分界点, 分界点如果在左边的话, 那么左边不是满二叉树, 右边是满二叉树
分界点在右边的话, 说明左边是满二叉树, 右边不是满二叉树
所以不管什么情况, 有且只有1颗不是满二叉树
那么是满二叉树的部分, 可以直接用公式算了, 不是满二叉树话就递归

整个过程其实就是二分的过程
举个例子: 如果当前区间不是满二叉树, 会递归到左儿子, 会算下左半边是不是满二叉树, 然后算下右半边是不是满二叉树. 如果右半边是满二叉树, 就不用递归了.
算左边不是满二叉树, 相当于分界点在左半边.
然后继续递归左边, 如果左边是满的, 会递归到右边去算

在这里插入图片描述
所以每次把分界点所在的区间缩小一半, 一共最多会二分logn次,
每次判断左右深度的时间复杂度是 O ( l o g n ) O(logn) O(logn),因为需要确定子树深度是否相同
所以时间复杂度 O ( l o g 2 n ) O(log^2n) O(log2n)

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:
    int countNodes(TreeNode* root) {
        if (!root) return 0;
        auto l = root->left, r = root->right;
        int x = 1, y = 1; // 分别计算左右两边递归的层数
        while (l) l = l->left, x ++; 
        while (r) r = r->right, y ++;
        if (x == y) return (1 << x) - 1; 
        return countNodes(root->left) + countNodes(root->right) + 1; // 计算左边的 + 计算右边的 + 当前点
    }
};

223. 矩形面积

分析

计算矩形并集的面积

如果是1维的话, 计算 A + B − A ∩ B A + B - A \cap B A+BAB, 核心是计算 A ∩ B A \cap B AB

1维的情况, 右端点其实就是min(B, D)
左端点就是max(A,C)
因此交集长度就是min(B,D) - max(A,C)
当然不相交的时候, 再和0取max
即:max(0, min(B,D) - max(A,C))
在这里插入图片描述
2维问题:
只需要计算下在横轴上投影的长度, 和纵轴上投影的长度
投影长度相乘就是交集的面积

在这里插入图片描述

code

总共面积不会爆int , 但是中间结果可能爆int

typedef long long LL;
class Solution {
public:

    int computeArea(LL A, LL B, LL C, LL D, LL E, LL F, LL G, LL H) {
        LL x = max(0ll, min(C, G) - max(A, E));
        LL y = max(0ll, min(D, H) - max(F, B));
        return (C - A) * (D - B) + (G - E) * (H - F) - x * y;
    }
};

224. 基本计算器

分析

在这里插入图片描述
模板题

code

class Solution {
public:
    stack<int> num;
    stack<char> op;

    void eval(){
        int b = num.top(); num.pop();
        int a = num.top(); num.pop();
        auto c = op.top(); op.pop();
        if (c == '+') a += b;
        else a -= b;
        num.push(a);
    }

    int calculate(string s) {
        for (int i = 0; i < s.size(); i ++ ){
            auto c = s[i];
            if (c == ' ') continue;
            else if (isdigit(c)){
                int x = 0, j = i;
                while (j < s.size() && isdigit(s[j]))
                    x = x * 10 + (s[j ++ ] - '0');
                i = j - 1;
                num.push(x);
            }else if (c == '(') op.push(c);
            else if (c == ')') {
                while (op.top() != '(') eval();
                op.pop();
            }else {
                if (!i || s[i - 1] == '(' || s[i - 1] == '+' || s[i - 1] == '-') num.push(0); // 特判, 正号/ 负号/ 左括号/ 第0个位置插入运算符的时候,  补一个0
                while (op.size() && op.top() != '(') eval(); // 因为当前遇到是'('的话, 就导致当前栈顶弹出'('来做运算, 因此'('得单独碰到‘)’的时候处理
                op.push(c);
            }
        }
        while (op.size()) eval();
        return num.top();
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值