代码随想录算法训练营第二天 | 977. 有序数组的平方、209. 长度最小的子数组、59. 螺旋矩阵 Ⅱ
977. 有序数组的平方
文档讲解:代码随想录 | 数组 | 有序数组的平方
视频讲解:双指针法经典题目 | LeetCode:977. 有序数组的平方
状态:这个暴力法贼简单!不过双指针法确实完全没想到
题目链接:977. 有序数组的平方
解题思路-1:双指针法
- 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 n u m s nums nums 的长度
- 空间复杂度: O ( 1 ) O(1) O(1),除了存储答案的数组以外,只需要维护常量空间
解题思路-2:暴力法
- 时空间复杂度这里取决于所用的排序方法:快速排序
- 时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn),其中 n n n 是数组 n u m s nums nums 的长度;实际上是 O ( n + n l o g n ) O(n + nlogn) O(n+nlogn) ,因为快速排序前的 for 循环时间复杂度为 O ( n ) O(n) O(n)
- 空间复杂度: O ( l o g n ) O(logn) O(logn),除了存储答案的数组以外,我们需要 O ( l o g n ) O(logn) O(logn) 的栈空间进行排序
双指针法
思路
由于负数的存在,负数平方之后可能就会成为最大数,因此数组平方后的最大值就应该在数组的两端,不是最左边就是最右边,不可能是中间
因此使用两个指针 left
、right
分别指向数组的第一位和最后一位 ,每次比较两个指针对应的数,选择较大的那个逆序放入答案并移动指针
代码
C++ 代码如下:
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
int left = 0;
int right = nums.size() - 1;
int maxIndex = nums.size() - 1;
vector<int> result(nums.size(), 0);
while (left <= right) {
if (nums[left] * nums[left] > nums[right] * nums[right]) {
result[maxIndex--] = nums[left] * nums[left];
left = left + 1;
} else {
result[maxIndex--] = nums[right] * nums[right];
right = right - 1;
}
}
return result;
}
};
Python 代码如下:
# (版本一)双指针法
class Solution:
def sortedSquares(self, nums: List[int]) -> List[int]:
l, r, i = 0, len(nums)-1, len(nums)-1
res = [float('inf')] * len(nums) # 需要提前定义列表,存放结果
while l <= r:
if nums[l] ** 2 < nums[r] ** 2: # 左右边界进行对比,找出最大值
res[i] = nums[r] ** 2
r -= 1 # 右指针往左移动
else:
res[i] = nums[l] ** 2
l += 1 # 左指针往右移动
i -= 1 # 存放结果的指针需要往前平移一位
return res
暴力法
思路
最直观的想法,序列中每个数平方后直接快速排序
代码
C++ 代码如下:
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;
}
};
Python 代码如下:
# (版本二)暴力排序法
class Solution:
def sortedSquares(self, nums: List[int]) -> List[int]:
for i in range(len(nums)):
nums[i] *= nums[i]
nums.sort()
return nums
# (版本三)暴力排序法+列表推导法
class Solution:
def sortedSquares(self, nums: List[int]) -> List[int]:
return sorted(x*x for x in nums)
注意要点
- 两个语法知识点
- 快速排序:
sort(nums.begin(), nums.end())
- vector 定义:
vector<int> result(nums.size(), 0)
- 快速排序:
- 易错点: 题目要求升序排序,而原数组平方后的最大值就在数组两端,而升序排列要求最大值应存放在结果数组最右端,因此逆序放入答案并移动指针
相关题目推荐
待补充
209. 长度最小的子数组
文档讲解:代码随想录 | 数组 | 长度最小的子数组
视频讲解:拿下滑动窗口! | LeetCode 209 长度最小的子数组
状态:轻松拿下!
题目链接:209. 长度最小的子数组
解题思路-1:滑动窗口
- 前提:滑动窗口中不会加入负数!
- 优势:滑动窗口的时间复杂度只能说遥遥领先!
- 时间复杂度:
O
(
n
)
O(n)
O(n) ,其中
n
n
n 为数组长度。指针
left
和right
最多各移动 n n n 次 - 空间复杂度: O ( 1 ) O(1) O(1)
解题思路-2:暴力法
- 时间复杂度: O ( n 2 ) O(n^2) O(n2) ,其中 n n n 为数组长度。需要遍历每个下标作为子数组的开始下标,对于每个开始下标,需要遍历其后面的下标得到长度最小的子数组
- 空间复杂度: O ( 1 ) O(1) O(1)
滑动窗口
思路
滑动窗口:本质是满足了单调性,即左右指针只会往一个方向走且不会回头。收缩的本质即去掉不再需要的元素。也就是做题我们可以先固定移动右指针,判断条件是否可以收缩左指针算范围
- 初始化一个
sum
来记录子数组的和 - 当
sum >= target
说明窗口内存在子数组的和大于target
,先扩张窗口右边界找到子数组,再缩减窗口左边界找到长度最小的子数组- 当窗口右边界向右扩张,统计窗口内的元素和
sum
; - 当窗口左边界向右缩减,统计窗口内的元素和
sum
;
- 当窗口右边界向右扩张,统计窗口内的元素和
以示例 1 为例:
输入: target = 7, nums = [2,3,1,2,4,3]
输出: 2
解释: 子数组 [4,3] 是该条件下的长度最小的子数组
滑动窗口实现示意图如下:
思考:双指针和滑动窗口有什么区别,感觉双指针也是不断缩小的窗口。这道题,想用两头取值的双指针,结果错了?
因为两头指针走完相当于最多只把整个数组遍历一遍,会漏掉很多情况。滑动窗口实际上是双层遍历的优化版本,而双指针其实只有一层遍历,只不过是从头尾开始遍历的
滑动窗口的原理是右边先开始走,然后直到窗口内值的总和大于target,此时就开始缩圈,缩圈是为了找到最小值,只要此时总和还大于target,我就一直缩小,缩小到小于target为止在这过程中不断更新最小的长度值,然后右边继续走,如此反复,直到右边碰到边界。这样就保证了可以考虑到最小的情况
代码
C++ 代码如下:
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
if (nums.size() == 0) {
return 0;
}
int minLen = INT32_MAX; // 最终的结果,目标为最小值常初始化设置为最大值
int sum = 0; // 滑动窗口数值之和
int left = 0, right = 0; // 滑动窗口的起始位置和终止位置
while (right < nums.size()) {
sum += nums[right++];
// 注意这里使用 while,每次更新 left(起始位置),并不断比较子序列是否符合条件
while (sum >= target) {
minLen = minLen < right - left ? minLen : right - left;
sum -= nums[left++]; // 这里体现出滑动窗口的精髓之处,不断变更 left(子序列的起始位置)
}
}
// 如果 minLen 没有被赋值的话,就返回 0,说明没有符合条件的子序列
return minLen != INT32_MAX ? minLen : 0;
}
};
Python 代码如下:
class Solution:
def minSubArrayLen(self, s: int, nums: List[int]) -> int:
if not nums:
return 0
n = len(nums)
ans = n + 1
sums = [0]
for i in range(n):
sums.append(sums[-1] + nums[i])
for i in range(1, n + 1):
target = s + sums[i - 1]
bound = bisect.bisect_left(sums, target)
if bound != len(sums):
ans = min(ans, bound - (i - 1))
return 0 if ans == n + 1 else ans
暴力法
思路
最直接的想法:2 层 for 循环进行遍历:第一层 for 循环控制区间的起始位置,第二层 for 循环控制区间的终止位置
- 把数组的所有区间情况都遍历出来,然后找到
>= target
的最小区间长度 - 在这个区间里面不断搜索,把所有区间情况都枚举出来,然后判断
>= target
的最小长度是多少,最后返回这个最小长度
代码
C++ 代码如下:
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
if (nums.size() == 0) {
return 0;
}
int minLen = INT32_MAX; // 最终的结果,目标为最小值常初始化设置为最大值
for (int left = 0; left < nums.size(); left++) { // 设置子序列起点为 left
int sum = 0; // 子序列的数值之和
for (int right = left; right < nums.size(); right++) { // 设置子序列终点为 right
sum += nums[right];
if (sum >= target) { // 一旦发现子序列和超过了 target,更新 minLen
minLen = minLen < right - left + 1 ? minLen : right - left + 1;
break; // 因为我们是找符合条件最短的子序列,所以一旦符合条件就 break 跳出内层循环
}
}
}
// 如果 minLen 没有被赋值的话,就返回0,说明没有符合条件的子序列
return minLen != INT32_MAX ? minLen : 0;
}
};
Python 代码如下:
# (版本二)暴力法
class Solution:
def minSubArrayLen(self, s: int, nums: List[int]) -> int:
if not nums:
return 0
n = len(nums)
ans = n + 1
for i in range(n):
total = 0
for j in range(i, n):
total += nums[j]
if total >= s:
ans = min(ans, j - i + 1)
break
return 0 if ans == n + 1 else ans
注意要点
- 滑动窗口前提: 滑动窗口中不会加入负数!
- LeetCode 给出的代码均考虑到
nums.size() == 0
的情况,确实会更严谨一些,需要注意一下 - 本题目目标结果是得到满足某条件(条件可替换)的最小连续子数组的长度
int minLen = INT32_MAX
,这种目标为最小值常初始化设置为最大值的方式以后会很常见,反之亦然 - 子序列长度取
r
i
g
h
t
−
l
e
f
t
right - left
right−left 还是
r
i
g
h
t
−
l
e
f
t
+
1
right - left + 1
right−left+1 需要仔细考虑,这一点会受
left++
与right++
代码位置影响,视情况而定 - 滑动窗口 方法中需要连续判定
sum >= target
以此来缩减左边界,相当于多个连续 if 判断语句,因此需要用 while 替代 - 其他一些小细节
- 滑动窗口的外循环条件
while (right < nums.size())
,最开始考虑用for (int right = 0; right < nums.size(); right++)
,其实回头来看效果是一样的,但感觉前者其实更好理解 - 如果
minLen
没有被赋值的话,就返回 0,说明没有符合条件的子序列:return minLen != INT32_MAX ? minLen : 0;
一行代码解决可以节省代码量
- 滑动窗口的外循环条件
相关题目推荐
待补充
59. 螺旋矩阵 Ⅱ
文档讲解:代码随想录 | 数组 | 螺旋矩阵 II
视频讲解:一入循环深似海 | LeetCode:59.螺旋矩阵 II
状态:一遍过!!!
题目链接:59. 螺旋矩阵 II
解题思路-1:模拟法
- 循环不变量原则:左闭右开
- 时间复杂度: O ( n 2 ) O(n^2) O(n2),其中 n n n 是给定的正整数。矩阵的大小是 n × n n×n n×n,需要填入矩阵中的每个元素
- 空间复杂度: O ( 1 ) O(1) O(1),除了返回的矩阵以外,空间复杂度是常数
模拟法
思路
- 可以将矩阵看成若干层,首先输出最外层的元素,其次输出次外层的元素,直到输出最内层的元素
- 对于每层,从左上方开始以顺时针顺序遍历所有元素,由外向内一圈一圈这么画下去
- 填充上行从左到右
- 填充右列从上到下
- 填充下行从右到左
- 填充左列从下到上
左闭右开原则示意图:
这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画!
代码
C++ 代码如下:
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组
int offset = 0; // 外层循环次数
int count = 1; // 用来给矩阵中每一个空格赋值
// 每循环一圈填充两个边,因此循环次数为 n / 2,但若 n 为奇数中间的值需要单独处理
while (offset < n / 2) {
// 下面开始的四个 for 就是模拟转了一圈
// 模拟填充上行从左到右(左闭右开)
for (int x = offset, y = offset; y < n - 1 - offset; y++) {
res[x][y] = count++;
}
// 模拟填充右列从上到下(左闭右开)
for (int x = offset, y = n - 1 - offset; x < n - 1 - offset; x++) {
res[x][y] = count++;
}
// 模拟填充下行从右到左(左闭右开)
for (int x = n - 1 - offset, y = n - 1 - offset; y > offset; y--) {
res[x][y] = count++;
}
// 模拟填充左列从下到上(左闭右开)
for (int x = n - 1 - offset, y = offset; x > offset; x--) {
res[x][y] = count++;
}
offset++;
}
// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
if (n % 2) {
res[n / 2][n / 2] = n * n;
}
return res;
}
};
Python 代码如下:
class Solution:
def generateMatrix(self, n: int) -> List[List[int]]:
nums = [[0] * n for _ in range(n)]
startx, starty = 0, 0 # 起始点
loop, mid = n // 2, n // 2 # 迭代次数、n为奇数时,矩阵的中心点
count = 1 # 计数
for offset in range(1, loop + 1) : # 每循环一层偏移量加1,偏移量从1开始
for i in range(starty, n - offset) : # 从左至右,左闭右开
nums[startx][i] = count
count += 1
for i in range(startx, n - offset) : # 从上至下
nums[i][n - offset] = count
count += 1
for i in range(n - offset, starty, -1) : # 从右至左
nums[n - offset][i] = count
count += 1
for i in range(n - offset, startx, -1) : # 从下至上
nums[i][starty] = count
count += 1
startx += 1 # 更新起始点
starty += 1
if n % 2 != 0 : # n为奇数时,填充中心点
nums[mid][mid] = count
return nums
注意要点
- 模拟过程处理边界条件时应注意循环不变量原则:左闭右开
- 易错点:
n
n
n 为奇数需要单独给矩阵最中间的位置赋值,判断语句条件
n % 2
不需要用n % 2 == 1
,下标索引直接n / 2
即可,会自动取整
相关题目推荐
待补充