题目链接:https://leetcode.cn/problems/squares-of-a-sorted-array/
给你一个按 非递减顺序 排序的整数数组
nums
,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。示例 1:
输入:nums = [-4,-1,0,3,10] 输出:[0,1,9,16,100] 解释:平方后,数组变为 [16,1,0,9,100] 排序后,数组变为 [0,1,9,16,100]
示例 2:
输入:nums = [-7,-3,2,3,11] 输出:[4,9,9,49,121]
提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums
已按 非递减顺序 排序进阶:
- 请你设计时间复杂度为
O(n)
的算法解决本问题
拿到这题,我首先没有想到双指针法,我首先是按照408算法题的思路来解决的,快排是应用最广的排序算法,平均复杂度比较优秀,适用于乱序数组的排序操作,下面是完美复刻王道咸鱼学长所讲的快排模板代码:
def quickSort(arr, low, high):
if low >= high: # 递归结束条件
return
mid = partition(arr, low, high) # 以第一个元素作为基准
quickSort(arr, low, mid - 1)
quickSort(arr, mid + 1, high)
def partition(arr, low, high):
pivot = arr[high] # 选择最后一个元素作为基准
while(low < high):
while(low < high and arr[low] <= pivot): # 从左往右找到第一个大于pivot的元素
low += 1
arr[high] = arr[low] # 将该元素放到右边
while(low < high and arr[high] >= pivot): # 从右往左找到第一个小于pivot的元素
high -= 1
arr[low] = arr[high] # 将该元素放到左边
arr[low] = pivot # 将基准放到中间
return low
def sortedSquares(nums):
squares = [num ** 2 for num in nums]
quickSort(squares, 0, len(squares) - 1) # 快速排序
return squares
if __name__ == "__main__":
nums = list(map(int, input().split()))
print(sortedSquares(nums))
快速排序在平均情况下的性能非常好(时间复杂度为 O(n log n)),但在最坏情况下(例如本题中如果输入数组全部都为非负整数时)其性能可能下降到 O(n^2),用来应付408算法题绰绰有余。
下面贴出王道咸鱼学长上课讲的模板:
虽然说上述代码直接使用sort()函数更好更简约,因为sort()函数内置更高效和更稳定的排序算法(如归并排序或 Timsort),但是在408中是不能直接调库实现的,而且面试中面试官也不愿意看见这样的代码,所以最好是手写一个排序代码用来实现。
在上一期博客中我们详细学习了双指针法的实现过程,这次我们继续利用双指针法来解决这道题目,这道题目有一个进阶要求就是:请你设计时间复杂度为 O(n)
的算法解决本问题,刚好双指针法就可以满足这个要求。(我打算以后都在力扣的核心代码模式下然后自己加入输入输出代码,如果想要去力扣上执行通过的话,只需要删除对应的输入输出代码即可)
(1)Python版本实现代码
class Solution:
def sortedSquares(self, nums):
left, right = 0, len(nums) - 1 # 左闭右开
result = [0] * len(nums)
for i in range(len(nums)-1, -1, -1): # 从后往前填充
if abs(nums[left]) > abs(nums[right]):
result[i] = nums[left] ** 2
left += 1
else:
result[i] = nums[right] ** 2
right -= 1
return result
if __name__ == "__main__":
nums = list(map(int, input().split()))
solution = Solution()
print(solution.sortedSquares(nums))
这里可能有人会好奇,为什么要从后往前遍历,即从 len(nums) - 1
开始倒序填充数组,这是因为平方最大的数可能在数组的两端。但是写代码的时候我也考虑过,如果从前面开始遍历的话,代码应该怎么写?
我尝试过修改上面的代码,但是由于水平有限,没能成功,因此我就去搜索了一番,下面是从前开始遍历的实现代码:
class Solution:
def sortedSquares(self, nums):
n = len(nums)
result = [0] * n
# 找到第一个非负数的位置
first_non_negative = 0
while first_non_negative < n and nums[first_non_negative] < 0:
first_non_negative += 1
i, j = first_non_negative - 1, first_non_negative # i指向左边,j指向右边
k = 0
# 从中间向两边遍历
while i >= 0 and j < n:
if nums[i] ** 2 < nums[j] ** 2:
result[k] = nums[i] ** 2
i -= 1
else:
result[k] = nums[j] ** 2
j += 1
k += 1
# 如果左边还有元素
while i >= 0:
result[k] = nums[i] ** 2
i -= 1
k += 1
# 如果右边还有元素
while j < n:
result[k] = nums[j] ** 2
j += 1
k += 1
return result
if __name__ == "__main__":
nums = list(map(int, input().split()))
solution = Solution()
print(solution.sortedSquares(nums))
如果选择从前面开始遍历数组,使用双指针法也是可能的,但这将需要一些额外的步骤,主要的问题是处理正负数平方后的排序顺序,因为负数平方后可能变得比正数大。
上面代码的实现是先找到第一个非负数的位置,这个位置将数组分成两部分:左边是负数(平方后递减),右边是非负数(平方后递增),然后像归并排序中合并两个有序数组那样,使用两个指针分别从这个分界点向左和向右遍历数组,比较并选择较小的平方数添加到结果数组中。
其中i
从分界点的左侧开始向左遍历,j
从分界点的右侧开始向右遍历。我们比较 nums[i]
和 nums[j]
的平方值,将较小的那个添加到结果数组 result
中。这种方法虽然也是 O(n) 的时间复杂度,但相比直接从两端开始的双指针法,它需要更多的步骤和计算。
(2)C++版本实现代码
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
vector<int> result(nums.size());
for (int i = nums.size() - 1; i >= 0; i--) {
if (abs(nums[left]) > abs(nums[right])) {
result[i] = nums[left] * nums[left];
left++;
} else {
result[i] = nums[right] * nums[right];
right--;
}
}
return result;
}
};
int main() {
Solution solution;
vector<int> nums;
int num;
while (cin >> num) {
nums.push_back(num);
if (cin.get() == '\n') break;
}
vector<int> result = solution.sortedSquares(nums);
for (int i : result) {
cout << i << " ";
}
cout << endl;
return 0;
}
题目链接:https://leetcode.cn/problems/minimum-size-subarray-sum/description/
给定一个含有
n
个正整数的数组和一个正整数target
。找出该数组中满足其总和大于等于
target
的长度最小的 连续子数组[numsl, numsl+1, ..., numsr-1, numsr]
,并返回其长度**。**如果不存在符合条件的子数组,返回0
。示例 1:
输入:target = 7, nums = [2,3,1,2,4,3] 输出:2 解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4] 输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1] 输出:0
提示:
1 <= target <= 109
1 <= nums.length <= 105
1 <= nums[i] <= 105
进阶:
- 如果你已经实现
O(n)
时间复杂度的解法, 请尝试设计一个O(n log(n))
时间复杂度的解法。
暴力解法就是使用两个嵌套for循环不断地遍历数组,寻找符合条件地子数组,时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
class Solution:
def minSubArrayLen(self, target, nums):
result = float('inf') # 初始化为正无穷
sum = 0 # 子序列的数值之和
subLength = 0 # 子数组长度
for i in range(len(nums)):
sum = 0; # 每次循环都要重新初始化
for j in range(i, len(nums)):
sum += nums[j]
if sum >= target: # 如果和大于等于target
subLength = j - i + 1 # 计算子数组长度
result = min(result, subLength) # 一旦发现子序列和超过了s,更新result
break
return result if result != float('inf') else 0 # 如果result没有被更新过,返回0
if __name__ == "__main__":
target = int(input())
nums = list(map(int, input().split()))
solution = Solution()
print(solution.minSubArrayLen(target, nums))
其中外层循环从数组的第一个元素开始遍历,直到数组的最后一个元素,这个循环的目的是确定子数组的起始位置。内层循环对于每个外层循环选定的起始位置 i
,内层循环从 i
开始,逐渐向数组的末尾扩展,这个循环的目的是确定子数组的结束位置,逐步将元素加到 sum
上,并检查 sum
是否达到或超过了目标值 target
。
C++代码我就直接贴出卡哥的代码随想录的代码,和上面的Python代码是类似的。
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;
}
};
滑动窗口是一种常用于解决数组或字符串相关问题的方法,特别适用于需要处理连续数据块的场景,其基本思想是在数据的一段连续区域内移动,同时根据问题的需要调整窗口的大小,以下是滑动窗口的一般实现过程:
(1) 初始化窗口:
left
和 right
,分别表示窗口的开始和结束位置,初始时两个指针都指向数组的起始位置。(2) 扩展窗口:
right
指针向右扩展窗口,直到窗口内的数据满足特定条件,在这个步骤涉及不断添加 right
指向的元素到当前考虑的数据集合中。(3) 收缩窗口:
left
指针以缩小窗口,在这个过程通常涉及从考虑的数据集合中移除 left
指向的元素。目的是看看是否能够在满足条件的情况下减小窗口的大小。(4) 更新结果:
(5) 重复以上步骤:
right
指针到达数组的末尾。下面是本题利用滑动窗口思想实现的代码:
(1)Python版本实现代码:
class Solution:
def minSubArrayLen(self, target: int, nums):
n = len(nums)
min_len = float('inf')
sum = 0 # 子序列的数值之和
left = 0 # 滑动窗口的左指针
for right in range(n): # 滑动窗口
sum += nums[right]
while sum >= target: # 当子序列和大于等于target时
min_len = min(min_len, right - left + 1) # 更新最小长度
sum -= nums[left] # 左指针右移
left += 1 # 更新左指针
return 0 if min_len == float('inf') else min_len # 如果min_len没有被更新过,返回0
if __name__ == "__main__":
target = int(input())
nums = list(map(int, input().split()))
solution = Solution()
print(solution.minSubArrayLen(target, nums))
(2)C++版本实现代码:
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int n = nums.size();
int min_len = INT_MAX;
int sum = 0;
int left = 0;
for (int right = 0; right < n; right++) {
sum += nums[right];
while (sum >= target) {
min_len = min(min_len, right - left + 1);
sum -= nums[left];
left++;
}
}
return (min_len != INT_MAX) ? min_len : 0;
}
};
int main() {
Solution solution;
int target, n;
cin >> target;
cin >> n;
vector<int> nums(n);
for (int i = 0; i < n; i++) {
cin >> nums[i];
}
cout << solution.minSubArrayLen(target, nums) << endl;
return 0;
}
在实现滑动窗口时,通常只需要一个 for
循环来表示滑动窗口的结束位置,通过单循环控制结束位置,同时在循环内调整起始位置,可以有效地遍历所有可能的窗口,同时保持时间复杂度为 O(n)。那么为什么for
循环必须要表示滑动窗口的结束位置而不能表示滑动窗口的起始位置原因在于如何有效地探索所有可能的窗口大小。
(1)为什么用 for
循环表示结束位置:
for
循环来递增结束位置时,我们实际上是在逐步增大窗口的大小,这样我们可以保证覆盖所有可能的窗口大小,从最小的窗口(起始和结束位置相同)开始,逐渐增大,直到覆盖整个数组。target
),我们可以尝试通过移动起始位置来减小窗口大小,同时继续保持总和条件满足,这种动态调整使得算法可以在遍历数组的同时不断更新可能的最优解。(2)为什么不用 for
循环表示起始位置:
for
循环来递增起始位置,我们将不得不在每次迭代中使用另一个循环(或类似机制)来找到满足条件的结束位置,这种方法会导致重复遍历某些数组元素,增加了不必要的计算,从而使得时间复杂度可能上升到
O
(
n
2
)
O(n^2)
O(n2)。由于本人实力不够,水平不足还无法实现进阶的要求,不过我们有强大的互联网,我上网搜索了该方法,可将时间复杂度降至O(n log(n)),那就是使用前缀和加二分查找的方法,这种方法的核心思想是首先计算数组的前缀和,然后对于每个前缀和,使用二分查找找到最小的索引,使得从这个索引到当前索引的子数组的和至少为 target
。
具体实现步骤如下:
prefixSums
,其中 prefixSums[i]
表示原数组 nums
中从第 0 个元素到第 i
个元素的和。O(n)
。prefixSums
中的每个元素,使用二分查找找到最小的索引 j
,使得 prefixSums[i] - prefixSums[j] >= target
(即子数组 nums[j+1...i]
的和至少为 target
)。O(log(n))
的时间复杂度,因此总的时间复杂度是 O(n log(n))
。下面是使用 Python 实现的代码:
import bisect
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
n = len(nums)
if n == 0:
return 0
min_length = float('inf')
prefixSums = [0] * (n + 1)
for i in range(n):
prefixSums[i + 1] = prefixSums[i] + nums[i]
for i in range(1, n + 1):
required_sum = target + prefixSums[i - 1]
bound = bisect.bisect_left(prefixSums, required_sum)
if bound != len(prefixSums):
min_length = min(min_length, bound - (i - 1))
return 0 if min_length == float('inf') else min_length
if __name__ == "__main__":
target = int(input())
nums = list(map(int, input().split()))
solution = Solution()
print(solution.minSubArrayLen(target, nums))
在这个实现中首先计算了数组 nums
的前缀和 prefixSums
,然后对于每个前缀和,使用 Python 内置的 bisect.bisect_left
函数来执行二分查找,这个方法找到满足条件的最短子数组长度,同时保持整体的时间复杂度为 O(n log(n))
,学有余力的朋友可以研究一下,我打算后续在学习,目前贴出来仅供参考学习。
题目链接:https://leetcode.cn/problems/spiral-matrix-ii/description/
给你一个正整数
n
,生成一个包含1
到n2
所有元素,且元素按顺时针顺序螺旋排列的n x n
正方形矩阵matrix
。示例 1:
输入:n = 3 输出:[[1,2,3],[8,9,4],[7,6,5]]
示例 2:
输入:n = 1 输出:[[1]]
提示:
1 <= n <= 20
这种题408目前没有出现,但是蓝桥杯应该挺喜欢出的,纯模拟过程,但是对于这种题目目前我还没有经过训练,所以实现起来很难,目前会写的也就是更408算法题相关的那些相似的题目,模拟题目不涉及算法,但是十分考察对代码的掌控能力,面试中应该也容易出现,后续周末我再好好研究一下这道题目,再更新出我自己的实现思路。
下面我就直接给出卡哥的代码随想录这道题目的地址:59.螺旋矩阵II,大家可以去代码随想录里面学习这道题目,卡哥讲的非常详细还有对应的讲解视频,我在这里就只贴出代码随想录上的代码,然后我帮忙加上输入和输出的代码,方便各位朋友调试。
(1)Python版本:
class Solution:
def generateMatrix(self, n):
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
if __name__ == "__main__":
n = int(input())
solution = Solution()
matrix = solution.generateMatrix(n)
for row in matrix:
print(" ".join(map(str, row)))
(2)C++版本:
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;
}
};
int main() {
Solution solution;
int n;
cin >> n;
vector<vector<int>> matrix = solution.generateMatrix(n);
for (auto &row : matrix) {
for (int val : row) {
cout << val << " ";
}
cout << endl;
}
return 0;
}
数组是一种基本的数据结构,用于存储一系列相同类型的元素,数组中的元素可以通过索引快速访问,在大多数编程语言中,数组的索引通常从0开始,数组的主要特点包括:
在408选择题、应用题以及算法题就很喜欢涉及有关数组的问题,因此大家请务必掌握有关数组的一些实用算法。
数组是最基本也是最常用的数据结构之一,熟练掌握各种数组操作技巧和算法对于提高编程能力至关重要,在实际应用中,选择合适的算法和数据结构是关键,例如对于搜索问题,如果数组是有序的,那么二分查找是一个很好的选择,对于需要频繁修改或动态扩展大小的数据集,可以考虑使用其他数据结构,如链表或动态数组,通过解决经典题目,如二分查找、双指针、滑动窗口和模拟等,我们可以加深对数组及其相关算法的理解,不断实践和挑战更复杂的问题将有助于提升解决实际问题的能力。