1.最大子数组和
思路分析:这个问题可以通过动态规划来解决,我们可以使用Kadane’s Algorithm(卡登算法)来找到具有最大和的连续子数组。
-
Kadane’s Algorithm 的核心思想是利用一个变量存储当前的累加和 currentSum,并通过每次与之前的最大子数组和 maxSum 比较,更新 maxSum。它在遍历过程中不断累加子数组的和,但一旦累加和变为负数时,放弃当前累加,从下一个元素重新开始累加。这样确保了子数组和始终是最大的。
-
初始化:将 maxSum 和 currentSum 初始化为数组的第一个元素,即 nums[0]
-
遍历数组
- 从第二个元素开始,逐个元素检查累加currentSum;
- currentSum = max(nums[i], currentSum + nums[i]):选择当前元素或当前累加和加上当前元素两者中的较大值。
- 如果 currentSum 大于 maxSum,则更新 maxSum。
- 从第二个元素开始,逐个元素检查累加currentSum;
-
返回结果:遍历完成后,maxSum 即为最大子数组的和。
具体实现代码(详解版):
class Solution {
public:
int maxSubArray(std::vector<int>& nums) {
int maxSum = nums[0]; // 初始化 maxSum 为数组第一个元素,用于存储最大子数组的和
int currentSum = nums[0]; // 初始化 currentSum 为数组第一个元素,用于当前子数组的累加和
// 遍历数组,从第二个元素开始(因为第一个元素已经初始化了)
for (int i = 1; i < nums.size(); i++) {
// 更新 currentSum,将当前元素和 currentSum + 当前元素 取较大值
// 如果 currentSum + nums[i] 比 nums[i] 小,说明重新开始一个新的子数组会更大
currentSum = max(nums[i], currentSum + nums[i]);
// 更新 maxSum,存储最大子数组的和
maxSum = max(maxSum, currentSum);
}
// 返回最大子数组的和
return maxSum;
}
};
2.合并区间
思路分析1(Acwing板子)
算法基础课-区间合并
具体实现代码(详解版):
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
vector<vector<int>> res; // 用于存放合并后的区间结果
sort(intervals.begin(), intervals.end()); // 按区间起点升序排序
// 初始化st和ed为一个极小的值,用于在第一次遍历时识别有效区间
int st = -2e9, ed = -2e9;
// 遍历所有区间
for (int i = 0; i < intervals.size(); i++) {
// 如果当前区间的起点大于当前的结束位置,则无重叠
if (ed < intervals[i][0]) {
// 如果是有效区间,将前一个区间加入结果中
if (st != -2e9) res.push_back({st, ed});
// 更新st和ed为当前区间的起点和终点
st = intervals[i][0];
ed = intervals[i][1];
} else {
// 当前区间与前一个区间重叠,合并区间
ed = max(ed, intervals[i][1]);
}
}
// 添加最后一个区间到结果中
if (st != -2e9) res.push_back({st, ed});
return res;
}
};
思路分析2:直接遍历区间数组,判断是否有重叠。
具体实现代码(详解版):
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
// 按区间的起始位置排序
sort(intervals.begin(), intervals.end());
// 用于存放合并后的结果
vector<vector<int>> res;
// 遍历区间数组
for (const auto& interval : intervals) {
// 如果结果数组为空或当前区间不与最后一个区间重叠,直接加入
if (res.empty() || res.back()[1] < interval[0]) {
res.push_back(interval);
} else {
// 否则,有重叠,更新最后一个区间的终点
res.back()[1] = max(res.back()[1], interval[1]);
}
}
return res;
}
};
3.轮转数组
思路分析1:将后面k个元素和前面n-k个元素添加到res中即可。要注意k = k % n
具体实现代码(详解版):
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int n = nums.size();
k = k % n; // 如果 k 大于数组长度,取余数来简化操作
vector<int> res;
// 将最后 k 个元素加入结果中
for (int i = n - k; i < n; ++i)
res.push_back(nums[i]);
// 然后把前 n - k 个元素加在结果后面
for (int i = 0; i < n - k; ++i)
res.push_back(nums[i]);
// 将旋转后的结果更新到原数组
nums = res;
}
};
思路分析2:直接进行反转,先整体反转,再将将前 k 个元素反转,再将剩余的 n-k 个元素反转
具体实现代码(详解版):
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int n = nums.size();
k = k % n; // 计算实际旋转步数,防止 k 超出数组长度
reverse(nums.begin(), nums.end()); // 1. 反转整个数组
reverse(nums.begin(), nums.begin() + k); // 2. 反转前 k 个元素
reverse(nums.begin() + k, nums.end()); // 3. 反转剩余的 n-k 个元素
}
};
很明显第二种方法更优!太快了!
4.除自身以外数组的乘积
思路分析:使用两个数组:一个数组存储每个元素左侧所有元素的乘积,另一个数组存储每个元素右侧所有元素的乘积。通过将这两个数组的对应元素相乘,得到每个位置的最终结果。
- 初始化:answer 数组初始化为 1,因为乘法的单位是 1
- 左侧乘积:
- 我们使用一个变量 left_product 来存储当前元素左侧的乘积。
- 对于每个元素 nums[i],我们将 left_product 的值赋给 answer[i],这表示在位置 i 的左侧乘积
- 然后,更新 left_product,将 nums[i] 的值乘入,准备计算下一个位置。
- 右侧乘积:我们继续使用之前的 answer 数组,它现在包含了每个元素左侧的乘积。接下来,我们将计算每个元素右侧的乘积。
- 使用一个变量 right_product 来存储当前元素右侧的乘积,并将其初始化为 1。
- 反向遍历
- 对于每个元素 nums[i],将 right_product 的值乘入 answer[i],这样 answer[i] 就等于左侧乘积乘以右侧乘积
- 更新 right_product,将 nums[i] 的值乘入,准备计算下一个位置。
具体实现代码(详解版):
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
vector<int> answer(n, 1); // 初始化结果数组为 1
// 1. 计算左侧的乘积
int left_product = 1; // 初始化左侧乘积
for (int i = 0; i < n; i++) {
answer[i] = left_product; // 设置 answer[i] 为左侧乘积
left_product *= nums[i]; // 更新左侧乘积
}
// 2. 计算右侧的乘积并更新结果
int right_product = 1; // 初始化右侧乘积
for (int i = n - 1; i >= 0; i--) {
answer[i] *= right_product; // 将右侧乘积乘以之前的结果
right_product *= nums[i]; // 更新右侧乘积
}
return answer; // 返回最终结果
}
};
另一种写法:
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
vector<int> answer(n, 1); // 初始化结果数组
// 计算左侧乘积
for (int i = 1; i < n; ++i) {
answer[i] = answer[i - 1] * nums[i - 1];
}
// 计算右侧乘积并更新 answer
int right_product = 1;
for (int i = n - 1; i >= 0; --i) {
answer[i] *= right_product; // 将右侧乘积乘入结果
right_product *= nums[i]; // 更新右侧乘积
}
return answer;
}
};
还有这种双指针的做法,也是大为简洁!值得借鉴!
std::vector<int> productExceptSelf2(std::vector<int>& nums) {
std::vector<int> answer(nums.size(), 1); // 初始化输出数组
int left = 0, right = nums.size() - 1; // 指针初始化
int lp = 1, rp = 1; // 左侧乘积和右侧乘积
// 双指针法
while (right >= 0 && left < nums.size()) {
answer[right] *= rp; // 更新右侧元素的乘积
answer[left] *= lp; // 更新左侧元素的乘积
lp *= nums[left++]; // 更新左侧乘积
rp *= nums[right--]; // 更新右侧乘积
}
return answer; // 返回结果
}
5.缺失的第一个正数
思路分析:要在时间复杂度O(n) 和常数级空间复杂度下找到未排序数组 nums 中最小的缺失正整数,我们可以采用原地哈希(或称为置换排序)的方式。具体思路如下:
- 目标:我们的目标是将每个正整数 x 放置在下标 x - 1 的位置上(例如,数字 1 应该放在下标 0 的位置,数字 2 应该放在下标 1,依此类推)。这样,如果数组中有数字 1, 2, …, n,它们都会出现在各自对应的位置上。
- 交换过程
- 遍历数组,如果nums[i]在[1,n]范围内,并且nums[i] != nums[nums[i] - 1],则将nums[i]放到其正确的位置num[i] -1。通过交换的方式来调整位置。
- 不断重复交换,直至当前位置的数字被放到了正确的位置为止。此过程会在 O ( n ) O(n) O(n)时间内完成,因为每个元素最多只会被交换一次
- 查找缺失的正整数
- 再次遍历数组,找到第一个位置i,使得nums[i] != i + 1,此时i + 1就是我们要找的最小的缺失正整数
- 如果所有位置都满足nums[i] == i + 1,那么数组中的所有正整数[1,n]都出现过,即最小缺失正整数为n + 1.
一个结论:不论数组 nums 是什么内容,数组长度为 N 时,最小缺失的正整数一定落在 [1, N+1]
这个范围内。这就是为什么算法中只需要关注 [1, N+1],而无需遍历其他更大的数字的原因。
具体实现代码(详解版):
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int n = nums.size();
//将每个数放到对应的位置
for(int i = 0 ; i < n ; i ++){
while(nums[i] > 0 && nums[i] <= n
&& nums[i] != nums[nums[i] - 1] ){
swap(nums[i],nums[nums[i] - 1]);
}
}
//查找第一个位置i,使得nums[i] != i + 1
for(int i = 0; i < n ; i ++){
if(nums[i] != i + 1){
return i + 1;
}
}
//如果所有位置都正确,则返回 n + 1
return n + 1;
}
};