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. 排序数组

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

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

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. 完整代码

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) 的时间复杂度内高效解决问题,确保结果唯一且完整。理解去重的本质——对相同元素的“位置重复性”进行过滤,而非“值重复性”——是掌握该算法的关键。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值