【LeetCode 热题 100】三数之和


精选专栏链接 🔗


欢迎订阅,点赞+关注,每日精进1%,与百万开发者共攀技术珠峰

更多内容持续更新中~



【LeetCode 热题 100】三数之和


📝题目描述

给你一个整数数组 nums ,判断是否存在三元组 nums\[i, numsj, numsk] 满足 i != j、i != k 且 j != k ,同时还满足 numsi + numsj + numsk == 0 。请你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

示例 1:

bash 复制代码
输入: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:

bash 复制代码
输入:nums = [0,1,1]
输出:[]


解释:唯一可能的三元组和不为 0 。

示例 3:

bash 复制代码
输入:nums = [0,0,0]
输出:[[0,0,0]]


解释:唯一可能的三元组和为 0 。

💡提示信息

  • 3 <= nums.length <= 3000;
  • − 10 5 -10^5 −105 <= numsi <= 10 5 10^5 105;

这道题最直接的暴力解法是使用三层循环遍历所有可能的组合,但这样时间复杂度会达到

O ( N 3 ) O(N^3) O(N3) ,在数据量较大时会超时。因此我们需要一种更高效的算法。


解题:排序 + 双指针

核心思路:

  1. 排序。首先将数组从小到大排序。排序有两个好处:

    • 方便我们后续使用双指针;
    • 方便我们去重。因为数组有序,相同的数字会挨在一起,我们可以通过比较相邻元素来跳过重复项;
  2. 固定一个数,并使用双指针寻找另外两个数:

    • 我们遍历数组,固定第一个数 nums[i]
    • 问题就转化为了在 i 之后的区间内,寻找两个数 nums[left]nums[right],使得它们的和等于 -nums[i](即 nums[i] + nums[left] + nums[right] == 0);
    • 定义左指针 left = i + 1,右指针 right = nums.length - 1
  3. 移动指针逻辑:

    • 如果 sum == 0:找到了一组解,加入结果集。然后 left 右移,right 左移。
    • 如果 sum < 0:说明总和太小了,需要增大,因此将左指针 left 右移(因为数组有序,右边的数更大)。
    • 如果 sum > 0:说明总和太大了,需要减小,因此将右指针 right 左移。
  4. 关键去重(Deduplication):

    • i 去重: 如果 nums[i] == nums[i-1],说明这个数字作为第一个数已经处理过了,直接跳过,避免重复结果。
    • leftright 去重: 当找到一组解后,在移动 leftright 之前,需要跳过所有与当前值相同的元素。

Java 代码实现如下 :

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();

        // 1. 校验:如果数组为空或长度小于3,直接返回空
        if (nums == null || nums.length < 3) {
            return result;
        }

        // 2. 排序:这是使用双指针的前提
        Arrays.sort(nums);

        int n = nums.length;

        // 3. 遍历第一个数
        for (int i = 0; i < n; i++) {
            // 剪枝优化:如果第一个数都大于0,后面的数肯定也大于0,肯定找不到三数之和为0
            if (nums[i] > 0) {
                break;
            }

            // 4. 去重逻辑 :跳过相同的第一个数
            // 注意:i > 0 是为了防止数组越界,nums[i] == nums[i-1] 说明之前已经处理过该数字
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }

            // 5. 双指针寻找剩下的两个数
            int left = i + 1;
            int right = n - 1;

            while (left < right) {
                int sum = nums[i] + nums[left] + nums[right];

                if (sum == 0) {
                    // 找到解,加入结果集
                    result.add(Arrays.asList(nums[i], nums[left], nums[right]));

                    // 6. 去重逻辑 :跳过相同的 left 和 right
                    // 在移动指针前,先跳过重复元素
                    while (left < right && nums[left] == nums[left + 1]) {
                        left++;
                    }
                    while (left < right && nums[right] == nums[right - 1]) {
                        right--;
                    }

                    // 找到答案后,双指针同时收缩
                    left++;
                    right--;
                } else if (sum < 0) {
                    // 和小于0,说明需要更大的数,左指针右移
                    left++;
                } else {
                    // 和大于0,说明需要更小的数,右指针左移
                    right--;
                }
            }
        }

        return result;
    }
}

注意:

为什么 i 的去重是 nums[i] == nums[i-1],而 left 的去重是 nums[left] == nums[left+1]

  • 对于 i (外层循环),我们是在处理当前 i 之前判断是否和上一个重复。因为如果 numsi == numsi-1,说明 numsi-1 已经作为第一个数,把所有可能的组合都找过了,numsi 直接跳过即可。因此逻辑为: if (i > 0 && nums[i] == nums[i-1]) continue;
  • 对于 left 和 right (内层双指针),我们是在找到一组解之后,为了跳过接下来重复的元素。比如数组 -2, 0, 0, 2,当 left 指向第一个 0 时找到了解,此时如果 numsleft == numsleft+1,我们必须把 left 移动到最后一个重复元素的下一个位置。
    因此去重逻辑为: while (left < right && nums[left] == nums[left+1]) left++;

提交代码,运行结果如下:


总结

算法复杂度分析

  • 时间复杂度: O ( N 2 ) O(N^2) O(N2)

    • 数组排序的时间复杂度是 O ( N log ⁡ N ) O(N \log N) O(NlogN);
    • 双重循环(外层遍历 i,内层双指针 while)的时间复杂度是 O ( N 2 ) O(N^2) O(N2);
    • 总体复杂度由 O ( N 2 ) O(N^2) O(N2) 主导;
  • 空间复杂度: O ( log ⁡ N ) O(\log N) O(logN)

    • 我们忽略存储答案的空间;
    • 主要消耗在于排序算法所使用的栈空间(Java 的 Arrays.sort 内部对于基本类型通常使用双轴快速排序,空间复杂度为 O ( log ⁡ N ) O(\log N) O(logN));

这道题是 "排序 + 双指针" 的经典应用,核心考点在于:利用排序预处理 将三数之和降维转化为两数之和问题;通过双指针相向移动 将时间复杂度从暴力的 O ( N 3 ) O(N^3) O(N3) 优化至 O ( N 2 ) O(N^2) O(N2);同时极度考验对边界去重逻辑 (固定位与双指针的双重去重)及剪枝优化的严谨处理能力。

希望这篇解析能帮你彻底掌握"三数之和"!如果有疑问,欢迎在评论区留言讨论。

相关推荐
Jack203 小时前
HarmonyOS开发中错误处理策略:网络异常统一处理
算法
小小杨树4 小时前
读懂色彩:拍照调色不再难
算法·计算机视觉·配色
唐青枫8 小时前
Java JDBC 实战指南:从 Connection 到事务和连接池
java
一个做软件开发的牛马9 小时前
MyBatis-Plus 从零实战:完整搭建可运行 Demo,BaseMapper 零 SQL、Wrapper 条件构造、分页插件与代码生成器详解
java·后端
用户3721574261359 小时前
Java 处理 PDF 图片:提取 PDF 中的图片,并压缩 PDF 图片体积
java
用户37215742613510 小时前
Java 打印 Word 文档:从基础打印到高级设置
java
JieE21220 小时前
LeetCode 226. 翻转二叉树|JS 递归超详细拆解,二叉树入门经典题
javascript·算法
JieE21221 小时前
LeetCode 104. 二叉树的最大深度|递归思路超详细拆解
javascript·算法
用户3521802454751 天前
当 Prompt 学会"热更新":Spring Boot × Nacos3 AI 实战
java·spring boot·ai编程