LeetCode100天Day5-最小长度子数组与三数之和

摘要:本文详细解析了LeetCode中两道经典算法题目------"长度最小的子数组"和"三数之和"。通过从暴力解法到优化解法的完整思路演进,深入分析滑动窗口和双指针算法的应用场景,帮助读者掌握数组问题的核心解题技巧。

目录

文章目录

    • 目录
    • [1. 长度最小的子数组(Minimum Size Subarray Sum)](#1. 长度最小的子数组(Minimum Size Subarray Sum))
      • [1.1 题目描述](#1.1 题目描述)
      • [1.2 为什么从暴力解法开始?](#1.2 为什么从暴力解法开始?)
      • [1.3 暴力解法实现](#1.3 暴力解法实现)
      • [1.4 暴力解法代码详解](#1.4 暴力解法代码详解)
      • [1.5 复杂度分析](#1.5 复杂度分析)
      • [1.6 边界情况处理](#1.6 边界情况处理)
    • [2. 三数之和(3Sum)](#2. 三数之和(3Sum))
    • [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. 思路直观:暴力解法最贴近题目描述,帮助我们理解问题的本质
  2. 验证工具:可以用暴力解法验证优化算法的正确性
  3. 性能基准:为优化提供对比基准,更清晰地看到优化的效果
  4. 必经之路:从简单到复杂是算法学习的自然规律
  5. 调试友好:代码结构清晰,便于理解和调试

代码简洁性优势:暴力解法的代码往往只有几十行,逻辑清晰,非常适合作为学习的第一步。当我们完全理解了问题的本质后,再考虑优化策略。

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 != ji != kj != 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 解题思路分析

核心思想:排序 + 双指针

这道题的关键在于避免重复,通过排序可以将相同元素聚集在一起,便于跳过重复值。

解题步骤

  1. 排序数组:将数组按非递减顺序排序
  2. 固定第一个数:遍历数组,每次固定一个数作为三元组的第一个元素
  3. 双指针查找后两个数:对于固定的第一个数,使用双指针查找另外两个数
  4. 去重处理:在遍历过程中跳过重复元素

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 暴力解法的适用场景

何时选择暴力解法

  1. 学习阶段:初次接触某类问题时,先用暴力解法理解题意
  2. 数据规模小:当 n <= 100 时,O(n²) 的解法通常可以接受
  3. 验证正确性:用来验证优化算法的正确性
  4. 面试思路:面试时可以先给出暴力解法,再逐步优化

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 相关题目推荐

  1. 最接近的三数之和:LeetCode 第16题
  2. 四数之和:LeetCode 第18题
  3. 长度最小的子数组(滑动窗口优化):LeetCode 第209题
  4. 和为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. 总结

今天我们深入学习了两个重要的数组问题:

  1. 长度最小的子数组:通过暴力解法理解子数组求和问题的本质,为后续学习滑动窗口打下基础

  2. 三数之和:掌握排序 + 双指针的经典组合,学会处理去重问题

核心收获

  • 暴力解法是理解问题的重要工具,不应被忽视
  • 排序是解决数组问题的强大工具,可以简化很多操作
  • 双指针算法的精髓在于根据问题特性确定指针移动策略
  • 去重是数组问题中常见的难点,需要仔细处理

互动时间:你在这两道题中遇到了哪些困难?你是如何理解的?欢迎在评论区分享你的解题思路!


参考资源

文章标签

#LeetCode #算法 #Java #双指针 #数组

喜欢这篇文章吗?别忘了点赞、收藏和分享!你的支持是我创作的最大动力!

相关推荐
小龙报2 小时前
【初阶数据结构】从 “数组升级” 到工程实现:动态顺序表实现框架的硬核拆解指南
c语言·数据结构·c++·算法·机器学习·信息与通信·visual studio
多米Domi0112 小时前
0x3f第九天复习(考研日)(10.57-14:00)
python·算法
博览鸿蒙2 小时前
FPGA 经典面试题目及答案汇总
fpga开发·面试·职场和发展
byzh_rc2 小时前
[模式识别-从入门到入土] 拓展-EM算法
算法·机器学习·概率论
努力学算法的蒟蒻2 小时前
day41(12.22)——leetcode面试经典150
算法·leetcode·面试
liliangcsdn2 小时前
Python拒绝采样算法优化与微调模拟
人工智能·算法·机器学习
Christo32 小时前
2024《A Rapid Review of Clustering Algorithms》
人工智能·算法·机器学习·数据挖掘
AndrewHZ2 小时前
【图像处理基石】图像梯度:核心算法原理与经典应用场景全解析
图像处理·算法·计算机视觉·cv·算子·边缘提取·图像梯度
让学习成为一种生活方式2 小时前
组蛋白短链酰化修饰--文献精读187
算法