977.有序数组的平方
题目链接:977.有序数组的平方
暴力解法:
时间复杂度:O(nlogn) (取决于快排)
将所有的元素都进行平方,然后再排序(可以用快排),这样就可以得到一个所有元素经过平方之后的有序数组。
AC代码: (核心代码模式)
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
for (int i = 0; i < nums.size(); i++) {
nums[i] *= nums[i];
}
sort(nums.begin(), nums.end()); //快速排序
return nums;
}
};
具体的时间复杂度:
for循环:O(n) ✖️ 快排:O(nlogn)
因此时间复杂度是 O(n + nlogn), 可以说是O(nlogn)的时间复杂度,但为了和下面双指针法算法时间复杂度有鲜明对比,记为 O(n + nlog n)。
⚠️排序使用了库函数sort
双指针解法:
时间复杂度:O(n)
数组其实是有序的,暴力解法并没有利用到这一特点。
由于负数的存在,负数平方之后可能就会成为最大数,因此数组平方后的最大值就应该在数组的两端,不是最左边就是最右边,不可能是中间。
此时可以考虑双指针法,i指向起始位置,j指向终止位置。
定义一个新的数组result来装进行平方之后的数组元素。
定义一个索引下标,k = nums.size() - 1; 下标的作用:新的数组要从大到小来更新。
因为每次取的时候都是取了一个最大值(两头向中间取,先取最大,再取次大)。
这样的话,更新result数组也要下标由大到小来更新,最后得到的这个result数组才是一个按非递减顺序排序(元素从小到大)的集合。
通过for循环定义2个下标 i 和 j ,
循环终止的条件:只要 i <= j 就继续执行该循环
Q:若只写
i < j会怎样?A:那我们就把i和j相等的时候指向的这个元素给落下了。
⚠️注意:for循环的条件里面不能写i++,j--
eg: for (int i = 0, j = 0; j = nums.size(); i++, j--) ❌
因为i++ 和 j--都是有条件的,取决于两头的元素谁比较大,取了那个大的元素,对应的下标再做 + + or - -。
Carl 板书:
动画:
AC代码: (核心代码模式)
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
vector<int> result(nums.size(), 0);
int k = nums.size() - 1;
for (int i = 0, j = nums.size() - 1; i <= j; ) {
if (pow(nums[i], 2) > pow(nums[j], 2)) {
result[k--] = pow(nums[i], 2);
i++;
}
else {
result[k--] = pow(nums[j], 2);
j--;
}
}
return result;
}
};
总结:代码很简单,但是很能体现出对这个双指针操作的细节。
209.长度最小的子数组
题目链接:209.长度最小的子数组
暴力解法:
时间复杂度:O(n^2)
最直接的想法:2层for循环进行遍历:第一层for循环控制区间的起始位置,第二层for循环控制区间的终止位置。
把数组的所有区间情况都遍历出来,然后找到 >= target 的最小区间长度。
在这个区间里面不断搜索,把所有区间情况都枚举出来,然后判断 >= target 的最小长度是多少,最后返回这个最小长度。
AC代码: (核心代码模式)
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int res = INT32_MAX; //最终的结果
int sum = 0; //子数组的序列之和
int subLength = 0; //子数组的长度
for (int i = 0; i < nums.size(); i++) { //子数组的起始位置为i
sum = 0;
for (int j = i; j < nums.size(); j++) { //子数组的终止位置为j
sum += nums[j];
if (sum >= target) {
subLength = j - i + 1;
res = res < subLength ? res : subLength;
break;
}
}
}
return res == INT32_MAX ? 0 : res;
}
};
力扣更新了数据,暴力解法会超时。
这题说了是连续最小数组,暴力法穷举了所有组合,所以多了很多不必要的计算。
滑动窗口:
所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
用一个for循环来做2个for循环所做的事情。
思考:这个索引下标
j表示的究竟是滑动窗口里面的终止位置还是起始位置?
可以先假设 j 表示的是起始位置,
若 j 表示的是起始位置,这个for循环一次一次把索引下标向后移动,这个终止位置要把后面所有的元素遍历一遍,才能返回所有以这个起始 i 为起始位置的集合,然后我们再去判断这个集合里面的所有元素:
如果 >= target,去搜集所有 >= target这些集合里面的所有的长度,再取一个最小的。
如果终止位置是一个一个向后移动的话,那么和这个暴力的解法又存在什么区别呢?
此时你会发现:如果这一个for循环里面的这个j表示的是起始位置的话,那终止位置依然要把所有位置都遍历一遍,那么它的思路就和暴力是一样的。
因此这一个for循环里面的j一定指向的是终止位置,而起始位置需要我们用动态移动的策略来移动起始位置,这样才能用一个for循环的思路来解决这道题。
🪟滑动窗口解题的关键:窗口的起始位置如何移动?
终止位置随着for循环一个一个向后移动,那什么时候移动起始位置?
如果我们的终止位置指向这里,这个集合里面的元素之和如果 >= target 的话,说明这是符合条件的一个集合,我们收集它的长度之后,起始位置就可以向后移动了。
从而缩小当前这个集合,看下一个集合是否符合我们的条件。
就是当我们发现集合里面所有的元素和 >= target,我们再去移动起始位置,这样就实现了动态调整起始位置,来去收集不同长度区间里面的和。
sum收集滑动窗口里面的和
我们需要有一个集合,通过这个集合里面的元素之和来和target做比较,
res是最终取的最小的长度。
⚠️应该写
if(sum >= target)还是while(sum >= target)?
如果你写的是if,看看这个样例: 输入:target = 100, nums = [1,1,1,1,1,100]
动画:
AC代码: (核心代码模式)
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int res = INT32_MAX; //最终取的最小的长度
int sum = 0; //滑动窗口内数值之和
int i = 0; //滑动窗口的起始位置i
int subLength = 0; //滑动窗口的长度
for (int j = 0; j < nums.size(); j++) {
sum += nums[j];
while (sum >= target) {
subLength = (j - i + 1);
res = res < subLength ? res : subLength;
sum -= nums[i++];
}
}
return res == INT32_MAX ? 0 : res;
}
};
一些录友会疑惑为什么时间复杂度是O(n) 。
不要以为for里放一个while就以为是O(n^2)啊, 主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也就是O(n)
59.螺旋矩阵Ⅱ
题目链接:59.螺旋矩阵Ⅱ
这是一道模拟题,没有涉及到什么算法,就是一个模拟转圈的过程。
不过转圈这个过程需要处理的边界条件很多。
左闭右开,处理第一个节点,最后一个节点留给下一条边遍历的时候再做处理,这样4条边的遍历规则就统一了。
还在写……