【LeetCode 热题 100】(五)普通数组
53. 最大子数组和
class Solution {public int maxSubArray(int[] nums) {int length = nums.length;if(length == 1){return nums[0];}// 1.定义全局最大和int ans = nums[0];int pre = 0;for (int i = 0; i < nums.length; i++) {pre = Math.max(nums[i], nums[i] + pre);ans = Math.max(ans, pre);} return ans;}
}
解题思路:动态规划(Kadane算法)
问题分析
寻找数组中连续子数组的最大和(至少包含一个元素)。例如:[-2,1,-3,4,-1,2,1,-5,4]
的最大子数组和为 6
(子数组 [4,-1,2,1]
)。
核心算法:动态规划
-
状态定义:
pre
:表示以当前元素结尾的最大子数组和ans
:记录全局最大子数组和
-
状态转移方程:
pre = Math.max(nums[i], // 选项1:从当前元素重新开始nums[i] + pre) // 选项2:延续前面的子数组 ans = Math.max(ans, pre) // 更新全局最大值
-
算法流程:
graph TD A[初始化 ans=nums[0], pre=0] --> B[遍历数组] B --> C{计算以nums[i]结尾的最大和} C --> D[选择1:独立子数组 nums[i]] C --> E[选择2:延续前序子数组 nums[i]+pre] D --> F[取较大值更新pre] E --> F F --> G[更新全局最大值ans] G --> B
关键步骤解析
-
初始化:
ans = nums[0]
(至少包含一个元素)pre = 0
(初始无前序子数组)
-
遍历数组:
- 对每个元素
nums[i]
计算:- 选择1:以当前元素作为新起点(
nums[i]
) - 选择2:将当前元素加入前序子数组(
nums[i] + pre
)
- 选择1:以当前元素作为新起点(
- 取两者较大值更新
pre
- 用
pre
更新全局最大值ans
- 对每个元素
-
终止条件:
- 遍历完成后返回
ans
- 遍历完成后返回
时间复杂度
- O(n):仅需一次遍历数组
- 空间复杂度 O(1):仅用常数空间存储状态
示例演算(nums = [-2,1,-3,4,-1,2,1])
i | nums[i] | 决策 | pre | ans |
---|---|---|---|---|
0 | -2 | max(-2, 0-2) = -2 | -2 | -2 |
1 | 1 | max(1, -2+1) = 1 | 1 | 1 |
2 | -3 | max(-3, 1-3) = -2 | -2 | 1 |
3 | 4 | max(4, -2+4) = 4 | 4 | 4 |
4 | -1 | max(-1, 4-1) = 3 | 3 | 4 |
5 | 2 | max(2, 3+2) = 5 | 5 | 5 |
6 | 1 | max(1, 5+1) = 6 | 6 | 6 |
最终结果:6(子数组
[4,-1,2,1]
)
算法优势
-
高效处理负值:
- 当
pre
为负时自动重置子数组(Math.max(nums[i], nums[i]+pre)
) - 避免负值拖累后续子数组和
- 当
-
实时更新:
- 在遍历过程中即时计算最大值
- 无需额外存储空间
-
边界处理:
- 单元素数组直接返回该元素
- 全负数数组返回最大负数
典型应用场景
- 股票买卖(最大利润区间)
- 信号处理(最大连续波形)
- 数据分析(最大收益区间)
此算法由Joseph Born Kadane提出(1984),是解决最大子数组问题的最优方案,体现了动态规划的核心思想:利用历史状态避免重复计算。
56. 合并区间
class Solution {public int[][] merge(int[][] intervals) {if(intervals.length == 0){int[][] ints = new int[0][2];return ints;}Arrays.sort(intervals, new Comparator<int[]>() {@Overridepublic int compare(int[] o1, int[] o2) {return o1[0] - o2[0];}});ArrayList<int[]> merged = new ArrayList<>();for (int i = 0; i < intervals.length; i++) {int L = intervals[i][0], R = intervals[i][1];if(merged.size()==0 || merged.get(merged.size() - 1)[1] < L){merged.add(new int[]{L,R});}else{merged.get(merged.size()-1)[1] = Math.max(merged.get(merged.size() - 1)[1], R);}}int[][] ints1 = merged.toArray(new int[merged.size()][]);return ints1;}
}
解题思路:区间合并算法
核心思想
通过排序和线性扫描合并重叠区间,将多个可能重叠的区间合并为互不重叠的区间集合。
关键步骤
-
边界处理:
- 当输入为空数组时,直接返回空二维数组
if(intervals.length == 0){return new int[0][2]; }
-
区间排序:
- 按照区间左端点升序排序
Arrays.sort(intervals, (o1, o2) -> o1[0] - o2[0]);
-
合并重叠区间:
graph TD A[开始遍历区间] --> B{是否第一个区间 或<br>当前左端点 > 上一个右端点?} B -- 是 --> C[添加新区间] B -- 否 --> D[合并区间] C --> E[继续遍历] D --> E E --> F[遍历结束]
-
具体合并逻辑:
- 初始化存储合并结果的列表
merged
- 遍历每个区间:
for (int i = 0; i < intervals.length; i++) {int L = intervals[i][0], R = intervals[i][1];// 不重叠情况if(merged.isEmpty() || lastRight < L) {merged.add(new int[]{L, R});} // 重叠情况else {lastRight = Math.max(lastRight, R);} }
- 初始化存储合并结果的列表
算法特点
-
时间复杂度:O(n log n)
- 排序消耗 O(n log n)
- 合并扫描消耗 O(n)
-
空间复杂度:O(n)
- 存储合并结果需要额外空间
-
关键优势:
- 处理任意数量区间
- 正确处理全包含/部分重叠/边界相接等情况
- 结果区间有序且不重叠
示例解析(输入:[[1,3],[2,6],[8,10],[15,18]])
当前区间 | 合并状态 | 操作 | 说明 |
---|---|---|---|
[1,3] | [] | 添加 | 初始状态 |
[2,6] | [[1,3]] | 合并 | 3≥2 → 更新为[1,6] |
[8,10] | [[1,6]] | 添加 | 6<8 → 无重叠 |
[15,18] | [[1,6],[8,10]] | 添加 | 10<15 → 无重叠 |
输出:[[1,6],[8,10],[15,18]]
处理各种重叠情况
- 完全包含:[[1,4],[2,3]] → [[1,4]]
- 部分重叠:[[1,3],[2,4]] → [[1,4]]
- 边界相接:[[1,2],[2,3]] → [[1,3]]
- 多重重叠:[[1,4],[2,5],[3,6]] → [[1,6]]
算法应用场景
- 日程安排合并
- 资源占用时间段优化
- 交通线路区间合并
- 任何需要处理重叠时间段的问题
该算法通过排序+单次扫描的巧妙设计,高效解决了区间合并问题,是处理这类问题的标准解法。
199. 轮转数组
class Solution {public void rotate(int[] nums, int k) {int length = nums.length;int[] arr = new int[length];for (int i = 0; i < nums.length; i++) {arr[(i+k)%length] = nums[i];}for (int i = 0; i < arr.length; i++) {nums[i] = arr[i];}}
}
这段代码实现了数组的旋转功能,即将数组元素向右移动 k
个位置。以下是代码的解题思路详解:
解题思路
-
问题分析:
- 向右旋转数组
k
次意味着每个元素会移动到其右侧k
个位置后的新位置。 - 若移动位置超出数组末尾,元素会循环移动到数组开头(例如,末尾元素移动后可能出现在数组头部)。
- 关键点:新位置 = (原位置 + k) % 数组长度,取模操作确保循环移动。
- 向右旋转数组
-
核心算法:
- 临时数组辅助:创建一个与原数组等长的临时数组
arr
。 - 计算新位置:遍历原数组,将每个元素
nums[i]
放入临时数组的(i + k) % length
位置。 - 复制回原数组:将临时数组的内容复制回原数组
nums
。
- 临时数组辅助:创建一个与原数组等长的临时数组
-
处理大 k 值:
- 当
k
大于数组长度时,实际有效移动次数为k % length
(例如,数组长度为 7,k=10 等价于 k=3)。 - 代码中的
(i+k)%length
自动处理了这种情况,无需单独调整k
。
- 当
-
示例说明:
- 假设
nums = [1, 2, 3, 4, 5]
,k = 2
:- 长度
length = 5
。 - 计算新位置:
i=0
:(0+2)%5 = 2
→arr[2] = 1
i=1
:(1+2)%5 = 3
→arr[3] = 2
i=2
:(2+2)%5 = 4
→arr[4] = 3
i=3
:(3+2)%5 = 0
→arr[0] = 4
i=4
:(4+2)%5 = 1
→arr[1] = 5
- 结果:
arr = [4, 5, 1, 2, 3]
,复制回nums
完成旋转。
- 长度
- 假设
代码特点
- 时间复杂度:O(n),遍历数组两次(一次填充临时数组,一次复制回原数组)。
- 空间复杂度:O(n),使用了一个临时数组。
- 优点:逻辑清晰,直观体现旋转的本质(位置映射)。
- 改进点:若要求空间复杂度 O(1),可使用反转法(先整体反转,再分段反转)。
关键代码解释
public void rotate(int[] nums, int k) {int length = nums.length;int[] arr = new int[length]; // 创建临时数组for (int i = 0; i < length; i++) {arr[(i + k) % length] = nums[i]; // 元素移动到新位置}for (int i = 0; i < length; i++) {nums[i] = arr[i]; // 复制回原数组}
}
此方法通过位置映射公式高效解决了数组旋转问题,适合理解旋转的本质逻辑。
238. 除自身以外数组的乘积
class Solution {public int[] productExceptSelf(int[] nums) {int length = nums.length;int[] left = new int[length];int[] right = new int[length];int[] answer = new int[length];// 1. 获取左边的累积left[0] = 1;for (int i = 1; i < length; i++) {left[i] = nums[i-1] * left[i-1];}// 2. 获取右边的累积right[length-1] = 1;for (int i = length-2; i >= 0; i--) {right[i] = nums[i+1]*right[i+1];}for (int i = 0; i < length; i++) {answer[i] = left[i] * right[i];} return answer;}
}
解题思路描述
这段代码实现了 “除自身以外数组的乘积” 问题。给定一个整数数组 nums
,要求返回一个新数组 answer
,其中 answer[i]
等于 nums
中除 nums[i]
之外所有元素的乘积。代码满足以下约束:
- 不能使用除法(避免除以零问题并符合题目要求)。
- 时间复杂度为 O(n)(高效的单次遍历)。
- 空间复杂度为 O(n)(使用额外数组,但非原地修改输入)。
核心思想:分解问题
将每个位置的答案拆分为两部分:
- 左侧所有元素的乘积(
left[i]
):索引0
到i-1
的累积乘积。 - 右侧所有元素的乘积(
right[i]
):索引i+1
到n-1
的累积乘积。
最终结果即为两者的乘积:answer[i] = left[i] * right[i]
。
步骤详解
-
初始化数组:
left[]
:存储每个位置左侧的累积乘积。right[]
:存储每个位置右侧的累积乘积。answer[]
:存储最终结果。
-
计算左侧乘积(从左向右遍历):
- 起始点:
left[0] = 1
(第一个元素左侧无元素)。 - 递推公式:
left[i] = nums[i-1] * left[i-1]
。 - 意义:
left[i]
表示nums[0] × nums[1] × ... × nums[i-1]
。 - 示例:若
nums = [1, 2, 3, 4]
,则left = [1, 1×1=1, 2×1=2, 3×2=6]
。
- 起始点:
-
计算右侧乘积(从右向左遍历):
- 起始点:
right[length-1] = 1
(最后一个元素右侧无元素)。 - 递推公式:
right[i] = nums[i+1] * right[i+1]
。 - 意义:
right[i]
表示nums[i+1] × nums[i+2] × ... × nums[n-1]
。 - 示例:
nums = [1, 2, 3, 4]
,则right = [2×3×4=24, 3×4=12, 4×1=4, 1]
。
- 起始点:
-
合并结果:
- 对每个索引
i
,计算answer[i] = left[i] * right[i]
。 - 意义:左侧乘积 × 右侧乘积 = 排除
nums[i]
的总乘积。 - 示例:
i=1
时,answer[1] = left[1] × right[1] = 1 × 12 = 12
(即1×3×4
)。
- 对每个索引
为什么有效?
- 避免除法:通过分解为左右两部分,无需除法操作。
- 时间复杂度 O(n):三次独立遍历(左、右、合并),每次 O(n)。
- 空间复杂度 O(n):使用
left
和right
两个辅助数组(各长度 n)。
示例演算
以 nums = [1, 2, 3, 4]
为例:
步骤 | 索引 0 | 索引 1 | 索引 2 | 索引 3 |
---|---|---|---|---|
left[] 计算 | left[0]=1 | left[1]=nums[0]×left[0]=1×1=1 | left[2]=nums[1]×left[1]=2×1=2 | left[3]=nums[2]×left[2]=3×2=6 |
right[] 计算 | right[0]=nums[1]×right[1]=2×12=24 | right[1]=nums[2]×right[2]=3×4=12 | right[2]=nums[3]×right[3]=4×1=4 | right[3]=1 |
answer[] 结果 | 1×24=24 | 1×12=12 | 2×4=8 | 6×1=6 |
最终 answer = [24, 12, 8, 6] (即 [2×3×4, 1×3×4, 1×2×4, 1×2×3] )。 |