leetcode 15.三数之和 思路分析

三数之和问题:双指针法与去重逻辑详解

最近在leetcode刷题时,在三数之和上面产生了一些理解上的误区,包括阅读carl的视频后对于部分内容的理解仍然存在偏差。

究其根本,是在于对每次返回数组首位元素遍历可能性产生了误解:

在该题目中,每次遍历应将首位数字的所有结果全部存入结果数组,以方便去重,而不是每次存入一个结果

一、问题定义与核心难点

给定一个整数数组 nums,找出所有满足以下条件的不重复三元组 [nums[i], nums[j], nums[k]]

  1. i、j、k 互不相同;
  2. 三元组元素之和为 0
  3. 结果中不存在重复的三元组(如 [1, -1, 0][-1, 1, 0] 视为同一组合,需去重)。

核心难点:去重逻辑

如何避免重复的三元组是算法设计的关键。例如,数组 [-1, -1, 2, 0, 1] 中,[-1, -1, 2][-1, 0, 1] 是合法解,但需确保每个解仅出现一次。

二、算法思路:排序+双指针法

1. 排序数组(预处理)

  • 目的:使相同元素相邻,便于后续去重;为双指针移动提供有序环境(左指针右移增大和,右指针左移减小和)。
  • 操作 :使用 Arrays.sort(nums) 将数组升序排列。

2. 固定第一个元素,双指针查找剩余两数

  • 遍历第一个元素 i :从数组头部开始,固定 nums[i],在剩余元素 [i+1, n-1] 中找两数 nums[j]nums[k],使得 nums[i] + nums[j] + nums[k] = 0
  • 双指针初始化 :左指针 j = i + 1(最小剩余元素),右指针 k = n - 1(最大剩余元素)。
  • 指针移动逻辑
    • nums[j] + nums[k] < -nums[i]:左指针右移(和太小,需增大)。
    • nums[j] + nums[k] > -nums[i]:右指针左移(和太大,需减小)。
    • 若相等:记录解,并移动双指针跳过重复元素(去重)。

三、三次去重操作:避免重复的关键

1. 第一个元素去重(外层循环)

  • 条件 :当 i > 0nums[i] == nums[i-1] 时,跳过当前 i
  • 逻辑:数组有序,若当前元素与前一个相同,则以当前元素开头的三元组必然与前一个元素开头的三元组重复(因为后续元素相同)。
  • 示例 :数组 [-1, -1, 0, 1],当 i=1 时,nums[i] == nums[i-1],直接跳过,避免重复处理以 -1 开头的组合。

2. 第二个元素去重(内层左指针)

  • 条件 :找到解 (i, j, k) 后,若 nums[j] == nums[j+1],右移 j 直到遇到不同元素。
  • 逻辑 :同一层循环中,固定 i 后,若 j 指向的元素重复,生成的三元组会重复(如 [-1, -1, 2] 若不跳过重复的 -1,会多次记录)。

3. 第三个元素去重(内层右指针)

  • 条件 :找到解 (i, j, k) 后,若 nums[k] == nums[k-1],左移 k 直到遇到不同元素。
  • 逻辑 :与第二个元素去重类似,确保同一 i 下,k 指向的元素唯一。

四、分步骤代码实现

1. 排序数组

java 复制代码
Arrays.sort(nums); // 升序排序,使相同元素相邻,便于去重

2. 遍历第一个元素并去重

java 复制代码
for (int i = 0; i < nums.length - 2; i++) {
    // 第一个元素去重:跳过与前一个相同的元素,避免重复组合
    if (i > 0 && nums[i] == nums[i-1]) {
        continue;
    }
    int target = -nums[i]; // 剩余两数之和需为 -nums[i]
    int j = i + 1, k = nums.length - 1; // 双指针初始化
    // 双指针查找剩余两数
    while (j < k) {
        int sum = nums[j] + nums[k];
        if (sum == target) {
            // 记录合法解
            res.add(Arrays.asList(nums[i], nums[j], nums[k]));
            // 第二个元素去重:跳过所有重复的 nums[j]
            while (j < k && nums[j] == nums[j+1]) {
                j++;
            }
            // 第三个元素去重:跳过所有重复的 nums[k]
            while (j < k && nums[k] == nums[k-1]) {
                k--;
            }
            // 移动指针继续查找
            j++;
            k--;
        } else if (sum < target) {
            j++; // 和太小,左指针右移
        } else {
            k--; // 和太大,右指针左移
        }
    }
}

3. 完整代码

java 复制代码
import java.util.*;

