【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);同时极度考验对边界去重逻辑 (固定位与双指针的双重去重)及剪枝优化的严谨处理能力。

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

相关推荐
希望永不加班1 小时前
SpringBoot 服务注册与发现:Nacos/Consul/Eureka
java·spring boot·eureka·consul·java-consul
逻辑君1 小时前
Foresight研究报告【20260010】
人工智能·算法·机器学习
仙俊红2 小时前
spring有多个对象时如何注入
java·后端·spring
一切皆是因缘际会2 小时前
AI高速迭代下的技术风险与理性突围
大数据·数据结构·人工智能·架构
专注VB编程开发20年2 小时前
B4A (Basic4Android) Process_Globals(应用全局)和 Globals(类中公用变量)
java·开发语言
weixin_468466852 小时前
大语言模型原理新手入门指南
人工智能·python·算法·语言模型·自然语言处理·transformer·注意力机制
小a杰.2 小时前
PTO ISA 指令架构 - PTO虚拟指令集架构解析
java·开发语言·架构
Java爱好狂.2 小时前
Redis高级笔记:深入浅出Java面试高频考点!
java·数据库·redis·后端·java面试·java程序员·java八股文
z200509302 小时前
今日算法(回溯找IP,加检测)
算法·leetcode