是对第一天双指针写法还有区间定义的一些进一步练习。
总体来说难度还是有一些的。加之有不少细节的地方需要处理。
977.有序数组的平方
读完题第一感觉就是暴力循环,然后把整个数组排序就行。如果不用暴力方式又该用什么思路呢?
数字的绝对值越大,平方数就越大。
虽然知道了上面这点,但是如何试图在一次遍历就形成一个新的按要求排序的数组呢?
因为数组是非递减顺序排序的,可以观察出数组两端的平方数肯定比中间的平方数大,这样一来,可以考虑用双指针法从两端向中间移动,并且保证每次取到一个最大的平方数,再反向遍历形成一个新数组。
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
int k = nums.size() - 1;
vector<int> res;
res.resize(nums.size()); //初始化和nums一样大的res数组
for (int i = 0, j = nums.size() - 1; i <= j;) {//i=j时说明两个指针指向了同一元素,用i<=j确保不会漏掉每一个元素
if (nums[i] * nums[i] >= nums[j] * nums[j]) {
res[k] = nums[i] * nums[i];
i++; //赋值后指针才移动
k--; //反向遍历
} else {
res[k] = nums[j] * nums[j];
j--;
k--;
}
}
return res;
}
};
双指针移动的时候需要注意的是对元素赋值后指针才移动,指针指向了某个下标,应当先完成了一些操作再移动指针。
209.长度最小的子数组
读完题第一感觉是和下标移动有关,也自然会想到两层循环暴力遍历的解法。
不过要优化这种解法,试图使用一次遍历解决这个问题,也联想到了双指针。
数组中还有一个重要的操作叫滑动窗口,通过不断调整起始位置和终止位置,使得在答案区间内的窗口不断向前移动来找到最优解,本质上还是对两个指针的一些操作。
本体思路大致如下:
终止位置不断向前移动去寻找合适的答案区间,窗口用来收集满足答案的区间值;
等到窗口值满足了目标值(也就是满足其元素和>=目标值),起始位置再向前移动,去寻找最优解。
其间用一个变量去记录每一次满足条件的子数组长度,最后取出长度最小的。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int sum=0,ans=0x3f3f3f3f,flag=0;
for(int i=0,j=0;i<=j,j<nums.size();){
sum+=nums[j];
while(sum>=target){
flag=1;
int res=j-i+1; //记录子数组长
ans = res<=ans?res:ans;//每次保持最短长度的答案
sum-=nums[i];
i++;
}
j++;
}
if(flag) return ans;
else return 0;
}
};
遍历过程中同样要注意先对窗口进行完条件判断的操作,再移动指针
59.螺旋矩阵II
是一道模拟题,并没有什么特定的算法,但是很考验边界条件处理。在每一条边的遍历都应该统一使用一种区间定义。
我选择了以左闭右开的区间去计算出矩阵
题目是nxn
的矩阵,而每顺时针旋转一圈,就会减少上下两边和左右两边,所以当n/2次循环(也就是整个矩阵边长的一半)就可以遍历完整个矩阵
*n为奇数时,会发现还差最中心一个元素没有处理到,加一个特殊判断补上。
定义一个res数组,用来记录矩阵,注意数组下标从0开始。
以n=3时为例,最外圈的最上面一层下标为0-2,那么以左闭右开区间来描述就是[0,3);
接下来考虑如何表示循环的起止条件:每轮循环有四条边要进行操作,每条边都有起始点和终止点,使用左闭右开的写法,则前一条边的终止点就是下一条边的起始点。而每轮循环起始点位置就是主对角线(下标为00 11 22 33 ……的那条对角线)
初始化flag
=1,则n-flag
可以表示终止点,flag-1
表示起始点。
(这里的推导画图可以形象地得出这个规律,如果分辨不清起始点可以设两个变量startx
和starty
遍历一轮之后观察一下)
每轮循环按四个方向遍历,经过一轮后用flag++
更改起止点为止(表示更深入一层的遍历)
*n为奇数时需补充最中心的一个元素,即补充一个主对角线元素,也就是完成这几圈遍历后的下一个起止点的位置。
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>>res (n,vector<int>(n,0));
int flag=1,cnt=1;
int i,j;
int t=n/2;
while(t--){
i=j=flag-1;
//每轮起始点进行一下初始化
for(;j<n-flag;j++) res[i][j]=cnt++;
for(;i<n-flag;i++) res[i][j]=cnt++;
for(j=n-flag;j>flag-1;j--) res[i][j]=cnt++;
for(i=n-flag;i>flag-1;i--) res[i][j]=cnt++;
flag++;
}
if(n%2) res[flag-1][flag-1]=n*n;
return res;
}
};
总结
-
数组的下标是从0开始的,且内存连续,对数组进行元素删除时实际上是在进行元素覆盖
-
二分法要注意边界条件,常见的区间条件有左闭右开和左闭右闭。要根据不同区间定义来写判断条件
-
双指针和滑动窗口是对下标值的一些控制,实现对于暴力解法的优化。一般是观察数组的下标值,找到一定规律来移动两个指针去实现。
(后续时不时会进行一些拓展题目的补充,欢迎大家一同讨论;文章有不妥之处也欢迎指出。)