2.24~2.25-数组
53. 最大子数组和 - 力扣(LeetCode)
法一:DP思想
定义f[i]表示以nums[i]结尾的最大子数组和。分类讨论:
nums[i]单独组成一个子数组,那么f[i]=nums[i]。
nums[i]和前面的子数组拼起来,也就是在以nums[i−1]结尾的最大子数组和之后添加nums[i],那么f[i]=f[i−1]+nums[i]。
两种情况取最大值,得
\[f[i] = \begin{cases} nums[i], & i = 0 \\ \max(f[i-1], 0) + nums[i], & i \geq 1 \end{cases} \]简单地说,如果nums[i]左边的子数组元素和是负的,就不用和左边的子数组拼在一起了。答案为max(f)。
遍历数组,每次记录当前cnt,如果增大了就更新res,小于0了就置0,相当于从下一个元素开始取。
可以进行空间优化,由于计算f[i]只会用到
f[i−1],不会用到更早的状态,所以可以用一个变量滚动计算。状态转移方程简化为: \(f=max(f,0)+nums[i]\)
f可以初始化成0或者任意负数。class Solution { public: int maxSubArray(vector<int>& nums) { int ans = INT_MIN; // 注意答案可以是负数,不能初始化成 0 int f = 0; for (int x : nums) { f = max(f, 0) + x; ans = max(ans, f); } return ans; } };
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int result = nums[0];
int count = 0;
for (int i = 0; i < nums.size(); i++) {
count += nums[i];
if (count > result) { // 取区间累计的最大值(相当于不断确定最大子序终止位置)
result = count;
}
if (count <= 0) count = 0; // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和。
//遇到类似4 + (-3) 的情况,虽然cnt不会置0 ,但res不会更新
}
return result;
}
};
方法二:前缀和
由于子数组的元素和等于两个前缀和的差,所以求出
nums的前缀和,问题就变成121. 买卖股票的最佳时机了。本题子数组不能为空,相当于一定要交易一次。我们可以一边遍历数组计算前缀和,一边维护前缀和的最小值(相当于股票最低价格),用当前的前缀和(卖出价格)减去前缀和的最小值(买入价格),就得到了以当前元素结尾的子数组和的最大值(利润),用它来更新答案的最大值(最大利润)。
请注意,由于题目要求子数组不能为空,应当先计算前缀和-最小前缀和,再更新最小前缀和。相当于不能在同一天买入股票又卖出股票。
如果先更新最小前缀和,再计算前缀和-最小前缀和,就会把空数组的元素和0算入答案。
示例 1
[−2,1,−3,4,−1,2,1,−5,4]的计算流程如下:以下是 Markdown 格式的表格:
i 前缀和 最小前缀和 前缀和-最小前缀和 0 -2 0 -2 1 -1 -2 1 2 -4 -2 -2 3 0 -4 4 4 -1 -4 3 5 1 -4 5 6 2 -4 67 -3 -4 1 8 1 -4 5 前缀和-最小前缀和的最大值等于6,即为答案。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int ans = INT_MIN;
int min_pre_sum = 0;
int pre_sum = 0;
for(int x : nums){
pre_sum += x;//当前的前缀和
ans = max(ans , pre_sum - min_pre_sum);//减去前缀和的最小值
min_pre_sum = min(min_pre_sum , pre_sum);//维护前缀和的最小值
}
return ans;
}
};
56. 合并区间 - 力扣(LeetCode)
按左端点排序:
以示例 1 为例,我们有 [1,3],[2,6],[8,10],[15,18] 这四个区间。
为方便合并,把区间按照左端点从小到大排序(示例 1 已经按照左端点排序了)。排序的理由会在下面的合并过程中说明。
排序后,我们就知道了第一个合并区间的左端点,即
intervals[0][0]=1。第一个合并区间的右端点是多少?目前只知道其 ≥
intervals[0][1]=3,但具体是多少现在还不确定,得向右遍历。具体算法如下:
- 把 intervals[0] 加入答案。注意,答案的最后一个区间表示当前正在合并的区间。
- 遍历到 intervals[1]=[2,6],由于左端点 2 不超过当前合并区间的右端点 3,可以合并。由于右端点 6>3,那么更新当前合并区间的右端点为 6。注意,由于我们已经按照左端点排序,所以 intervals[1] 的左端点 2 必然大于等于合并区间的左端点,所以无需更新当前合并区间的左端点。
- 遍历到 intervals[2]=[8,10],由于左端点 8 大于当前合并区间的右端点 6,无法合并(两个区间不相交)。再次利用区间按照左端点排序的性质,更后面的区间的左端点也大于 6,无法与当前合并区间相交,所以当前合并区间 [1,6] 就固定下来了,把新的合并区间 [8,10] 加入答案。
- 遍历到 intervals[3]=[15,18],由于左端点 15 大于当前合并区间的右端点 10,无法合并(两个区间不相交),我们找到了一个新的合并区间 [15,18] 加入答案。
- 上述算法同时说明,按照左端点排序后,合并的区间一定是 intervals 中的连续子数组。
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
ranges::sort(intervals); // 按照左端点从小到大排序
vector<vector<int>> ans;
for (auto& p : intervals) {
if (!ans.empty() && p[0] <= ans.back()[1]) { // 当前元素左端点小于答案最后一个元素的右端点,可以合并
ans.back()[1] = max(ans.back()[1], p[1]); // 更新右端点最大值
} else { // 不相交,无法合并
ans.emplace_back(p); // 新的合并区间
}
}
return ans;
}
};
复杂度分析
- 时间复杂度:O(nlogn),其中 n 是 intervals 的长度。瓶颈在排序上。
- 空间复杂度:O(1)。排序的栈开销和返回值不计入。
补充:
ranges::sort是 C++20 引入的一个算法,用于对范围中的元素进行排序。它的比较规则如下:
自定义比较器:可以通过
comp参数传递自定义比较器,例如std::ranges::greater(降序排序)或用户定义的比较函数对象。默认比较器:
ranges::sort的默认比较器是std::ranges::less,它会对元素进行升序排序。std::vector<int> vec = {5, 3, 1, 4, 2}; std::ranges::sort(vec); // 默认升序排序降序排序:
std::ranges::sort(vec, std::ranges::greater{}); // 降序排序自定义比较器和投影:
std::vector<std::string> words = {"apple", "banana", "cherry", "date"}; std::ranges::sort( words, [](const auto& a, const auto& b) { return a.size() < b.size(); }, // 按字符串长度排序 [](const std::string& s) { return s.size(); } // 投影为字符串长度 );
- 范围要求:
ranges::sort要求输入范围必须是随机访问范围(如std::vector、数组),不支持单向迭代器(如std::forward_list)。- 排序稳定性:
ranges::sort是不稳定排序,相等元素的相对顺序可能改变。如果需要稳定排序,可以使用std::ranges::stable_sort。二维数组排序的情况
默认情况下,
ranges::sort会按照字典序(lexicographical order)对子数组进行排序。在这种情况下,排序的依据是子数组的 “大小”(即整体比较)。vec = {{5, 3}, {1, 4}, {2, 6}};输出结果:
1 4 2 6 5 3如果你希望按照二维数组中每个子数组的特定元素或某个条件进行排序,可以通过传递自定义比较器
示例1:按子数组的第一个元素排序
vector<vector<int>> vec = {{5, 3}, {1, 4}, {2, 6}}; ranges::sort(vec, [](const auto& a, const auto& b) { return a[0] < b[0]; }); }示例 2:按子数组的和排序
vector<vector<int>> vec = {{5, 3} ,{1, 4},{2, 6},{3, 5}}; ranges:sort(vec , [](const auto& a , const auto& b){ return accumulate(a.begin() , a.end() , 0) < accumulate(b.begin() , b.end() , 0); });
189. 轮转数组 - 力扣(LeetCode)
三次翻转(类似链表)
把 [1,2,3,4,5,6,7] 变成 [5,6,7,1,2,3,4],首先要保证 5,6,7 在 1,2,3,4 前面,这可以通过反转整个数组做到。
反转后数组变成 [7,6,5,4,3,2,1],对比最终目标可以发现,前三个数需要反转,后四个数需要反转,这样就得到了 [5,6,7,1,2,3,4]。
轮转 k 次等于轮转 k % n 次,于是先将k %= n
class Solution {
public:
void rotate(vector<int>& nums, int k) {
k %= nums.size(); // 轮转 k 次等于轮转 k % n 次
ranges::reverse(nums);
reverse(nums.begin(), nums.begin() + k);
reverse(nums.begin() + k, nums.end());
}
};
//手写reverse函数的版本
class Solution {
public:
void rotate(vector<int>& nums, int k) {
auto reverse = [&](int i, int j) {
while (i < j) {
swap(nums[i++], nums[j--]);
}
};
int n = nums.size();
k %= n; // 轮转 k 次等于轮转 k % n 次
reverse(0, n - 1);//reverse函数捕获全部引用,不需要加nums[0]
reverse(0, k - 1);
reverse(k, n - 1);
}
};
238. 除自身以外数组的乘积 - 力扣(LeetCode)
前后缀分解
answer[i] 等于 nums 中除了 nums[i] 之外其余各元素的乘积。换句话说,如果知道了 i 左边所有数的乘积,以及 i 右边所有数的乘积,就可以算出 answer[i]。
于是:
定义
pre[i]表示从nums[0]到nums[i−1]的乘积。定义
suf[i]表示从nums[i+1]到nums[n−1]的乘积。我们可以先计算出从
nums[0]到nums[i−2]的乘积pre[i−1],再乘上nums[i−1],就得到了pre[i],即 \(pre[i]=pre[i−1]⋅nums[i−1]\)
同理有 \(suf[i]=suf[i+1]⋅nums[i+1]\)
初始值:\(pre[0]=suf[n−1]=1\)。因为 1 乘以任何数 x 都等于 x,这样可以方便递推计算pre[1],suf[n−2]等。算出 pre 数组和 suf 数组后,有
\(answer[i]=pre[i]⋅suf[i]\)
//优化前
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
vector<int> pre(n , 1);
for (int i = 1; i < n; i++) {
pre[i] = pre[i - 1] * nums[i - 1];
}
vector<int> suf(n , 1);
for(int i = n - 2 ; i >= 0 ; i --){
suf[i] = suf[i + 1] * nums[i + 1];
}
vector<int> ans(n);
for (int i = 0; i < n; i++) {
ans[i] = pre[i] * suf[i];
}
return ans;
}
};
复杂度分析
- 时间复杂度:O(n),其中 n 是 nums 的长度。
- 空间复杂度:O(n)。
优化:不使用额外空间
先计算 suf,然后一边计算 pre,一边把 pre 直接乘到
suf[i]中。最后返回 suf。
题目说「输出数组不被视为额外空间」,所以该做法的空间复杂度为 O(1)。此外,这种做法比上面少遍历了一次。
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
vector<int> suf(n);
suf[n - 1] = 1;
for (int i = n - 2; i >= 0; i--) {
suf[i] = suf[i + 1] * nums[i + 1];
}
int pre = 1;
for (int i = 0; i < n; i++) {
// 此时 pre 为 nums[0] 到 nums[i-1] 的乘积,直接乘到 suf[i] 中
suf[i] *= pre;
//从前往后更新suf不会影响答案
pre *= nums[i];
}
return suf;//最后返回suf数组即可
}
};
复杂度分析
- 时间复杂度:O(n),其中 n 是 nums 的长度。
- 空间复杂度:O(1)。返回值不计入。
41. 缺失的第一个正数 - 力扣(LeetCode)
原地哈希
由于题目要求我们「只能使用常数级别的空间」,分析题意可知:
要找的数一定在 [1, N + 1] 左闭右闭(这里 N 是数组的长度)这个区间里
因此,我们可以就把原始的数组当做哈希表来使用。事实上,哈希表其实本身也是一个数组;
我们要找的数就在
[1, N + 1]里,最后N + 1这个元素我们不用找。因为在前面的N个元素都找不到的情况下,我们才返回N + 1;我们可以采取这样的思路:就把 1 这个数放到下标为 0 的位置, 2 这个数放到下标为 1 的位置,按照这种思路整理一遍数组。
然后我们再遍历一次数组,第 1一个遇到的它的值不等于下标的那个数,就是我们要找的缺失的第一个正数。
这个思想就相当于我们自己编写哈希函数,这个哈希函数的规则特别简单,那就是数值为 i 的数映射到下标为 i - 1 的位置。
class Solution {
public:
int firstMissingPositive(vector<int> &nums) {
for (int i = 0; i < nums.size(); i++) {
while (nums[i] != i + 1) {
if (nums[i] <= 0 || nums[i] > nums.size() || nums[i] == nums[nums[i] - 1])
break;
// 将nums[i] 放置到对应位置上[1,2,3...]
swap(nums[i] , nums[nums[i] - 1]);
}
}
for (int i = 0; i < nums.size(); i++) {
if (nums[i] != (i + 1)) {
return (i + 1);
}
}
return (nums.size() + 1);
}
};




浙公网安备 33010602011771号