Java双指针算法精讲(六)|LeetCode 15 三数之和 原地去重无Set详解

📚 目录

  • [1. 题目解析](#1. 题目解析)

  • [2. 算法原理](#2. 算法原理)

  • [3. 编写代码](#3. 编写代码)

  力扣原题直达~三数之和

前置阅读:

  Java双指针算法精讲(五)|LCR 179 有序数组两数之和 剑指Offer详解

1. 题目解析

题干核心总结 :

  1. 选取3个下标互不相同的数字,满足三者相加等于0;
  2. 三元组内数字调换顺序视为同一答案,结果集合必须去重;
  3. 解题转化思路:固定第一个数字,剩余两数求和等于 -nums[i],复用前文有序两数之和双指针模板;
  4. 面试核心难点:不借助HashSet仅通过排序+指针原地跳过重复值实现去重

[🔙 返回目录](#🔙 返回目录)


2. 算法原理

  解决这一道题目:我们采取的方法还是双指针 ;

  以此数组为例:

暴力解法:

  使用三重循环进行枚举出所有情况,进行相加判断是否等于0,再通过hashset进行去重,最后放入到链表当中进行返回;

指标 复杂度 详细说明
时间复杂度 O ( n 3 ) O(n^3) O(n3) 三层嵌套循环枚举全部三元组,数据量稍大直接超时,无法通过LeetCode大数据用例
空间复杂度(不计结果集) O ( 1 ) O(1) O(1) 仅使用循环下标临时变量,无额外数组、哈希容器开辟
空间复杂度(含Set去重存储答案) O ( n ) O(n) O(n) 依赖HashSet存放结果实现去重,额外占用线性空间

双指针算法 :

  首先给数组进行排序:

   [-4 , -4 , -1 , -1 , -1 , 0 , 1 , 2] :排序后:

  算法原理:

  1. 初始化指针:固定外层下标 i,区间左边界 left = i+1,区间右边界 right = nums.length - 1,在 [left, right] 区间内查找两数之和等于 -nums[i],查找过程中,即使已经遇到了满足条件的数,也不要停下,直到left>=right的时候停;
  2. 单次区间遍历结束后,i 向右移动进入下一轮循环;
  3. 外层i指针去重 :若当前 nums[i] 与前一个数字 nums[i-1] 相等,直接跳过本轮;
    例:数组中两个 -4,第二个 -4 作为 i 时,会和前一个 -4 生成完全一致的三元组,直接跳过避免重复结果;
  4. 内层left、right指针去重 :匹配到一组和为0的三元组后,持续跳过左右侧相邻重复数字
      left指针:循环跳过后续和当前left相等的值,防止第二个数字重复;
      right指针:循环跳过前面和当前right相等的值,防止第三个数字重复;
  5. 重复整套逻辑,直至 i 遍历到数组倒数第三个元素,全部组合枚举完成。

[🔙 返回目录](#🔙 返回目录)


3. 编写代码

  边界处理:

  1. 外层固定指针 i 的边界去重
      边界条件i > 0 && nums[i] == nums[i - 1]
  • 下限边界 i>0:必须保证i存在前一位,防止数组下标越界;
  • 去重逻辑:若当前i值和上一轮数值相等,直接跳过本轮循环;
  • 示例边界:数组连续两个 -4,第二个i=-4时触发判断,跳过重复首元素,避免生成完全重复三元组;
      - 终止边界:i最大只能遍历到 nums.length - 3,预留left、right两个指针位置。
  1. 左指针 left 的边界去重
      边界条件left < right && nums[left] == nums[left + 1]
  • 核心边界 left < right:必须保证左指针始终在右指针左侧,防止下标交叉越界;
  • 去重逻辑:匹配一组合法三元组后,持续右移left,跳过所有连续重复值;
  • 临界场景:数组连续多个 -1,不加 left < right 会出现 left+1 > right 数组越界报错。
  1. 右指针 right 的边界去重
      边界条件left < right && nums[right] == nums[right - 1]
      - 边界约束 left < right:和左指针共用同一安全边界,避免right持续左移跨过left;
      - 去重逻辑:匹配成功后持续左移right,跳过右侧重复数值;
java 复制代码
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
          Arrays.sort(nums);
        int len = nums.length;
        List<List<Integer>> list = new LinkedList<>();
        for (int i = 0; i < len-2; i++) {
            if(i>0 && nums[i] == nums[i-1]) {
                continue;
            }
            //定义两个指针:
            int left = i+1;
            int right = len-1;
            while (left<right) {
                int sum = nums[i] + nums[left] +nums[right];
                List<Integer> ret = new LinkedList<>();
                if(sum == 0) {
                   //添加到链表中
                    ret.add(nums[i]);
                    ret.add(nums[left]);
                    ret.add(nums[right]);
                    //添加完,放入集合
                    list.add(ret);
                    //边界处理
                    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 list;
    }
}

易错点:

指针 边界判断条件 边界作用 越界风险
外层i i > 0 保证存在前一位元素用于去重 i=0时 i-1=-1,数组下标越界
左left left < right 保证左指针在右指针左侧 left+1 > right,访问不存在数组下标
右right left < right 限制右指针不跨过左指针 right-1 < left,下标反向越界
全局前置 nums.length < 3 过滤无合法三元组场景 循环内i、left、right下标访问报错

时间、空间复杂度分析

指标 复杂度 详细说明
时间复杂度 O ( n 2 ) O(n^2) O(n2) 1. 排序开销 O ( n log ⁡ n ) O(n\log n) O(nlogn); 2. 外层i单层循环遍历数组 O ( n ) O(n) O(n),内层双指针最多遍历一次剩余区间 O ( n ) O(n) O(n); n 2 \boldsymbol{n^2} n2 量级主导整体耗时,远优于暴力三层循环 O ( n 3 ) O(n^3) O(n3)
额外空间复杂度 O ( log ⁡ n ) O(\log n) O(logn) 仅排序递归栈占用空间; 结果集合List为题目输出要求,不计入额外空间;全程无HashSet、额外数组,无线性空间开销

[🔙 返回目录](#🔙 返回目录)