560. 和为 K 的子数组
class Solution {
public int subarraySum(int[] nums, int k) {
int count = 0;
for (int i = 0; i < nums.length; i++) {
int sum = 0;
for (int j = i; j < nums.length; j++) {
sum += nums[j];
if (sum == k) {
count++;
}
// else if(sum>k){
// continue;
// }
}
}
return count;
}
}
public class Solution {
public int subarraySum(int[] nums, int k) {
// count:记录满足条件的子数组数量
// pre:当前的前缀和(nums[0] + nums[1] + ... + nums[i])
int count = 0, pre = 0;
// mp:哈希表,key 是前缀和,value 是该前缀和出现的次数
HashMap<Integer, Integer> mp = new HashMap<>();
// 初始化:前缀和为 0 的情况出现 1 次(空子数组的和为 0)
mp.put(0, 1);
// 遍历数组,计算前缀和
for (int i = 0; i < nums.length; i++) {
// 更新当前的前缀和
pre += nums[i];
// 检查是否存在 pre - k 的前缀和
// 如果存在,说明从某个位置到当前位置的子数组和为 k
if (mp.containsKey(pre - k)) {
// 累加 pre - k 的出现次数到 count
count += mp.get(pre - k);
}
// 将当前前缀和存入哈希表,并更新出现次数
mp.put(pre, mp.getOrDefault(pre, 0) + 1);
}
// 返回满足条件的子数组数量
return count;
}
}
关键逻辑注释
1.
mp.put(0, 1)
的作用
为什么需要初始化
mp.put(0, 1)
?
因为空子数组(不选任何元素)的和为0
。如果某个前缀和直接等于k
(比如nums[0..i] == k
),我们需要pre - k = 0
的情况能被统计到。2.
if (mp.containsKey(pre - k))
的逻辑
为什么
pre - k
能表示子数组和为k
?
假设pre[j]
是当前前缀和,pre[i]
是之前某个前缀和,如果pre[j] - pre[i] = k
,那么子数组nums[i+1..j]
的和就是k
。
举例:
nums = [1, 2, 3]
,k = 3
i = 1
时,pre = 1 + 2 = 3
,pre - k = 0
(mp
中有0
),说明子数组[1, 2]
的和为3
。
i = 2
时,pre = 1 + 2 + 3 = 6
,pre - k = 3
(mp
中有3
),说明子数组[3]
的和为3
。3.
mp.put(pre, ...)
的作用
为什么需要更新哈希表?
记录每个前缀和的出现次数,后续可以通过pre - k
快速判断是否存在满足条件的子数组。
示例运行(
nums = [1, 1, 1]
,k = 2
)
步骤 i nums[i] pre pre - k mp 更新前 count 变化 mp 更新后 初始化 - - 0 - {0: 1} - - 1 0 1 1 -1 {0: 1} 无 {0: 1, 1: 1} 2 1 1 2 0 {0: 1, 1: 1} count += 1 {0: 1, 1: 1, 2: 1} 3 2 1 3 1 {0: 1, 1: 1, 2: 1} count += 1 {0: 1, 1: 1, 2: 1, 3: 1}
结果:
count = 2
(子数组[1, 1]
和[1, 1]
)。
总结
核心思想:用前缀和和哈希表将问题转化为两数之差问题。
时间复杂度:O(n),只需遍历一次数组。
空间复杂度:O(n),哈希表存储前缀和。
53. 最大子数组和
class Solution {
public int maxSubArray(int[] nums) {
// pre:以当前元素结尾的子数组的最大和
// maxAns:全局最大子数组和
int pre = 0, maxAns = nums[0];
for (int x : nums) {
// 决定是否抛弃之前的子数组,或加入当前元素
pre = Math.max(pre + x, x);
// 更新全局最大值
maxAns = Math.max(maxAns, pre);
}
return maxAns;
}
}
这段代码使用 动态规划(Kadane算法) 高效求解 最大子数组和 问题。它的核心思想是:
维护两个变量:
pre
:表示以当前元素结尾的子数组的最大和。
maxAns
:记录全局最大子数组和。递推关系:
对于每个元素
x
,决定是将其加入前面的子数组(pre + x
),还是以它作为新子数组的起点(x
)。更新
maxAns
为当前最大值。
逐行解析
1. 初始化
java
int pre = 0, maxAns = nums[0];
pre
:初始化为0
,表示尚未开始累加。
maxAns
:初始化为nums[0]
,因为至少包含一个元素的子数组的最大和可能是第一个元素本身。
2. 遍历数组
java
for (int x : nums) { pre = Math.max(pre + x, x); maxAns = Math.max(maxAns, pre); }
pre = Math.max(pre + x, x)
:
pre + x
:将当前元素x
加入之前的子数组。
x
:以当前元素x
作为新子数组的起点。选择较大值,确保
pre
始终是以x
结尾的子数组的最大和。
maxAns = Math.max(maxAns, pre)
:
更新全局最大值
maxAns
。
示例演示
假设
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
:
元素 (x) pre (更新前) pre + x x 新 pre maxAns (更新前) 新 maxAns -2 0 -2 -2 -2 -2 -2 1 -2 -1 1 1 -2 1 -3 1 -2 -3 -2 1 1 4 -2 2 4 4 1 4 -1 4 3 -1 3 4 4 2 3 5 2 5 4 5 1 5 6 1 6 5 6 -5 6 1 -5 1 6 6 4 1 5 4 5 6 6
最终结果:
maxAns = 6
(对应子数组[4, -1, 2, 1]
)。
复杂度分析
时间复杂度:O(n),只需遍历一次数组。
空间复杂度:O(1),仅使用常数空间。
关键点总结
动态规划状态转移:
pre
表示以当前元素结尾的子数组的最大和。通过比较
pre + x
和x
决定是否抛弃之前的子数组。全局最大值更新:
每次迭代后更新
maxAns
,确保不遗漏任何可能的子数组。初始化技巧:
maxAns
初始化为nums[0]
,处理数组全为负数的情况
56. 合并区间
class Solution {
public int[][] merge(int[][] intervals) {
if (intervals.length <= 1) {
return intervals;
}
// 按区间起始点排序
Arrays.sort(intervals, Comparator.comparingInt(a -> a[0]));
List<int[]> merged = new ArrayList<>();
int[] current = intervals[0];
merged.add(current);
for (int[] interval : intervals) {
if (interval[0] <= current[1]) {
// 合并区间
current[1] = Math.max(current[1], interval[1]);
} else {
// 无法合并,添加到结果
current = interval;
merged.add(current);
}
}
return merged.toArray(new int[merged.size()][]);
}
}
1. 输入处理与特殊情况
java
if (intervals.length <= 1) { return intervals; }
作用:如果输入的区间数量 ≤ 1(即空数组或单个区间),直接返回原数组。
原因:无需合并,直接返回即可。
2. 区间排序
java
Arrays.sort(intervals, Comparator.comparingInt(a -> a[0]));
作用:按区间的起始点(
intervals[i][0]
)升序排序。原因:排序后,重叠的区间会相邻排列,便于后续合并。
示例:
输入:
[[2,6],[1,3],[8,10],[15,18]]
排序后:
[[1,3],[2,6],[8,10],[15,18]]
3. 初始化合并列表
java
List<int[]> merged = new ArrayList<>(); int[] current = intervals[0]; merged.add(current);
merged
:用于存储合并后的结果区间。
current
:当前正在处理的区间,初始化为第一个区间。原因:从第一个区间开始逐步合并。
4. 遍历与合并区间
java
for (int[] interval : intervals) { if (interval[0] <= current[1]) { // 合并区间 current[1] = Math.max(current[1], interval[1]); } else { // 无法合并,添加到结果 current = interval; merged.add(current); } }
逻辑:
可合并:如果当前区间的起始点 ≤
current
的结束点,说明两区间重叠。
更新
current
的结束点为两者的最大值(扩展区间)。示例:
current = [1,3]
,interval = [2,6]
→ 合并为[1,6]
。不可合并:如果区间不重叠,将
current
加入结果,并更新current
为新区间。
示例:
current = [1,6]
,interval = [8,10]
→ 无法合并,将[1,6]
加入结果,current
更新为[8,10]
。
5. 返回结果
java
return merged.toArray(new int[merged.size()][]);
作用:将
List<int[]>
转换为int[][]
。原因:题目要求返回二维数组。
深入解析
Comparator.comparingInt(a -> a[0])
这是Java中用于排序的一个关键表达式,属于函数式编程的用法。我们来彻底理解它的含义和工作原理。
1. 基本作用
java
Arrays.sort(intervals, Comparator.comparingInt(a -> a[0]));
功能:对二维数组
intervals
按照每个子数组的第一个元素(即区间起点)进行升序排序。等价于:
java
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
2. 分解解析
(1)
Comparator
接口
是Java中用于定义比较规则的接口。
核心方法:
int compare(T o1, T o2)
。(2)
comparingInt()
方法
是
Comparator
的静态工厂方法。签名:
java
static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor)参数:
ToIntFunction
,即从对象中提取一个int
类型排序键的函数。作用:根据提取的
int
键生成一个Comparator
。(3)
a -> a[0]
是一个Lambda表达式,等价于:
java
new ToIntFunction<int[]>() { @Override public int applyAsInt(int[] a) { return a[0]; } }功能:输入一个区间
a
,返回它的起始点a[0]
。
3. 完整执行流程
提取排序键:对每个区间
a
,提取a[0]
(起始点)。比较规则:根据提取的
a[0]
值生成默认的升序比较器。
相当于隐式实现了:
java
(a, b) -> Integer.compare(a[0], b[0])
4. 对比其他写法
写法 说明 Comparator.comparingInt(a -> a[0])
函数式风格,简洁 (a, b) -> a[0] - b[0]
传统Lambda,可能溢出 new Comparator<int[]>() { ... }
匿名类,冗长
5. 为什么用
comparingInt
?
避免整数溢出:
a[0] - b[0]
在极大值时可能溢出。
Integer.compare(a[0], b[0])
会正确处理边界。代码可读性:
明确表达"按某字段排序"的意图。
类型安全:
编译期检查提取的键是否为
int
。
6. 扩展用法
(1) 降序排序
java
Arrays.sort(intervals, Comparator.comparingInt(a -> a[0]).reversed());(2) 多级排序
java
// 先按起点升序,再按终点降序 Arrays.sort(intervals, Comparator.comparingInt((int[] a) -> a[0]) .thenComparing(Comparator.comparingInt(a -> a[1]).reversed()) );
7. 实际应用示例
输入:
[[3,4],[1,2],[2,3]]
排序后:[[1,2],[2,3],[3,4]]
代码执行:
提取每个元素的
a[0]
:3, 1, 2
按提取值排序:
1, 2, 3
最终顺序:
[1,2]
,[2,3]
,[3,4]