1. 最大子数组和
题目描述
给你一个整数数组 nums
,请你找出数组中连续子数组(最少包含一个元素)的最大和。
解题思路
💡 核心技巧:动态规划 + 状态压缩
与滑动窗口不同,本题不能用双指针,因为数组包含负数,子数组和不具有单调性。
关键洞察:
- 定义
dp[i]
为以nums[i]
结尾的最大子数组和 - 状态转移方程:
dp[i] = max(nums[i], dp[i-1] + nums[i])
- 无需存储整个
dp
数组,用sum
变量实时更新
✅ 为什么有效 :当
sum
变为负数时,说明以当前元素结尾的子数组和不如从下一个元素开始,因此重置sum = num
。
代码实现
javascript
/**
* @param {number[]} nums
* @return {number}
*/
var maxSubArray = function(nums) {
let ans = -Infinity;
let sum = 0;
for (let num of nums) {
// 选择:从当前元素开始,还是延续之前子数组
sum = Math.max(sum + num, num);
ans = Math.max(ans, sum);
}
return ans;
};
复杂度分析
- 时间复杂度:O(n),单次遍历
- 空间复杂度:O(1),仅用常数额外空间
2. 合并区间
题目描述
给定一组区间,合并所有重叠的区间。
解题思路
💡 核心技巧:排序 + 一次遍历
区间合并的关键在于排序,让重叠区间在遍历中自然出现。
关键洞察:
- 按区间起始位置排序(
intervals.sort((a, b) => a[0] - b[0])
) - 初始化结果数组,放入第一个区间
- 遍历后续区间:
- 若当前区间与结果数组最后一个区间重叠(
current[0] <= last[1]
),则合并 - 否则,将当前区间加入结果
- 若当前区间与结果数组最后一个区间重叠(
✅ 为什么排序是关键:排序后,重叠区间必然相邻,避免了 O(n²) 的暴力检查。
代码实现
javascript
/**
* @param {number[][]} intervals
* @return {number[][]}
*/
var merge = function(intervals) {
if (intervals.length === 0) return [];
// 按起始位置升序排序
intervals.sort((a, b) => a[0] - b[0]);
const merged = [intervals[0]];
for (let i = 1; i < intervals.length; i++) {
const current = intervals[i];
const lastMerged = merged[merged.length - 1];
// 检查重叠:当前区间起始 <= 上一个合并区间的结束
if (current[0] <= lastMerged[1]) {
// 合并:更新结束位置为两者的最大值
lastMerged[1] = Math.max(lastMerged[1], current[1]);
} else {
// 无重叠,直接加入
merged.push(current);
}
}
return merged;
};
复杂度分析
- 时间复杂度:O(n log n),排序占主导
- 空间复杂度:O(n),结果数组最多存储 n 个区间
3. 旋转数组
题目描述
给定一个整数数组 nums
和一个整数 k
,将数组向右旋转 k 次。
解题思路
💡 核心技巧:三次反转
无需额外空间,仅通过数组反转实现旋转。
关键洞察:
- 整体反转:
[1,2,3,4,5]
→[5,4,3,2,1]
- 反转前 k 个元素:
[5,4]
→[4,5]
→[4,5,3,2,1]
- 反转剩余元素:
[3,2,1]
→[1,2,3]
→[4,5,1,2,3]
代码实现
javascript
/**
* @param {number[]} nums
* @param {number} k
* @return {void} Do not return anything, modify nums in-place instead.
*/
var rotate = function(nums, k) {
const n = nums.length;
k %= n; // 避免无效旋转
if (k === 0) return;
const reverse = (start, end) => {
while (start < end) {
[nums[start], nums[end]] = [nums[end], nums[start]];
start++;
end--;
}
};
reverse(0, n - 1); // 整体反转
reverse(0, k - 1); // 前 k 个反转
reverse(k, n - 1); // 后面部分反转
};
复杂度分析
- 时间复杂度:O(n),三次反转各 O(n)
- 空间复杂度:O(1),原地操作
4. 除自身以外的数组乘积
题目描述
给你一个整数数组 nums
,返回一个数组 answer
,其中 answer[i]
是 nums
中除 nums[i]
以外所有元素的乘积。
解题思路
💡 核心技巧:前缀乘积 × 后缀乘积
关键洞察:
ans[i] = (nums[0] * ... * nums[i-1]) * (nums[i+1] * ... * nums[n-1])
- 用
pre
数组存前缀乘积,suf
数组存后缀乘积
代码实现
javascript
/**
* @param {number[]} nums
* @return {number[]}
*/
var productExceptSelf = function(nums) {
const n = nums.length;
const ans = new Array(n);
// 计算前缀乘积(ans[0] = 1)
let pre = 1;
for (let i = 0; i < n; i++) {
ans[i] = pre;
pre *= nums[i];
}
// 计算后缀乘积并累乘到 ans
let suf = 1;
for (let i = n - 1; i >= 0; i--) {
ans[i] *= suf;
suf *= nums[i];
}
return ans;
};
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(n)
✅ 空间优化 :直接在
ans
数组中同时存前缀和后缀,可以使空间复杂度降到O(1)
5. 第一个缺失的正数
题目描述
给你一个未排序的整数数组 nums
,找出其中最小的缺失的正整数。
解题思路
💡 核心技巧:原地哈希
利用数组下标作为哈希表,将正整数放到对应位置。
关键洞察:
- 有效范围:1 ~ n(n 为数组长度)
- 交换操作:将
nums[i]
放到nums[nums[i]-1]
位置 - 最后遍历数组,第一个
nums[i] != i+1
的位置即为答案
✅ 为什么高效:O(n) 时间,O(1) 空间,无需额外哈希表。
代码实现
javascript
/**
* @param {number[]} nums
* @return {number}
*/
var firstMissingPositive = function(nums) {
const n = nums.length;
// 第一步:将正整数放到正确的位置(1~n)
for (let i = 0; i < n; i++) {
// 当前元素在1~n范围内,且不在正确位置
while (nums[i] > 0 && nums[i] <= n && nums[i] !== nums[nums[i] - 1]) {
// 交换到目标位置
const j = nums[i] - 1;
[nums[i], nums[j]] = [nums[j], nums[i]];
}
}
// 第二步:遍历数组,找到第一个不符合条件的位置
for (let i = 0; i < n; i++) {
if (nums[i] !== i + 1) {
return i + 1;
}
}
// 如果全部符合,返回n+1
return n + 1;
};
复杂度分析
- 时间复杂度:O(n),两次遍历,交换操作总次数 ≤ n
- 空间复杂度:O(1),原地操作
总结对比
题目 | 核心技巧 | 关键优化 |
---|---|---|
最大子数组和 | 动态规划 + 状态压缩 | 无需额外数组 |
合并区间 | 排序 + 一次遍历 | 排序后重叠区间自然相邻 |
旋转数组 | 三次反转 | 无额外空间 |
除自身以外的数组乘积 | 前缀/后缀乘积 | 用结果数组存储中间结果 |
第一个缺失的正数 | 原地哈希 | 利用数组下标作为哈希表 |
希望这篇解析对你有帮助!如果你喜欢这类结构清晰、对比鲜明的算法总结,欢迎关注我的 LeetCode 热题 100 系列 👋