15. 三数之和

15. 三数之和

✨核心逻辑

本题采用 排序 + 双指针 的策略:

  1. 排序预处理:首先对数组进行排序,排序后不仅可以利用有序性快速查找到目标,还方便跳过重复元素。
  2. 固定第一层循环:遍历数组的每一个元素 nums[i],将其作为三个数中的第一个数。因为要求三数之和为 0,所以问题就转化为了寻找两数之和等于 -nums[i]
  3. 双指针寻找:在 i 之后的区间 [i+1, n-1] 内,使用对撞指针 leftright 查找两个数,使它们的和等于目标值。
  4. 详细的去重逻辑:排序数组中可能包含大量重复元素,若不去重会导致结果中有重复的三元组,因此在遍历 i、移动 leftright 时都需要专门逻辑跳过重复值。

🔥代码实现(含详细变量注释)

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        // n:记录数组的长度
        int n = nums.length;
        // 1. 先对数组进行升序排序,这是后续能够使用双指针和去重的前提
        Arrays.sort(nums);
        // ans:用于存放最终所有不重复的符合条件的三元组集合
        List<List<Integer>> ans = new ArrayList<List<Integer>>();
        
        // i:外层循环指针,遍历整个数组,作为三元组的第一个固定数
        for (int i = 0; i < nums.length; i++) {
            // 剪枝优化:因为数组已经排序,如果当前固定的 nums[i] 已经大于 0
            // 那么它后面的数肯定也都大于 0,三个正数相加不可能等于 0,直接结束循环
            if (nums[i] > 0) {
                break;
            }
            // 第一层去重:如果当前固定的 nums[i] 与前一个数相同(如 -3, -3, 1, 2)
            // 那么该数作为首元素的情况已经处理过,直接跳过避免结果重复
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            
            // left:左指针,指向当前 i 之后的第一个元素
            int left = i + 1;
            // right:右指针,指向数组的最后一个元素
            int right = n - 1;
            
            // 当左指针在右指针左边时,继续查找
            while (left < right) {
                // num:计算当前三个指针指向的元素之和
                int num = nums[i] + nums[left] + nums[right];
                
                if (num == 0) {
                    // 如果三数之和正好为 0,将这三个元素构成 List 添加到结果集中
                    ans.add(Arrays.asList(nums[i], nums[left], nums[right]));
                    
                    // 第二层去重(左指针):在找到一组答案后,如果左指针右边相邻的元素和当前相同,跳过
                    while (left < right && nums[left] == nums[left + 1]) left++;
                    // 第三层去重(右指针):在找到一组答案后,如果右指针左边相邻的元素和当前相同,跳过
                    while (left < right && nums[right] == nums[right - 1]) right--;
                    
                    // 左右指针同时向中间收缩,继续寻找下一组可能的三元组
                    left++;
                    right--;
                } else if (num < 0) {
                    // 如果三数之和小于 0,说明左侧的数太小,左指针向右移动寻找更大的数
                    left++;
                } else {
                    // 如果三数之和大于 0,说明右侧的数太大,右指针向左移动寻找更小的数
                    right--;
                }
            }
        }
        // 循环结束后返回包含所有不重复三元组的结果集
        return ans;
    }
}

  • ⏱️复杂度分析
    • 时间复杂度:O(N^2),其中 N 是数组的长度。排序的时间复杂度为 O(N log N),双指针遍历部分的外层循环为 O(N),内层双指针寻找为 O(N),总体时间复杂度为 O(N^2)。

    • 空间复杂度:O(log N) 或 O(1)。主要消耗在于排序算法所需的栈空间(Java 的 Arrays.sort 底层是双轴快排,占用 O(log N) 栈空间)。如果不计入返回值 ans 占用的空间,额外使用的指针变量为 O(1)。

🔍总结一下:

排序的目的是为了使用双指针,只前有总结过双指针的适用场景。然后定A,开双指针B,C即可