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, 这样的话, 就可以保证, 对于左边的先大后小, 对于右边的数, 先小后大, 左右重合的话, 先左后右
联动题
- 亚特兰蒂斯(提高班)
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_bound
和up_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
这种情况直接递归, 左儿子直接算, 右儿子直接算, 然后返回
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+B−A∩B, 核心是计算 A ∩ B A \cap B A∩B
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();
}
};