力扣解题-三数之和

4 阅读4分钟

力扣解题-三数之和

给定一个整数数组 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,1,1] 输出:[] 解释:唯一可能的三元组和不为 0 。

示例 3: 输入:nums = [0,0,0] 输出:[[0,0,0]] 解释:唯一可能的三元组和为 0 。

提示: 3 <= nums.length <= 3000 -105 <= nums[i] <= 105

Related Topics 数组 双指针 排序


第一次解答

解题思路

核心方法:排序 + 固定单值 + 双指针查找剩余两数 + 全链路去重,将暴力解法的O(n³)时间复杂度优化至O(n²),同时通过有序性实现高效的重复值过滤,确保结果无重复且性能适配题目数据规模。

具体步骤:

  1. 数组排序预处理:首先对输入数组nums进行排序(Arrays.sort(nums)),核心作用有三:
    • 让相同元素相邻,为后续“跳过重复值”的去重逻辑提供基础;
    • 使数组具备有序性,可通过双指针的单向移动快速调整三数之和的大小;
    • 避免重复生成相同的三元组(如无序数组中[-1,0,1]和[0,-1,1]会被判定为不同组合,排序后则统一为[-1,0,1])。
  2. 外层循环固定第一个数:遍历数组下标i(作为三元组的第一个数nums[i]),循环终止于nums.length - 2(需预留至少两个位置给后续双指针leftright):
    • 关键去重操作:若i > 0nums[i] == nums[i-1],直接跳过当前i(避免因第一个数重复生成相同三元组,如连续的-1会导致重复的[-1,0,1])。
  3. 双指针查找剩余两数:对每个固定的i,初始化双指针:
    • left = i + 1:指向i的下一个位置(确保left > i,避免重复使用同一元素);
    • right = nums.length - 1:指向数组末尾; 在left < right的条件下循环,计算三数之和sum = nums[i] + nums[left] + nums[right],并根据sum的大小调整指针:
    • sum == 0:找到符合条件的三元组,将其添加到结果列表res
      • 立即跳过left的重复值:while (left < right && nums[left] == nums[left + 1]) left++(避免连续相同的left值生成重复三元组);
      • 立即跳过right的重复值:while (left < right && nums[right] == nums[right - 1]) right--(避免连续相同的right值生成重复三元组);
      • 指针双向收缩:left++right--,继续查找当前i下的其他可能组合。
    • sum < 0:三数之和偏小,需增大数值,因此将左指针left右移(有序数组中left右移会使nums[left]变大);
    • sum > 0:三数之和偏大,需减小数值,因此将右指针right左移(有序数组中right左移会使nums[right]变小)。
  4. 返回结果:遍历完成后,结果列表res中存储了所有和为0且不重复的三元组,直接返回即可。

性能说明

  • 时间复杂度:排序的O(nlogn) + 外层循环O(n) × 内层双指针循环O(n),整体为O(n²),适配题目n <= 3000的规模(3000²=9×10⁶次计算,在时间阈值内);
  • 耗时41ms击败28.57%的用户:是因为该解法是三数之和的“标准最优解”,多数提交均采用此逻辑,耗时差异主要来自评测机环境;
  • 内存消耗56.6MB击败98.98%的用户:核心原因是未使用额外的哈希表等存储结构,仅用结果列表存储最终答案,排序的空间开销为O(logn)(Java内置排序的栈空间),属于极低的内存占用。

执行耗时:41 ms,击败了28.57% 的Java用户 内存消耗:56.6 MB,击败了98.98% 的Java用户

public List<List<Integer>> threeSum(int[] nums) {
        Arrays.sort(nums);
        List<List<Integer>> res = new ArrayList<>();

        for (int i = 0; i < nums.length - 2; i++) {
            // 跳过外层重复
            if (i > 0 && nums[i] == nums[i - 1]) continue;

            int left = i + 1, right = nums.length - 1;
            while (left < right) {
                int sum = nums[i] + nums[left] + nums[right];
                if (sum == 0) {
                    res.add(Arrays.asList(nums[i], nums[left], nums[right]));

                    // 跳过 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) {
                    left++;
                } else {
                    right--;
                }
            }
        }
        return res;
    }

总结

  1. 该解法的核心是排序+双指针:排序解决了重复值过滤和指针移动的有序性问题,双指针将“查找剩余两数”的时间复杂度从O(n)降至O(1)(单次移动);
  2. 去重逻辑是关键:分别对第一个数、左指针、右指针的重复值进行跳过,确保结果无重复;
  3. 时间复杂度O(n²)是该问题的最优下界,无法进一步优化(需至少遍历所有可能的二元组),该解法已达到理论最优性能。