代码随想录算法训练营第二天 | LeetCode209.长度最小的子数组、LeetCode59.螺旋矩阵II、区间和、开发商购买土地
01 数组专题总结
题型 | 关键理念 |
---|---|
二分查找法 | 循环不变量 |
移除元素 | 快慢指针,一次for循环完成两次for循环的查找和删除的功能 |
有序数组的平方 | 首尾指针 |
长度最小的子数组 | 滑动窗口 |
螺旋矩阵II | 循环不变量 |
区间和 | 一维前缀和 |
开发商购买土地 | 二维前缀和 |
其实我觉得算法题就是在一个很普适的问题里面加入了一些条件,从而可以提出针对性的低复杂度算法。我想通过这种方式把数组的内容串联起来!
数组地基:数组是存放在连续内存空间上的相同类型数据的集合,既然是数据,那必然涉及到增删改查
二分法:如果数组存放的是一段无序的数据,那只能通过遍历的方式去查找数据,复杂度为O(n),但是一旦是有序的数组(不管是正序逆序),就可以通过二分查找的方式降低复杂度为O(logn),而在编写代码的时候,因为二分法是在一段区间上的迭代过程,涉及到循环和边界,所以循环不变量就是一个抓手
删除元素:很直观地删除元素的方法是一次遍历找到要删除的元素,然后再次循环把后面的元素往前移动,复杂度为 O ( n 2 ) O(n^2) O(n2)。那么人们不由得想在一次遍历中同时完成删除+更新数据的操作。理念为快指针指向新数组的元素,而慢指针指向更新下标的位置,快指针不断往前把要增加的元素添加到慢指针指向的位置,最终慢指针指向的即为新数组的尾端
一维子数组求和:长度为n的数组能够通过两次for循环生出n(n-1)/2个子数组并得到各个子数组元素和,复杂度为 O ( n 2 ) O(n^2) O(n2)。此外,按照前缀和的理念,一次循环实现一次遍历构建出长度为n的前缀和数组,复杂度为O(n),如果需要得到完备情况的子数组元素和,需要n(n-1)/2次查询,因此复杂度为O[n(n-1)/2+n],还是 O ( n 2 ) O(n^2) O(n2)。因此在讨论子数组元素和的时候,最坏的算法复杂度应该就是 O ( 2 ∗ n 2 ) O(2*n^2) O(2∗n2),也就是生成+查询。例如,需要找到一个元素和为target的子数组,只有把所有的子数组情况列举出来才能得到最终的答案,完全没有其他投机取巧的可能。
但是偏偏算法题不可能这么蠢!对于长度最小的子数组而言,其实没有必要加入查询环节,只要不断生成适合的子数组即可,因此复杂度能够降为O(n)。对于非完备的找指定区间内元素的总和情况(符合业务情况),随机来了m次查询,如果傻愣愣当场来需求当场计算,复杂度来到O(mn),如果聪明点,先得到一个完备的二维表,再查询(注意这个查询其实直接索引即可),复杂度为 O ( n 2 + m ) O(n^2+m) O(n2+m)。而再聪明点,用前缀和,先构建一维表,再查询(这个查询需要经过作差运算),复杂度为O(m+n),实际运行其实很难知道谁更胜一筹,前者复杂度虽然更高,但把数学运算提前做完了,而后者还需要经过一次运算。
二维子数组求和:如果把二维数组的子数组简化为仅用一条横线或者竖线一分为二原来的数组。那基本等价于一位数组求和的情况。假设数组为 n × n n×n n×n,那划分方式为2(n-1),遍历每种划分方式并求和子数组即可的完备的数据,复杂度为 O ( n 3 ) O(n^3) O(n3)。但是前缀和,也就两次循环实现一次遍历,构建出两个长度为n的行和数组和列和数组,复杂度为 O ( n 2 ) O(n^2) O(n2),此时想要知道完备情况的子数组元素和,只需要一次遍历得到前缀和数组即可,复杂度来到 O ( n 2 + n ) O(n^2+n) O(n2+n),那为啥不一样呢,因为此完备是由我简化过的!也是开发商购买土地问题所定义的。
有序数组的平方:如果是一个无序数组,想要得到由其中元素的平方构成的有序数组,只能先遍历一遍得到平方,然后再排序,复杂度为O(n+nlogn)。但是现在是一个有序的数组,而有序数组的特点在于平方的大值一定排布在其两端,那么不断从两端取元素就能达到目标。
螺旋矩阵:这个没有啥好说的,就是遍历二维数组以循环不变量为抓手
ref:https://programmercarl.com/%E6%95%B0%E7%BB%84%E6%80%BB%E7%BB%93%E7%AF%87.html
02-1 LeetCode209.长度最小的子数组
相关资源
题目链接:https://leetcode.cn/problems/minimum-size-subarray-sum/
视频讲解:https://www.bilibili.com/video/BV1tZ4y1q7XE
题目:给定一个含有 n
个正整数的数组和一个正整数 target
**。找出该数组中满足其总和大于等于 target
的长度最小的子数组[numsl, numsl+1, ..., numsr-1, numsr]
,并返回其长度。**如果不存在符合条件的子数组,返回 0
。
第一想法:看到题目首先就想到两层for循环确定起始位置和终止位置,然后再通过一层for循环完成区间的求和,但复杂度直接来到了 O ( n 3 ) O(n^3) O(n3),细细一想,区间求和压根不需要重新计算,在第二层for循环的时候同时完成区间的求和并同目标值比较,一旦和大于目标值即可停止第二层循环。但其实我注意到了第一层for循环其实没必要再让区间总和从0开始,这样又会导致重复计算,此时脑中初步浮现出滑动窗口的理念,但我不觉得能覆盖整个数组,遂放弃。
实现:写的两层for循环代码如下:
#include<iostream>
#include<vector>
using namespace std;
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
// length的赋值其实我比较犹豫,我本来想要赋值一个很大的数,但会让代码不太通用,遂只赋值为数组的长度,然后引入一个flag来判断是否被更新过
int length = nums.size();
bool flag = false;
for (int i = 0; i <= nums.size() - 1;i++) {
int sum = 0;
for (int j = i;j <= nums.size() - 1;j++) {
sum = sum + nums[j];
// 一旦求和大于目标值就没必要再进行循环了,因为是正整数数组,再进行循环和一定大于目标值,同时长度增加,没有必要
if (sum >= target) {
flag = true;
length = min(j - i + 1,length);
break;
}
}
}
if (flag) {
return length;
}
else {
return 0;
}
}
};
int main() {
int target = 7;
vector <int> nums{ 2, 3, 1, 2, 4, 3 };
Solution solution;
cout << solution.minSubArrayLen(target, nums) << endl;
return 0;
}
遇到的问题:LeetCode提交上去之后会超出时间限制,也就是算法复杂度过高了
看完代码随想录之后的想法: 滑动窗口!一个for循环为窗口的终止位置,另一个while循环移动起始位置(但while循环起始也可以是for循环),但这样的确避免了重复计算,复杂度降为O(n),非常神奇!
收获:双指针另一变式:滑动窗口
ToDo:尝试实现滑动窗口、其他题目LeetCode904、LeetCode76
02-2 LeetCode59.螺旋矩阵II
相关资源
题目链接:https://leetcode.cn/problems/spiral-matrix-ii/
文章讲解:https://programmercarl.com/0059.%E8%9E%BA%E6%97%8B%E7%9F%A9%E9%98%B5II.html
视频讲解:https://www.bilibili.com/video/BV1SL4y1N7mV/
题目:
给你一个正整数 n
,生成一个包含 1
到 n2
所有元素,且元素按顺时针顺序螺旋排列的 n x n
正方形矩阵 matrix
。
第一想法:这道题倒是没有什么高深的算法,模拟转圈的过程即可。
实现:
#include<iostream>
#include<vector>
#include<math.h>
using namespace std;
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector <vector<int> >matrix(n, vector<int>(n, 0));
int num = 1;
int begin = 0;
int end = 0;
int i;
int j;
//length是每次循环的边的长度
for (int length = n; length > 0; length = length - 2) {
for (i = begin, j = begin; j <= begin + length - 1; j++) {
matrix[i][j] = num;
num = num + 1;
}
for (i = begin + 1, j = begin + length - 1 ; i <= begin + length - 1; i++) {
matrix[i][j] = num;
num = num + 1;
}
for (i = begin + length - 1, j = begin + length - 2; j >= begin ; j--) {
matrix[i][j] = num;
num = num + 1;
}
for (i = begin + length - 2, j = begin; i >= begin + 1; i--) {
matrix[i][j] = num;
num = num + 1;
}
begin = begin + 1;
}
return matrix;
}
};
int main() {
Solution solution;
int n = 3;
vector <vector<int> >matrix(n, vector<int>(n, 0));
matrix = solution.generateMatrix(n);
cout << matrix[1][1] << endl;
return 0;
}
存在的问题:我对边界的处理原则如下,并不是统一的,因此代码的可读性比较差,而且容易出错
看完代码随想录之后的想法: 要遵从循环不变量的原则,对于边界条件应该保持相同的左闭右开或者左开右闭
收获:循环不变量不仅可以是一维的(二分法),还可以是二维的(螺旋矩阵遍历边)
ToDo:尝试左闭右开和左开右闭实现
02-3 区间和
相关资源
题目:(ACM)
给定一个整数数组 Array,请计算该数组在每个指定区间内元素的总和。
输入描述:第一行输入为整数数组 Array 的长度 n,接下来 n 行,每行一个整数,表示数组的元素。随后的输入为需要计算总和的区间下标:a,b (b > = a),直至文件结束。
输出描述:输出每个指定区间内元素的总和
第一想法:暴力求和,没有任何强度
实现:
#include<iostream>
#include<vector>
#include<math.h>
using namespace std;
int summary(vector<int>& nums, int a, int b) {
int sum = 0;
for (int i = a; i <= b; i++) {
sum = sum + nums[i];
}
return sum;
}
int main() {
int n, a, b;
cin >> n;
vector<int> nums(n);
for (int i = 0; i < n; i++) {
cin >> nums[i];
}
while (cin >> a >> b)
{
cout << summary(nums, a, b) << endl;
}
return 0;
}
遇到的问题:时间超限
看完代码随想录之后的想法: 利用前缀和先通过一次O(n)复杂度的遍历,来使得m次查询的复杂度为O(m+n);而我的暴力遍历复杂度为O(m*n)
收获:了解用前缀和实现区间和,从而降低多次区间查询需要累加计算的次数(一次查询甚至不如直接求和)
ToDo:了解ACM的输入输出模型、重新以前缀和的方式完成本题
02-4 开发商购买土地
相关资源
- 题目链接:44. 开发商购买土地(第五期模拟笔试) (kamacoder.com)
- 文章讲解:https://www.programmercarl.com/kamacoder/0044.%E5%BC%80%E5%8F%91%E5%95%86%E8%B4%AD%E4%B9%B0%E5%9C%9F%E5%9C%B0.html
题目:
在一个城市区域内,被划分成了n * m个连续的区块,每个区块都拥有不同的权值,代表着其土地价值。目前,有两家开发公司,A 公司和 B 公司,希望购买这个城市区域的土地。
现在,需要将这个城市区域的所有区块分配给 A 公司和 B 公司。
然而,由于城市规划的限制,只允许将区域按横向或纵向划分成两个子区域,而且每个子区域都必须包含一个或多个区块。 为了确保公平竞争,你需要找到一种分配方式,使得 A 公司和 B 公司各自的子区域内的土地总价值之差最小。
注意:区块不可再分。
输入描述:第一行输入两个正整数,代表 n 和 m。接下来的 n 行,每行输出 m 个正整数。
输出描述:请输出一个整数,代表两个子区域内土地总价值之间的最小差距。
第一想法:两个子区域总价值的差距其实就是|总区域-2*区域一|,因此题目就转化把这个表达式的最小值求出来,而区域一的大小和划分方式有关,如果暴力求解——先划分再把里面所有的元素求和,复杂度为O((m+n)mn),很明显这里面有重复计算,其实把划分方式分为横着和竖着,横着划分求和只需要把行和求出来,竖着划分则把列和求出来,然后遍历一遍行和列和数组就行,复杂度为O(m+n+mn)
实现:
#include<iostream>
#include<vector>
#include<math.h>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
vector<int> row(n,0);
vector<int> col(m,0);
int diff = INT32_MAX;
int sum = 0;
vector<vector<int>> ground(n,vector<int>(m,0));
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> ground[i][j];
// 遍历出行和和列和数组
row[i] = row[i] + ground[i][j];
col[j] = col[j] + ground[i][j];
sum = sum + ground[i][j];
}
}
int row_sum = 0;
int col_sum = 0;
// 行和
for (int h = 0; h < n-1; h++) {
row_sum = row_sum + row[h];
diff = min(abs(sum-2*row_sum),diff);
}
// 列和
for (int p = 0; p < m-1; p++) {
col_sum = col_sum + col[p];
diff = min(abs(sum - 2*col_sum), diff);
}
cout << diff << endl;
return 0;
}
看完代码随想录之后的想法: 思路差不多,都是先将行方向,和列方向的和求出来,这样可以方便知道划分的两个区间的和。
收获:土地问题其实就是二维层面上的区间和问题,都是避免了重复计算,不管是O(m*n)-->O(m+n)
还是O((m+n)mn)-->O(m+n+mn)
都是把原本的重复遍历行为转化为一次遍历行为+一次查询行为。
ToDo:其实我的代码和代码随想录的代码都有优化的空间,在于abs(sum - 2*col_sum),按理说找到拐点其实就可以不用再继续了,后续试试能不能优化。