997.有序数组的平方
题目描述:
给你一个按 非递减顺序 排序的整数数组 nums
,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例一:
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]
示例二:
输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]
提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums 已按 非递减顺序 排序
解题思路:
- 关键词提取:整数数组、数组有序、元素乘积
- 暴力解法:两次遍历,一次遍历取乘积,一次遍历排序,时间复杂度是o(n + nlogn)
- 双指针法:一次遍历,取乘积时同步做排序,时间复杂度是o(n)
- 数组有序,由负数->正数依次排列,因此两端的元素的乘积是最大的,才可以采用双指针
- 左右指针的取值,左指针取下标0,右指针取下标size-1
- 为了不影响原数组的顺序,新建一个数组result存储元素乘积
- 从数组两端开始比较,每次都比较两端元素,比较之后,将较大元素乘积存放到数组中
- 移动已存放元素对应的指针,缩短数组范围,重复第4点,直至数组的长度为0(左右指针相遇)
双指针法代码如下:
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
// 双指针法,数组是有序的
// 申请一个新的数组,大小与nums一致,存放nums元素乘积
vector<int> result(nums.size(), 0);
int len = nums.size() - 1;
// 定义左指针与右指针,分别从nums两边向内移动
int left = 0;
int right = nums.size() - 1;
// 两个指针相遇之后,说明元素检索完成
while(left <= right)
{
// 记录左指针的元素乘积与右指针的元素乘积
int leftSum = nums[left]* nums[left];
int rightSum = nums[right] * nums[right];
// 将大的元素放到result数组中,由大到小依次摆放,也就是从后往前摆放
if(leftSum > rightSum)
{
result[len--] = leftSum;
left++;
}
else if(leftSum <= rightSum)
{
result[len--] = rightSum;
right--;
}
}
return result;
}
};
总结:
- 第一次做的时候,仅仅会暴力解法,但是会有一个想法是靠近双指针法的,只是比较模糊。
1)按照提示,思路很自然,就是先乘积,后排序。
2)提交之后,再来思考,会发现,以0为界限,左侧的负数乘积是先大后小,右侧的正数乘积是先小后大。可以先找到正数和负数交界的位置,由中间向两边取数,由小到大排列。但是这个很难去实现,搁置了。 - 跟着代码随想录刷题的时候,才发现可以用双指针法。
1)与第二点的想法类似,但是是从两边开始,由大到小,这个符合双指针法的普遍使用方式。
2)做过移除元素的题目之后,就更容易理解新建一个数组,依次存放有序的元素乘积的做法。
209.长度最小的子数组
题目描述:
给定一个含有 n
个正整数的数组和一个正整数 target
。
找出该数组中满足其总和大于等于 target
的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr]
,并返回其长度**。**如果不存在符合条件的子数组,返回 0
。
示例一:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,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^2)
- 滑动窗口法:一次遍历,双指针,一个指针负责窗口的起始位置,一个指针负责窗口的结束位置
- 窗口的含义是:不符合要求的、连续的子数组
- 一次遍历,以结束位置的指针为主,当该指针指向数组最后一个元素时,遍历结束
- 在遍历的过程中,假如窗口的值符合条件,记录当前的最短长度
- 记录长度后,缩小窗口,移动起始位置的指针,直到窗口中的值不符合条件为止
- 假如遍历结束,最短长度为初值,返回0
滑动窗口法代码如下:
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
// 滑动窗口法
// 双指针实现,窗口中的元素,相加后,值大于等于目标值,窗口由起始位置与终止位置框选出来
int left = 0;
int right = 0;
int sum = 0;
int result = INT32_MAX;
// 终止指针已经指向数组最后一个元素
while(right < nums.size())
{
// 将下一个元素纳进窗口
sum = sum + nums[right];
// 窗口中的值符合条件,需要缩小窗口,移动起始位置,将窗口缩小至不符合条件,并且记录窗口的最小长度
while(sum >= target)
{
int subSum = right - left + 1;
result = result > subSum ? subSum : result;
sum = sum - nums[left];
left++;
}
// 终止指针往右移动
right++;
}
// 若找不到符合条件的窗口
if(result == INT32_MAX)
{
result = 0;
}
return result;
}
};
总结:
-
第一次做的时候,仅仅会暴力解法。
1)最短的、连续的子数组,想到的就是以每个元素为子数组的第一个元素,遍历一遍,找到符合条件的子数组。2)在所有的子数组中,找到最短的长度。
3)很难想到滑动窗口法,而且也跟双指针没有什么关系。
-
跟着代码随想录刷题的时候,发现还有滑动窗口法,而且对于窗口、起始指针、终止指针都给了很明确的解释。
1)知道要用滑动窗口法的时候,会在想,双指针的作用分别是什么,该怎么移动。
2)但是,最重要的一点,就是窗口的定义没有思考清楚,导致起始位置的移动是错误的。我只考虑到满足条件后,起始位置往右移动一位,删除掉窗口左侧的一个元素,然后,就继续往窗口右侧添加新的元素。仔细看代码随想录的解释才明白原因。3)现在做题是有动力的,原因是代码随想录的视频讲解和文字讲解都特别清晰,而且是系统性地学习。相比于之前做不出来看题解,透过官方题解以及别人的题解去学习,属实太过痛苦,很难掌握原理,并且很难去应用这个原理。即使是相同原理的题目,也会夹杂着新的知识,这让一个新手很难适应,并且很难找到正反馈。感谢carl哥,让我找到了算法的乐趣,可以触碰新的领域。
59.螺旋矩阵II
题目描述:
给你一个正整数 n
,生成一个包含 1
到 n2
所有元素,且元素按顺时针顺序螺旋排列的 n x n
正方形矩阵 matrix
。
示例一:
输入:n = 3
输出:[[1,2,3],[8,9,4],[7,6,5]]
示例二:
输入:n = 1
输出:[[1]]
提示:
1 <= n <= 20
解题思路:
- 关键词提取:正整数n、1-n^2、顺时针旋转、n*n矩阵
- 模拟法:
1)需要参数去做累加,加到n*n为止
2)一共4条边,每条边都用一个参数去记录起始位置
3)明确循环不变量,就是每条边的起始位置与结束位置,都是由第一个元素开始,倒数第二个元素结束
4)针对每条边,采用一次遍历,将对应的值放进去二维数组中
5)一共循环的次数是n/2,左右、上下,两条边分别往中间靠拢
6)针对于奇数与偶数下的矩阵中心值不一致,需要单独再做填充
模拟法代码如下:
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0));
// 一共n*n个元素,需要一个数字记录
int count = 1;
// 一共四条边,需要由一个参数记录
int firstRow = 0;
int endCol = n - 1;
int endRow = n - 1;
int firstCol = 0;
// 转多少圈
int loop = n / 2;
// 将所有的值放置完
// 共4条边,每条边都要放n!的数
// 顺时针旋转
// 循环不变量,每条边的长度一定,并且相等,都是n-2
while(loop--)
{
// 第一条边,也是第0行,从第0个位置开始放,到倒数第2个位置
for(int i = firstRow; i < endCol; i++)
{
res[firstRow][i] = count;
count++;
}
// 第二条边,也是第n-1列,从第0个位置开始放,到倒数第2个位置
for(int j = firstRow; j < endRow; j++)
{
res[j][endCol] = count;
count++;
}
// 第三条边,也是第n-1行,从倒数第1个位置开始放,到第1个位置
for(int k = endRow; k > firstCol; k--)
{
res[endRow][k] = count;
count++;
}
// 第四条边,也是第0列,从倒数第1个位置开始放,到第1个位置
for(int m = endRow; m > firstRow; m--)
{
res[m][firstCol] = count;
count++;
}
// 各条边的起始位置均移位1
firstRow++;
endRow--;
endCol--;
firstCol++;
}
// n为奇数与n为偶数,矩阵中心会有区别
if(n % 2 == 1)
{
res[n/2][n/2] = count;
}
return res;
}
};
总结:
-
第一次做的时候,一眼看过去,就是要放弃的题目,没有继续下去的勇气。
1)逻辑性不够强,压根不理解题目的意思,找不到解题的突破口。2)在看了一遍代码随想录的讲解后,开始尝试自己做,每个循环下,都将整个二维数组打印出来,看是否正确。
3)不断修改边界与判断条件,调试了整整5个小时,才勉强将这个题目提交通过。
-
跟着代码随想录刷题的时候,会让自己的逻辑清晰很多,但容易陷入典型的一听就会,一做就不会。
1)第一个点就是,听着carl哥讲解的时候,是那种很有信心可以解决这道题目的状态,而且这道题很简单,不难,这点对于一个初学者来说,特别重要,即使第一次做不出来,后面多做几次,类似的,能做出来就是学会了,所以,不用担心。
2)第二个点就是,有个核心的点叫循环不变量,这个题目与二分法会有相关性。题目自身没有相关性,但是carl哥找到了这个相关性,并且以此为示例,让初学者对这个概念有很深的印象。
3)在我二刷的时候,会发现,这道题目的思路是水到渠成的,尽管在最后的一个循环中,起始位置和结束位置写错了,但是通过打印二维数组以及打印m的值,很快就能找到问题点。我的解答是4条边各自采用一个参数记录起始位置,这个还能再做优化,因为发现在循环中,会有重复使用的参数,最终可以优化为三个参数。