public class ThreeSum {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        if (nums == null || nums.length < 3) {
            return res; // 特判:元素不足3个,直接返回
        }
        Arrays.sort(nums); // 排序,为去重和双指针做准备
        int n = nums.length;
        
        for (int i = 0; i < n - 2; i++) {
            // 第一个元素去重:跳过与前一个相同的元素
            if (i > 0 && nums[i] == nums[i-1]) {
                continue;
            }
            int target = -nums[i]; // 剩余两数需和为 -nums[i]
            int j = i + 1, k = n - 1; // 双指针初始化
            
            while (j < k) {
                int sum = nums[j] + nums[k];
                if (sum == target) {
                    // 添加合法解
                    res.add(Arrays.asList(nums[i], nums[j], nums[k]));
                    // 跳过重复的第二个元素
                    while (j < k && nums[j] == nums[j + 1]) {
                        j++;
                    }
                    // 跳过重复的第三个元素
                    while (j < k && nums[k] == nums[k - 1]) {
                        k--;
                    }
                    // 移动指针,继续寻找下一组解
                    j++;
                    k--;
                } else if (sum < target) {
                    j++; // 和太小,左指针右移
                } else {
                    k--; // 和太大,右指针左移
                }
            }
        }
        return res;
    }

    public static void main(String[] args) {
        ThreeSum solution = new ThreeSum();
        int[] nums = {-1, 0, 1, 2, -1, -4};
        System.out.println(solution.threeSum(nums)); // 输出 [[-1, -1, 2], [-1, 0, 1]]
    }
}

五、关键细节与示例验证

1. 去重逻辑的数学证明

  • 第一个元素去重 :假设 nums[i] = nums[i-1],由于数组有序,i 位置的元素与 i-1 位置元素相同,且后续元素相同,因此以 i 开头的三元组必然与以 i-1 开头的三元组重复,跳过 i 不影响结果完整性。
  • 第二、三个元素去重 :在固定 i 的情况下,若 nums[j] 重复,双指针移动时会再次遇到相同值,导致重复解,因此必须跳过。

2. 示例:数组 [-1, -1, 0, 1, 2]

  1. 排序后[-1, -1, 0, 1, 2]
  2. 遍历 i=0(第一个 -1
    • 双指针找到 j=1(第二个 -1)、k=42),和为 0,记录 [-1, -1, 2]
    • 第二个元素去重:nums[j] == nums[j+1] 不成立(j=1 后是 0),直接移动 j=2k=3,和为 0+1=1,等于 target=1,记录 [-1, 0, 1]
  3. 遍历 i=1(第二个 -1
    • 由于 nums[i] == nums[i-1],跳过,避免重复处理以 -1 开头的组合。

六、总结:去重的核心原则

  1. 排序是基础:有序数组让相同元素相邻,为去重提供条件。
  2. 分层去重
    • 第一层(第一个元素):确保每个不同的起始值仅处理一次,避免重复的"头部"组合。
    • 第二、三层(剩余元素):在固定头部的情况下,确保同一层内的中间和尾部元素唯一,避免重复的"身体"和"尾部"组合。
  3. 逻辑完整性 :去重操作不会漏掉合法解,因为每个不同的起始值(如第一个 -1)会处理所有可能的后续组合(如第二个 -101 等),而重复的起始值(如第二个 -1)会被跳过,避免冗余计算。

通过这三次去重,算法在 O ( n 2 ) O(n^2) O(n2) 的时间复杂度内高效解决问题,确保结果唯一且完整。理解去重的本质------对相同元素的"位置重复性"进行过滤,而非"值重复性"------是掌握该算法的关键。

相关推荐
lingxiao1688822 分钟前
双目立体视觉
图像处理·算法·机器学习·计算机视觉
JNU freshman26 分钟前
和为target问题汇总
算法
2401_8590490830 分钟前
MSPM0--Timer(一口一口喂版)
arm开发·单片机·mcu·算法
寂空_36 分钟前
【算法笔记】ACM数论基础模板
c++·笔记·算法
ggabb44 分钟前
当九九乘法口诀“出海”英国:文化碰撞下的数学教育变革
算法
爱coding的橙子1 小时前
每日算法刷题计划Day7 5.15:leetcode滑动窗口4道题,用时1h
算法·leetcode
wuqingshun3141591 小时前
蓝桥杯 10. 全球变暖
c++·算法·职场和发展·蓝桥杯
阳洞洞1 小时前
leetcode 56. 合并区间
leetcode
手握风云-1 小时前
二叉树深搜:在算法森林中寻找路径
算法
xu_wenming1 小时前
华为Watch的ECG功能技术分析
人工智能·嵌入式硬件·算法