摘要:本文详细解析了LeetCode中两道经典算法题目------"长度最小的子数组"和"三数之和"。通过从暴力解法到优化解法的完整思路演进,深入分析滑动窗口和双指针算法的应用场景,帮助读者掌握数组问题的核心解题技巧。
目录
文章目录
-
- 目录
- [1. 长度最小的子数组(Minimum Size Subarray Sum)](#1. 长度最小的子数组(Minimum Size Subarray Sum))
- [2. 三数之和(3Sum)](#2. 三数之和(3Sum))
-
- [2.1 题目描述](#2.1 题目描述)
- [2.2 解题思路分析](#2.2 解题思路分析)
-
- [核心思想:排序 + 双指针](#核心思想:排序 + 双指针)
- [2.3 代码实现与详细解析](#2.3 代码实现与详细解析)
- [2.4 代码逐行解析](#2.4 代码逐行解析)
- [2.5 示例执行过程](#2.5 示例执行过程)
- [2.6 复杂度分析](#2.6 复杂度分析)
- [3. 算法思想对比与总结](#3. 算法思想对比与总结)
-
- [3.1 两题算法对比](#3.1 两题算法对比)
- [3.2 暴力解法的适用场景](#3.2 暴力解法的适用场景)
- [3.3 双指针算法的核心要素](#3.3 双指针算法的核心要素)
- [4. 实战技巧与注意事项](#4. 实战技巧与注意事项)
-
- [4.1 代码优化建议](#4.1 代码优化建议)
- [4.2 常见错误及解决方案](#4.2 常见错误及解决方案)
- [4.3 调试技巧](#4.3 调试技巧)
- [5. 扩展思考](#5. 扩展思考)
-
- [5.1 相关题目推荐](#5.1 相关题目推荐)
- [5.2 进阶优化方向](#5.2 进阶优化方向)
- [5.3 算法模板总结](#5.3 算法模板总结)
- [6. 总结](#6. 总结)
- 参考资源
- 文章标签
1. 长度最小的子数组(Minimum Size Subarray Sum)
1.1 题目描述
给定一个含有 n 个正整数的数组和一个正整数 target。
找出该数组中满足其总和大于等于 target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr],并返回其长度。如果不存在符合条件的子数组,返回 0。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
1.2 为什么从暴力解法开始?
在学习算法的过程中,暴力解法具有重要的教学价值:
学习价值:
- 思路直观:暴力解法最贴近题目描述,帮助我们理解问题的本质
- 验证工具:可以用暴力解法验证优化算法的正确性
- 性能基准:为优化提供对比基准,更清晰地看到优化的效果
- 必经之路:从简单到复杂是算法学习的自然规律
- 调试友好:代码结构清晰,便于理解和调试
代码简洁性优势:暴力解法的代码往往只有几十行,逻辑清晰,非常适合作为学习的第一步。当我们完全理解了问题的本质后,再考虑优化策略。
1.3 暴力解法实现
java
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int ans = 0;
int sum = 0;
int len = nums.length;
int index = 0;
for(int i = 0; i < len; i++){
sum += nums[i];
if(sum >= target){
// 找到满足条件的子数组,更新最小长度
if(ans == 0 || i - index < ans){
ans = i - index + 1;
}
// 重置,从下一个位置开始寻找
i = index;
index++;
sum = 0;
}
}
return ans;
}
}
1.4 暴力解法代码详解
核心变量说明
| 变量名 | 类型 | 作用 |
|---|---|---|
ans |
int | 存储最小长度,初始为0表示未找到 |
sum |
int | 当前子数组的和 |
len |
int | 数组长度 |
index |
int | 当前子数组的起始位置 |
i |
int | 当前遍历到的位置 |
执行流程分析
初始状态:target = 7, nums = [2,3,1,2,4,3]
第1轮 (index=0):
i=0: sum=2, sum < 7
i=1: sum=5, sum < 7
i=2: sum=6, sum < 7
i=3: sum=8, sum >= 7 → ans=4, 重置
第2轮 (index=1):
i=1: sum=3, sum < 7
i=2: sum=4, sum < 7
i=3: sum=6, sum < 7
i=4: sum=10, sum >= 7 → ans=3 (更新), 重置
第3轮 (index=2):
i=2: sum=1, sum < 7
i=3: sum=3, sum < 7
i=4: sum=7, sum >= 7 → ans=2 (更新), 重置
第4轮 (index=3):
i=3: sum=2, sum < 7
i=4: sum=6, sum < 7
i=5: sum=9, sum >= 7 → ans=3 (不更新), 重置
第5轮 (index=4):
i=4: sum=4, sum < 7
i=5: sum=7, sum >= 7 → ans=2 (不更新), 重置
最终返回:2
1.5 复杂度分析
| 分析维度 | 暴力解法 | 说明 |
|---|---|---|
| 时间复杂度 | O(n²) | 最坏情况需要多次遍历数组 |
| 空间复杂度 | O(1) | 只使用常数级额外空间 |
| 代码行数 | 20行左右 | 结构清晰,易于理解 |
1.6 边界情况处理
java
// 情况1:数组为空
if(nums == null || nums.length == 0){
return 0;
}
// 情况2:单个元素就满足条件
if(nums[0] >= target){
return 1;
}
// 情况3:所有元素加起来都不满足
int total = 0;
for(int num : nums){
total += num;
}
if(total < target){
return 0;
}
2. 三数之和(3Sum)
2.1 题目描述
给你一个整数数组 nums,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k,同时还满足 nums[i] + nums[j] + nums[k] == 0。
请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = -1 + 0 + 1 = 0
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0
nums[0] + nums[3] + nums[4] = -1 + 2 + (-1) = 0
不同的三元组是 [-1,0,1] 和 [-1,-1,2]
注意,输出的顺序和三元组的顺序并不重要
示例 2:
输入:nums = [0,0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组是 [0,0,0]
示例 3:
输入:nums = []
输出:[]
2.2 解题思路分析
核心思想:排序 + 双指针
这道题的关键在于避免重复,通过排序可以将相同元素聚集在一起,便于跳过重复值。
解题步骤:
- 排序数组:将数组按非递减顺序排序
- 固定第一个数:遍历数组,每次固定一个数作为三元组的第一个元素
- 双指针查找后两个数:对于固定的第一个数,使用双指针查找另外两个数
- 去重处理:在遍历过程中跳过重复元素
2.3 代码实现与详细解析
java
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
int len = nums.length;
List<List<Integer>> ans = new ArrayList<>();
// 步骤1:排序数组
Arrays.sort(nums);
// 步骤2:边界检查
if(len < 3){
return ans;
}
// 步骤3:固定第一个数
for(int i = 0; i < len - 2; i++){
// 剪枝:如果第一个数大于0,后面的数都大于0,不可能和为0
if(nums[i] > 0){
break;
}
// 去重:跳过重复的第一个数
if(i > 0 && nums[i] == nums[i-1]){
continue;
}
// 步骤4:双指针查找后两个数
int l = i + 1; // 左指针
int r = len - 1; // 右指针
while(l < r){
int sum = nums[i] + nums[l] + nums[r];
if(sum == 0){
// 找到一个三元组
ans.add(Arrays.asList(nums[i], nums[l], nums[r]));
// 去重:跳过重复的左指针值
while(l < r && nums[l] == nums[l + 1]){
l++;
}
// 去重:跳过重复的右指针值
while(l < r && nums[r] == nums[r - 1]){
r--;
}
// 同时移动左右指针
l++;
r--;
}
else if(sum > 0){
// 和大于0,需要减小,移动右指针
r--;
}
else if(sum < 0){
// 和小于0,需要增大,移动左指针
l++;
}
}
}
return ans;
}
}
2.4 代码逐行解析
第一部分:初始化和排序
java
int len = nums.length;
List<List<Integer>> ans = new ArrayList<>();
Arrays.sort(nums);
Arrays.sort(nums):排序是整个算法的基础- 排序后可以利用数组的有序性进行双指针查找
- 排序后相同元素相邻,便于去重
第二部分:边界检查和剪枝
java
if(len < 3){
return ans;
}
if(nums[i] > 0){
break;
}
| 检查项 | 作用 | 示例 |
|---|---|---|
len < 3 |
数组元素不足3个 | [1, 2] 直接返回 [] |
nums[i] > 0 |
剪枝优化 | [1, 2, 3] 第一个数就大于0,不可能和为0 |
第三部分:去重逻辑
java
// 第一个数的去重
if(i > 0 && nums[i] == nums[i-1]){
continue;
}
// 第二个数的去重
while(l < r && nums[l] == nums[l + 1]){
l++;
}
// 第三个数的去重
while(l < r && nums[r] == nums[r - 1]){
r--;
}
去重原理:排序后,相同的数是相邻的。当我们处理完某个值后,直接跳过后面所有相同的值。
第四部分:双指针移动逻辑
nums[i] nums[l] nums[r]
| | |
v v v
[-4, -1, -1, 0, 1, 2, 3]
↑ ↑
left pointer right pointer
当前和:-4 + (-1) + 3 = -2 < 0
操作:sum < 0,需要增大,左指针右移(l++)
2.5 示例执行过程
以 nums = [-1, 0, 1, 2, -1, -4] 为例:
步骤1 :排序后 nums = [-4, -1, -1, 0, 1, 2]
步骤2:遍历第一个数
| i | nums[i] | l | r | sum | 操作 | ans |
|---|---|---|---|---|---|---|
| 0 | -4 | 1 | 5 | -3 | sum < 0, l++ | [] |
| 0 | -4 | 2 | 5 | -2 | sum < 0, l++ | [] |
| 0 | -4 | 3 | 5 | -1 | sum < 0, l++ | [] |
| 0 | -4 | 4 | 5 | 0 | sum == 0, 添加 | [] |
| 1 | -1 | 2 | 5 | 1 | sum > 0, r-- | [[-4,1,3]] |
| 1 | -1 | 2 | 4 | 0 | sum == 0, 添加 | [[-4,1,3]] |
| ... | ... | ... | ... | ... | ... | ... |
最终结果 :[[-1, -1, 2], [-1, 0, 1]]
2.6 复杂度分析
| 分析维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n²) | 排序 O(n log n) + 双重循环 O(n²) |
| 空间复杂度 | O(1) | 不考虑输出数组的空间 |
| 去重时间 | O(n) | 跳过重复元素的时间 |
3. 算法思想对比与总结
3.1 两题算法对比
| 对比项 | 最小长度子数组 | 三数之和 |
|---|---|---|
| 核心算法 | 暴力搜索 | 排序 + 双指针 |
| 时间复杂度 | O(n²) | O(n²) |
| 空间复杂度 | O(1) | O(1) |
| 关键技巧 | 子数组求和 | 去重处理 |
| 适用场景 | 连续子数组问题 | 三元组查找问题 |
3.2 暴力解法的适用场景
何时选择暴力解法:
- 学习阶段:初次接触某类问题时,先用暴力解法理解题意
- 数据规模小:当 n <= 100 时,O(n²) 的解法通常可以接受
- 验证正确性:用来验证优化算法的正确性
- 面试思路:面试时可以先给出暴力解法,再逐步优化
3.3 双指针算法的核心要素
java
// 双指针算法三要素
int left = 0; // 1. 起始位置
int right = array.length - 1; // 2. 结束位置
while(left < right){ // 3. 终止条件
// 处理逻辑
}
适用条件:
- 数组已经排序
- 需要查找满足特定条件的元素对
- 可以根据当前状态确定指针移动方向
4. 实战技巧与注意事项
4.1 代码优化建议
java
// 建议1:使用增强for循环遍历结果
for(List<Integer> triplet : ans){
System.out.println(triplet);
}
// 建议2:提前计算数组长度,避免重复调用
int len = nums.length;
// 建议3:使用三元运算符简化代码
int min = (a < b) ? a : b;
4.2 常见错误及解决方案
| 错误类型 | 错误代码 | 正确代码 |
|---|---|---|
| 去重位置错误 | 在找到答案前去重 | 在找到答案后去重 |
| 边界处理遗漏 | 忘略 len < 3 的情况 | 添加边界检查 |
| 指针移动错误 | 只移动一个指针 | 根据sum值决定移动哪个 |
| 整数溢出 | 直接相加可能溢出 | 使用 long 类型存储 |
4.3 调试技巧
java
// 添加调试输出
System.out.println("i=" + i + ", l=" + l + ", r=" + r);
System.out.println("nums[i]=" + nums[i] + ", nums[l]=" + nums[l] + ", nums[r]=" + nums[r]);
System.out.println("sum=" + sum);
// 使用断点调试
// 在关键位置设置断点,观察变量变化
5. 扩展思考
5.1 相关题目推荐
- 最接近的三数之和:LeetCode 第16题
- 四数之和:LeetCode 第18题
- 长度最小的子数组(滑动窗口优化):LeetCode 第209题
- 和为K的子数组:LeetCode 第560题
5.2 进阶优化方向
对于"长度最小的子数组"题目,可以使用滑动窗口算法将时间复杂度优化到 O(n):
java
// 滑动窗口解法(时间复杂度 O(n))
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int left = 0, sum = 0;
int result = Integer.MAX_VALUE;
for(int right = 0; right < nums.length; right++){
sum += nums[right];
while(sum >= target){
result = Math.min(result, right - left + 1);
sum -= nums[left];
left++;
}
}
return result == Integer.MAX_VALUE ? 0 : result;
}
}
5.3 算法模板总结
暴力搜索模板:
java
for(int i = 0; i < n; i++){
for(int j = i; j < n; j++){
// 处理 [i, j] 区间
}
}
双指针模板:
java
int left = 0, right = n - 1;
while(left < right){
int sum = array[left] + array[right];
if(sum == target){
// 找到答案
} else if(sum < target){
left++;
} else {
right--;
}
}
6. 总结
今天我们深入学习了两个重要的数组问题:
-
长度最小的子数组:通过暴力解法理解子数组求和问题的本质,为后续学习滑动窗口打下基础
-
三数之和:掌握排序 + 双指针的经典组合,学会处理去重问题
核心收获:
- 暴力解法是理解问题的重要工具,不应被忽视
- 排序是解决数组问题的强大工具,可以简化很多操作
- 双指针算法的精髓在于根据问题特性确定指针移动策略
- 去重是数组问题中常见的难点,需要仔细处理
互动时间:你在这两道题中遇到了哪些困难?你是如何理解的?欢迎在评论区分享你的解题思路!
参考资源
文章标签
#LeetCode #算法 #Java #双指针 #数组
喜欢这篇文章吗?别忘了点赞、收藏和分享!你的支持是我创作的最大动力!