1.有序数组平方和
1.暴力解法
主要思路:
平方之后直接排序
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;
}
}
快速排序这里使用了库函数,其实最好不要用,自己实现快速排序代码。
补充部分见双指针法实现快速排序博客。
时间复杂度
这里暴力解法用到的排序算法是快速排序,因此时间复杂度是平方计算本身n+快速排序的复杂度O(nlog n)
。即为 O(n + nlogn)
, 可以说是O(nlogn)
的时间复杂度,但为了和下面双指针法算法时间复杂度有鲜明对比,我记为 O(n + nlog n)
。
补充:快速排序的时间复杂度
快速排序,都知道快速排序是O(nlogn)
,但是当数据已经有序情况下,快速排序的时间复杂度是O(n^2) 的,所以严格从大O的定义来讲,快速排序的时间复杂度应该是O(n^2)。
但是我们依然说快速排序是O(nlogn)
的时间复杂度,这个就是业内的一个默认规定,这里说的O代表的就是一般情况,而不是严格的上界。
我们主要关心的还是一般情况下的数据形式。
面试中说道算法的时间复杂度是多少指的都是一般情况。但是如果面试官和我们深入探讨一个算法的实现以及性能的时候,就要时刻想着数据用例的不一样,时间复杂度也是不同的,这一点是一定要注意的。
2.双指针法
主要思路:
由于本题目nums
已按 非递减顺序 排序,因此,平方的最大值只有可能在数组的最左侧或者最右侧,没有在中间的可能性。
因此,使用左右边缘的双指针法,左侧和右侧向中间逼近的同时进行对比。
建立一个新的数组,如果平方和左侧>右侧,那么新的数组中最大的在左侧。如果平方和右侧>左侧,那么新数组中最大的是右侧。
class solution{
public:
vector<int> sortedSquares(vector<int>& nums){
//int leftIndex=0;
int rightIndex=nums.size()-1;
vector<int>numsSort(nums.size(),0);
int newIndex = nums.size()-1;
for(int leftIndex=0;leftIndex<=rightIndex){ //注意这里不要用leftIndex++!后面已经+过了
//如果平方和左侧>右侧,那么新的数组中最大的是左侧
if(nums[leftIndex]*nums[leftIndex]*>nums[rightIndex]*nums[rightIndex]){
numsSort[newIndex--]=nums[leftIndex]*nums[leftIndex];
leftIndex++;
}
//如果平方和右侧>右侧,那么新的数组中最大的是右侧
else{
numsSort[newIndex--]=nums[rightIndex]*nums[rightIndex];
rightIndex--;
}
}
return numsSort;
}
}
2.长度最小的子数组:滑动窗口法
1.暴力解法
主要思路:
直接定义两个for循环,一个for循环搜索起始位置,一个for循环搜索终止位置,然后判断大于等于S的最小长度是多少。
**注意:本题目中是正整数数组,一旦题目数组中有负数,就不能使用滑动窗口了,只能用暴力解法。**因此暴力解法也需要掌握。
- 加入滑动窗口中有负数怎么办?
如果有负数的话感觉也不能用滑动窗口了,因为有负数的话无论你收缩还是扩张窗口,你里面的值的总和都可能增加或减少,就不像之前收缩一定变小,扩张一定变大,一切就变得不可控了。如果要 cover 所有的情况,那每次 left 都要缩到 right,那就退化为暴力了哈哈。
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int result = 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 >= s) { // 一旦发现子序列和超过了s,更新result
subLength = j - i + 1; // 取子序列的长度
result = result < subLength ? result : subLength;
break; // 因为我们是找符合条件最短的子序列,所以一旦符合条件就break
}
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
};
2.滑动窗口
主要思路:
滑动窗口其实也属于双指针的做法,两个指针操作,但是我们取的是两个指针中间的集合。
用一个for循环解决两个for循环需要做的事情。
滑动窗口:本质是满足了单调性,即左右指针只会往一个方向走且不会回头。收缩的本质即去掉不再需要的元素。也就是做题我们可以先固定移动右指针,判断条件是否可以收缩左指针算范围。
注意问题:
1.唯一一个for循环里面的j,表示的是滑动窗口的起始位置还是终止位置?
如果j是起始位置,终止位置不停向后遍历,得到所有长度>=S的集合再进行大小对比,那么就和暴力解法没有区别了。因此,滑动窗口for循环里面的j指向的一定是终止位置。起始位置使用动态移动的策略,才能只用一个for循环来完成需求。
2.如何移动起始位置?
终止位置确定,终止位置向前的集合里面的元素和如果>=S的话,说明这是符合条件的集合,那么我们收集其长度之后,起始位置就可以向后移动了。(缩小目前的集合,来看下一个集合是不是符合条件。)
即为,集合元素和>=S的时候,再去移动起始位置。
伪代码
//终止位置一开始从0开始
int i=0; //起始位置
for(j=0,j<=nums.size(),j++){
//收集终止位置指向的元素
sum+=nums[j];
//下面这里是写if还是写while?
//如果是if的话,只判断一次,循环就结束了,而滑动窗口的起始位置需要持续的向后移动。
//所以这里必须用while
while(sum>=s){ //如果收集的和满足基本条件
subL=j-i+1; //区间长度
result = min(result,subL); //result持续更新,符合最小长度
//result初始为最大值,只有取最大值才能不断更新
//开始移动起始位置
sums = sums-nums[i];//更新当前窗口的和
i++; //起始位置后移
}
return result; // >=s的最短长度
}
代码扩展:
class solution{
public:
int minSubArrayLen(int s,vector<int>&nums){
int result = INT32_MAX; //result先取最大值,因为不知道固定值的大小范围
int sum=0;
int i=0;
int subLength=0; //滑动窗口的长度
for(int j=0;j<nums.size();j++){
sum+=nums[j];
//此处使用while,开始确保窗口的滑动
while(sum>=s){
subLength = j-i+1; //更新窗口的长度
//将最新的长度与result结果比较,取更小的,result初始值是最大值
result = result < subLength ? result : subLength;
//此时窗口左边界左移,并且sums更新
sum = sum-nums[i];
i++;
}
}
return result==INT32_MAX ? 0:result;
//注意这里不能返回subLength!因为result才是更小的那一个
//如果while循环从头到尾都没执行,应返回0而不是result,因此在这里做result的判断,因为是条件判断,所以需要用==
}
}
注意点:
1.sums>=s
的判断条件必须用while而不是If。为了保证滑动窗口大小持续向后移动。
2.三元操作符:如果 result
小于 subLength
,则返回 result
,否则返回 subLength
。三元操作符用法补充在后面。
3.滑动窗口的左边界是一直在滑动的,只要满足sum>=s的条件,左边界就会一直滑动,一直滑动到不满足为止;最后选出的result, 是滑动整个过程中所有length的最小值。
4.这个题目给的条件是正整数数组,如果第一轮,所有值加起来都没有大于目标值,那滑动也就没有意义了,所以只有当前面出现了和大于目标值的情况才滑动。 看看后面还有没有,到下一个满足大于目标值再滑动,这样下去
三元操作符用法:
在C++中,三元操作符也称为条件操作符,它是一种简洁的替代if-else
语句的方式。它的一般形式如下:
condition ? expression_if_true : expression_if_false;
这里的condition
是一个布尔表达式,expression_if_true
是在condition
为true
时执行的表达式,expression_if_false
是在condition
为false
时执行的表达式。
例如,假设我们有两个整数a
和b
,我们希望获取两者之间的最大值。我们可以使用三元操作符来实现这一点:
cppCopy codeint a = 5;
int b = 10;
int max = (a > b) ? a : b; // max 将被赋值为 10
在这个例子中,a > b
就是我们的条件,如果它为true
(也就是说,如果a
大于b
),那么max
将被赋值为a
,否则max
将被赋值为b
。
例如本题目中:
return result==INT32_MAX? 0:result; //利用三元操作符选择返回值
需要注意的是,三元操作符的优先级较低,因此在复杂的表达式中使用它时,可能需要使用括号来确保正确的执行顺序。
时间复杂度
本题目的时间复杂度是O(n)。、
为什么这道题既有for又有while,时间复杂度还是O(n)?
时间复杂度并不是看有几个for,while。是看每个元素被操作了几次。每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也就是O(n)。
3.螺旋矩阵
注意点
边界条件非常多,处理边界容易出问题
和二分法一样,需要遵循循环不变量的原则
不变量:对每一条边的处理规则,每一条边保持同样的左闭右开/左闭右闭
方法一:自定义上下边界
题解链接:Spiral Matrix II (模拟法,设定边界,代码简短清晰) - 螺旋矩阵 II - 力扣(LeetCode)
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
int num = 1;
int left = 0, top = 0, right = n - 1, bottom = n - 1;
//初始化数组
vector<vector<int>> res(n,vector<int>(n));
while (num <= n*n ) {
//left to right
for (int i = left; i <= right; ++i) res[top][i] = num++;
++top;
//top to bottom
for (int i = top; i <= bottom; ++i) res[i][right] = num++;
--right;
//right to left
for (int i = right; i >= left; --i) res[bottom][i] = num++;
--bottom;
//bottom to top
for (int i = bottom; i >= top; --i) res[i][left] = num++;
++left;
}
return res;
}
};
方法二:判断方向
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> ans(n, vector<int>(n));
int t = 0; // 初始方向
int dx[] = {0, 1, 0, -1}, dy[] = {1, 0, -1, 0}; // 四个方向
for (int i = 0, j = 0, k = 1; k <= n * n; ++k) {
ans[i][j] = k;
int x = i + dx[t], y = j + dy[t]; // 计算下个方向
// 判断下个点是否合理,不合理就改方向
if (x < 0 || x >= n || y < 0 || y >= n || ans[x][y] != 0) t = (t + 1) % 4;
i += dx[t];
j += dy[t];
}
return ans;
}
};