今日任务(2024/05/09)
- 977.有序数组的平方
- 209.长度最小的子数组
- 59.螺旋矩阵II
- 总结
977.有序数组的平方
题目建议:
- 本题关键在于理解双指针思想
题目:977. 有序数组的平方 - 力扣(LeetCode)
给你一个按 非递减顺序 排序的整数数组
nums
,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。示例 1:
输入:nums = [-4,-1,0,3,10] 输出:[0,1,9,16,100] 解释:平方后,数组变为 [16,1,0,9,100] 排序后,数组变为 [0,1,9,16,100]
示例 2:
输入:nums = [-7,-3,2,3,11] 输出:[4,9,9,49,121]
提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums
已按 非递减顺序 排序进阶:
- 请你设计时间复杂度为
O(n)
的算法解决本问题
暴力解法(求平方+排序)[ 用时: 5 m 19 s ]
思路
- 对每个元素求平方,替代原元素
- 对整个数组进行排序(如果是排序,应该用
vector
容器)
代码
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
vector<int> res(nums.size());
int i = 0;
for (auto& n : nums) {
res[i++] = n * n;
}
sort(res.begin(), res.end());
return res;
}
};
-
时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
- 计算平方的遍历:: O ( n ) O(n) O(n)
- 排序操作:: O ( n l o g n ) O(nlogn) O(nlogn)
-
空间复杂度: O ( n ) O(n) O(n)
分析不足
- 排序操作:增加了时间复杂度
- 额外使用一个数组:增加了空间复杂度
区间边界法[ 用时: 18 m 30 s ]
这里应该可以称作双指针法,为了和Carl的思路区分,我又起了个名字,哈哈
思路
分析问题,发现存在三种情况:
- 如果数组里没有负数,则可以直接在原数组位置求平方(空间复杂度仍是 O ( 1 ) O(1) O(1))
- 如果数组里全是正数,则可以求平方后倒序存放(存到新数组会比较方便,但空间复杂度 O ( n ) O(n) O(n))
- 如果有正有负,不确定最左端和最右端的元素谁更大,还是存到新数组会更加方便
- 设置两个下标指针
l
、r
,分别指向原始数组的左右两端(也可以理解为未处理的数据区间) - 倒序遍历新数组,选择大的元素填充到新数组里,更新下标指针(未处理区间)
- 设置两个下标指针
其中,情况3的思路可以统一处理情况1、2。因此可以得到一个通用的处理思路:
- 初始化未处理的数组区间
(l, r)
,定义新数组res
- 倒序遍历新数组
res
(每次循环填充一个元素res[i]
)- 比较区间边界元素的绝对值大小(比较绝对值可以减少运算量)
- 将绝对值大的元素处理为平方,填充到
res[i]
- 更新区间边界
- 输出数组
代码
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
vector<int> res(nums.size());
// 未处理区间的边界
int l = 0, r = nums.size() - 1;
// 倒序遍历新数组
for (int i = res.size() - 1; i >= 0; i--) {
if (abs(nums[l]) > abs(nums[r])) {
res[i] = nums[l] * nums[l];
l++; // 更新左边界
} else {
res[i] = nums[r] * nums[r];
r--; // 更新右边界
}
}
return res;
}
};
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( n ) O(n) O(n)
Carl思路
[代码随想录 (programmercarl.com)](代码随想录 (programmercarl.com))
看了Carl的思路后,发现对上面的两种方法都可以进一步优化,下面列出优化后的代码:
-
暴力解法:
- 优化1:可以直接在原始数组上求平方,空间复杂度可以降低到 O ( 1 ) O(1) O(1)
for (auto& n : nums) { n *= n; }
-
双指针法:
- 优化1:将左右边界定义为循环内的局部变量,空间使用有些许减少
- 注意:我这里使用新数组的个数
i
来判断是否处理完,等同于判断条件l <= r
// 倒序遍历新数组,未处理区间的边界是(l, r) for (int i = res.size() - 1, l = 0, r = nums.size() - 1; i >= 0; i--) {...}
209.长度最小的子数组
题目建议:
- 本题关键在于理解滑动窗口
- 这个滑动窗口看文字讲解 还挺难理解的,建议大家先看视频讲解。
- 拓展题目可以先不做。
题目:209. 长度最小的子数组 - 力扣(LeetCode)
给定一个含有
n
个正整数的数组和一个正整数target
。找出该数组中满足其总和大于等于
target
的长度最小的 连续子数组[numsl, numsl+1, ..., numsr-1, numsr]
,并返回其长度**。**如果不存在符合条件的子数组,返回0
。示例 1:
输入:target = 7, nums = [2,3,1,2,4,3] 输出:2 解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4] 输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1] 输出:0
提示:
1 <= target <= 109
1 <= nums.length <= 105
1 <= nums[i] <= 105
进阶:
- 如果你已经实现
O(n)
时间复杂度的解法, 请尝试设计一个O(n log(n))
时间复杂度的解法。
我的思路
思路
-
设置两个下标指针作为窗口边界
(l, r)
,l = r = 0
-
设置最小窗口长度
len = INT_MAX
-
遍历数组:
- 没超过目标值:扩张右边界
- 超过目标值:探寻最大左边界
- 计算当前窗口大小
r - l + 1
,更新len
- 缩小左边界,直至总和不满足目标
- 计算当前窗口大小
-
若
len == INT_MAX
,输出0;否则,输出len
代码
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int len = INT32_MAX, sum = 0;
for (int l = 0, r = 0; r < nums.size();) {
sum += nums[r];
// 超过目标值:探寻最大左边界
while (l <= r && sum >= target) {
if (r - l + 1 < len) len = r - l + 1;
sum -= nums[l];
l++;
}
// 没超过目标值:扩张右边界
if (sum < target) r++;
}
return (len == INT_MAX) ? 0 : len;
}
};
-
时间复杂度: O ( n ) O(n) O(n)
- 窗口移动: O ( n ) O(n) O(n)
- 窗口收缩: O ( n ) O(n) O(n)
-
空间复杂度: O ( 1 ) O(1) O(1)
Carl思路
[代码随想录 (programmercarl.com)](代码随想录 (programmercarl.com))
- 数组操作中另一个重要的方法:滑动窗口
- 怎么滑动?不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果
相关题目
59.螺旋矩阵II
题目建议:
- 本题关键还是在转圈的逻辑
- 在二分搜索中提到的区间定义,在这里又用上了。
给你一个正整数
n
,生成一个包含1
到n2
所有元素,且元素按顺时针顺序螺旋排列的n x n
正方形矩阵matrix
。示例 1:
输入:n = 3 输出:[[1,2,3],[8,9,4],[7,6,5]]
示例 2:
输入:n = 1 输出:[[1]]
提示:
1 <= n <= 20
我的思路 [ 用时: 34 m 26 s ]
思路
要实现的是一个正方形矩阵,观察其几何位置可以发现以下特点:
- 若边长是偶数,则绕圈数是
n/2
- 若边长是奇数,绕圈数也可以表示为
n/2
,但是**n/2
圈后会剩余一个数字没有填到最中间的空格中**。 - 每一圈的起始位置在左上角,其坐标特点是:
(x, x)
- 每一圈到方形边界的距离都是一样的,当圈数增加1,偏移的距离也增加1
根据上面的特点来思考,我们可以按照圈数来遍历,给出每圈的边界,而每圈中按照顺时针遍历4条边:
-
设定起始坐标
(sta, sta)
,这里的sta
其实与偏移量offset
相等,且都为0**(因为第一圈相当于偏移0)** -
设置边界:
x
,y
都在[offset, n - offset - 1]
范围内***(注意:这里最大值必须要减一)*** -
设置初值填充值
cnt = 1
,遍历4条边,逐个填充(注意,为了防止重复,我们需要先确定每条边的形式,这里采用的是左闭右开,如图):- 上:
x = sta
,y
在[offset, n - offset)
递增 - 右:
y = n - sta
,x
在[offset, n - offset)
递增 - 下:
x = n - sta
,y
在(offset, n - offset]
递减 - 左:
y = n - sta
,x
在(offset, n - offset]
递减
- 上:
-
遍历完一圈后更新偏移量
offset
和每圈起始位置(sta, sta)
(加一!) -
若边长是奇数,还需要填充中心
代码
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n));
int sta = 0, offset = 0, cnt = 1;
for (int i = 0; i < n/2; i++) {
int x = sta, y = sta;
// 上
for (; y < n - offset - 1; y++) {
res[x][y] = cnt++;
}
// 右
for (; x < n - offset - 1; x++) {
res[x][y] = cnt++;
}
// 下
for (; y > sta; y--) {
res[x][y] = cnt++;
}
// 左
for (; x > sta; x--) {
res[x][y] = cnt++;
}
sta++;
offset++;
}
// 填充中心
if (n % 2) res[n/2][n/2] = cnt;
return res;
}
};
-
时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 因为输入的n是边长,而我们这里填充了 n 2 n^2 n2次
-
空间复杂度: O ( n 2 ) O(n^2) O(n2)
- 因为我们创建了一个
n
⋅
n
n\cdot n
n⋅n的
vector
- 因为我们创建了一个
n
⋅
n
n\cdot n
n⋅n的
Carl思路
- 我觉得是空间复杂度是
O
(
n
2
)
O(n^2)
O(n2)呀,因为我们不是创建了一个
n
⋅
n
n\cdot n
n⋅n的
vector
吗?为啥Carl说是 O ( 1 ) O(1) O(1)呢?感觉这里可能有一点小问题~
相关题目
收获
这几天一直在忙,可能是太高强度了,突然有些消沉,没有心情去完成这边的任务,所以今天重新来完善一下。
关于数组专题的总结,我打算将相关的推荐题目全部完成后再做整理,先立个flag!
今日任务的难点在于双指针法以及其衍生的滑动窗口:
-
- 有序数组的平方
- 这一题的双指针法比较基础,只是按部就班地挪动区间的左右边界,逻辑比较清晰
-
- 长度最小的子数组
- 这一题其实也是双指针法的思想,只是双指针的使用更加灵活,但是其实我觉得还蛮难理解的
- 但是***”滑动窗口“***这个名字就非常形象(大家可以看Carl的动图感受一下),这让我觉得和溜冰的一个场景很像:
- 假若两人手牵手,刚开始前一个人奋力向前(后一个人不动),此时两人的距离会拉大;
- 当距离不能再大的时候(胳膊都拉直啦),前一个人的力量会带动着后一个人前进,直至距离不能再小
- 然后前一个人再奋力前进(后一个人不动)…
- 如此循环,直到两人滑倒冰场一端
- 我感觉真的和这个场景很像,不知道我的语言有没有表达清楚,如果有人能get到就太好啦
- 没超过目标值:扩张右边界
-
超过目标值:探寻最大左边界
-
- 螺旋矩阵Ⅱ
- 这题是我接触的第一个模拟类型的题目,个人感觉模拟是很看重场景的构建的,要找到当前场景中各个参数之间的关系是基础(比如说位置坐标的数学关系),重点的逻辑就是”模拟“本身(我的理解可能还不是很完善)
- 有一些要注意的地方:
- 每圈循环内要分别处理四条边,这是要单独处理的
- 每条边上还得注意边界(左闭右开),否则就会重复填充
- 要理解”每圈偏移量“的概念,意思是这一圈离最外圈的距离
后续工作
- 完成推荐习题
- 完成数组专题总结
- 本文若存在侵权,烦请指出,本人会立马删除相关内容;
- 本文内容若有不正确或不规范指出,请大家不吝赐教~