2023年3月2日
今天的任务有三个:
一、有序数组的平方 https://leetcode.cn/problems/squares-of-a-sorted-array/
1.思路
看到这道题的第一想法肯定是暴力解法,直接全部取平方然后重新排序即可,但这样做的时间复杂度为O(n+nlogn),源码如下:
class Solution {
public:
vector<int> sortedSquares(vector<int>& A) {
for (int i = 0; i < A.size(); i++) {
A[i] *= A[i];
}
sort(A.begin(), A.end()); // 快速排序
return A;
}
};
这种做法无疑是最好想的,以前看到数组的题基本都是顺着题目的思路进行求解,从来没考虑过算法的因素,这次的双指针法无疑让我更加深入了解双指针的本质。
2.双指针解法
经过简单的思考我们可以发现,平方后的数组最大值对应原数组的位置要么是在最左边,要么是在最右边(因为原数组是升序的),由此我们可以得出:设置两个指针一左一右,同时创建一个新的数组用来存储排序的元素,当我的左指针指向的值的平方小于右指针指向的值的平方时,就将右指针的值平方后插入到新数组的末尾,然后将右指针左移一位,再次比较两个指针对应的值的平方大小,当两个指针相遇的时候就说明循环结束(或者新数组被填满的时候),源代码如下:
class Solution {
public:
vector<int> sortedSquares(vector<int>& A) {
int right = A.size()-1;
vector<int> res(A.size(),0);
for(int i =0, j=A.size()-1;i<=j;){
if(A[i]*A[i] < A[j]*A[j]){
res[right--] = A[j] * A[j];
j--;
}else{
res[right--] = A[i] * A[i];
i++;
}
}
return res;
}
};
其中right指针用来指向新创建的数组res,从右至左插入新元素,i,j分别为老数组的左右指针,这种做法的时间复杂度为O(n),相对于暴力解法快了很多,也更能体现双指针解法在数组中的应用。
以后再碰到有关数组的题,一定要往双指针方面去想,如果老是按固有思路进行思考的话不会提升,只有不断尝试新的解法才能做到“下笔如有神”的境界。
二、长度最小的子数组 https://leetcode.cn/problems/minimum-size-subarray-sum/
1.暴力解法
这个题一开始想的是两层for循环,第一层for控制左边的起始位置,第二个for控制右边的终止位置,通过将每一种方案穷举出来进行判断,取最小值即可,源代码如下:
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int sum=0;
int sublength = 0;
int res = INT32_MAX;
for(int i = 0;i<nums.size();i++){
sum = 0;//每次更新起点的时候要更新sum的值,相当于更换的一个子序列的开始位置
for(int j =i;j<nums.size();j++){
sum += nums[j];//子序列的和
if(sum >= s){
sublength = j-i +1;
res = sublength < res ? sublength:res;//返回最小值
break;//找到最小的退出最后一次循环
}
}
}
return res == INT32_MAX ? 0:res;
}
};
这种方式的时间复杂度无疑是O(n^2),这是一个较大的时间复杂度,我们需要进行改进。
自己写的时候有点卡在了sum的位置范围,一直写在第二个循环外面,导致加出来的数一直都是错误的,有时候真的很需要自己实际动手操作一遍,比自己看一遍要强很多。
2.滑动窗口解法(比较重要!!)
a.思路
自己一开始思考的时候想过滑动窗口的方法,思路是借鉴于《计算机网络》这门课中数据链路层的“流量控制”(当然并不是按照这个算法来写,而是通过这个算法想到可以用滑动窗口来思考这个问题)。当我滑动窗口大小为1时,即从左到右划过去,看看有没有大于s的,有的话可以直接返回1,没有的话增大滑动窗口大小,当滑动窗口大小为2时,两个两个数相加和s进行比较,以此类推:(思考过程如下,相当于伪代码,只是一个思路)
//窗口大小为1时
for(int i=0;i<nums.size();i++){
if(nums[i] >= s){
break;
}
}
//窗口大小为2时
for(int i=0;i<nums.size();i++){
if(nums[i]+nums[i+1] >= s){
break;
}
}
//窗口大小为3时
for(int i=0;i<nums.size();i++){
if(nums[i]+nums[i+1]+nums[i+2] >= s){
break;
}
}
//.....
我们会发现,这中情况下不可避免地还是会有两层循环,一层是用来改变滑动窗口的起始位置,二层是用来移动,最外层可以用while来改变滑动窗口的大小,这种解法虽然应用了滑动窗口,但归根结底没有降低时间复杂度,反而比暴力解法更加麻烦,上不了台面。
b.进一步剖析
如果想降低时间复杂度,无疑是把两个for循环变成一个for循环,那for循环的参数到底是窗口的起始位置还是终止位置呢?经过思考,我们发现只能是终止位置,因为它如果代表的是起始位置,那跟刚刚a步骤的滑动窗口思路一样了,依然是两个for循环,相当于绕了一圈回到原点,所以这个一个for循环中的参数代表的只能是终止位置,那么起始位置如何移动便成了滑动窗口的精华所在,同时也是难点所在。
c.解决问题
当j(一层for循环时的参数)不断地向后移动时,累加前面的和,当第一次出现sum>=s时,将起始指针(假设是i)向右移,这时候就相当于移动了窗口。为什么呢?因为当第一次出现sum>=s时,很容易想到,就是你所要求的最小子数组长度一定小于等于目前的这个长度,因为你要求最小的,你就需要不断的缩小窗口的大小来观察会不会存在更小的数组长度满足我的要求,这就是为什么当第一次出现sum>=s时,将i右移的原因。这样一个过程就动态地调整了我们的起始位置而抛弃了原有的for循环方法。
一开始思考的时候没有想着固定起始位置还是终止位置这个点,而是想着如何能实现窗口的移动,这道题用滑动窗口解决确实很有收获,让我初步了解了滑动窗口的实现过程及整个的流程,是一道不可多得的好题目。
源代码如下:
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX;
int sum = 0; // 滑动窗口数值之和
int i = 0; // 滑动窗口起始位置
int subLength = 0; // 滑动窗口的长度
for (int j = 0; j < nums.size(); j++) {
sum += nums[j];
// 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件
while (sum >= s) {
subLength = (j - i + 1); // 取子序列的长度
result = result < subLength ? result : subLength;
sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置)
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
};
d.总结
这种滑动窗口的题首先要找是固定终止位置还是起始位置,之后再动态地改变另一个位置,通过这种方式来达到只是用一层for循环来解决题目,还是需要多见多做才能熟能生巧。
三、螺旋矩阵II https://leetcode.cn/problems/spiral-matrix-ii/
1.思路
一开始见到这个题以为是跟算法有关,后来思考思考发现这是一道“找规律”的题。首先分析可得,n就是矩阵的行列数,n^2就是总个数,每次转一圈以后会少一个元素,有可能还要分奇偶数等等,但尽管有思路,但上手操作的时候还是一头雾水,可能这种题确实没练过,没有任何感觉。经过半小时的思考和学习,写出了通过循环来对二维数组进行赋值,代码如下:
for (j = starty; j < n - offset; j++) {
res[startx][j] = count++;
}
// 模拟填充右列从上到下(左闭右开)
for (i = startx; i < n - offset; i++) {
res[i][j] = count++;
}
// 模拟填充下行从右到左(左闭右开)
for (; j > starty; j--) {
res[i][j] = count++;
}
// 模拟填充左列从下到上(左闭右开)
for (; i > startx; i--) {
res[i][j] = count++;
}
2.问题解决
这种问题简单来说是有迹可循的,往复杂了说是需要较强的观察能力,源代码如下:
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组
int startx = 0, starty = 0; // 定义每循环一个圈的起始位置
int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
int count = 1; // 用来给矩阵中每一个空格赋值
int offset = 1; // 需要控制每一条边遍历的长度,每次循环右边界收缩一位
int i,j;
while (loop --) {
i = startx;
j = starty;
// 下面开始的四个for就是模拟转了一圈
// 模拟填充上行从左到右(左闭右开)
for (j = starty; j < n - offset; j++) {
res[startx][j] = count++;
}
// 模拟填充右列从上到下(左闭右开)
for (i = startx; i < n - offset; i++) {
res[i][j] = count++;
}
// 模拟填充下行从右到左(左闭右开)
for (; j > starty; j--) {
res[i][j] = count++;
}
// 模拟填充左列从下到上(左闭右开)
for (; i > startx; i--) {
res[i][j] = count++;
}
// 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
startx++;
starty++;
// offset 控制每一圈里每一条边遍历的长度
offset += 1;
}
// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
if (n % 2) {
res[mid][mid] = count;
}
return res;
}
};
关于loop,offset的理解还需要多读几遍代码才能印象更为深刻。
那今天就到此为止吧,希望明天自己还能继续坚持,明天见!