LeetCode 15 三数之和:python3 题解


题目链接:15. 三数之和


1. 题目理解

目标:在一个整数数组 nums 中,找到所有不重复的三元组 [a, b, c],使得 a + b + c = 0

关键约束

  1. 下标不同:三个数必须来自数组中不同的位置。
  2. 结果不重复:例如 [-1, 0, 1][0, -1, 1] 被视为同一个三元组,结果中只能出现一次。
  3. 效率:数组长度最大为 3000,这意味着 \(O(N^3)\) 的暴力解法会超时,我们需要 \(O(N^2)\) 或更优的解法。

2. 解题思路演变

为了更好地理解最优解,我们先从最直观的想法开始,逐步优化。

思路一:暴力枚举(不可行)

最直观的方法是使用三层循环:

  1. 第一层选第一个数 nums[i]
  2. 第二层选第二个数 nums[j]
  3. 第三层选第三个数 nums[k]
  4. 判断和是否为 0,并用集合(Set)去重。
  • 时间复杂度\(O(N^3)\)。当 \(N=3000\) 时,运算次数约为 270 亿次,必然超时 (Time Limit Exceeded)
  • 去重难点:暴力法去重非常麻烦,需要额外存储已见过的组合。

思路二:排序 + 双指针(最优解)

这是解决此类问题(K-Sum)的标准范式。核心思想是将问题转化为 “固定一个数,寻找两数之和”

为什么要排序?

  1. 方便去重:排序后,相同的数字会挨在一起。如果我们发现当前数字和前一个数字一样,就可以直接跳过,从根源上避免重复三元组。
  2. 使用双指针:排序后,数组有序。如果当前和太小,我们可以确定地向右移动指针来增大和;如果和太大,向左移动指针来减小和。这避免了第三层循环。

算法步骤

  1. 排序:将数组 nums 从小到大排序。
  2. 遍历第一个数:用索引 i 遍历数组,nums[i] 作为三元组的第一个数。
    • 剪枝优化:如果 nums[i] > 0,因为数组已排序,后面的数都大于 0,三数之和不可能为 0,直接结束循环。
    • 去重:如果 i > 0nums[i] == nums[i-1],说明这个数字作为第一个数已经处理过了,跳过(continue)。
  3. 双指针寻找后两个数
    • 定义左指针 left = i + 1,右指针 right = len(nums) - 1
    • left < right 时循环:
      • 计算总和 s = nums[i] + nums[left] + nums[right]
      • 如果 s == 0:找到一组解!
        • 加入结果集。
        • 移动 leftright
        • 关键去重:移动后,如果 nums[left] 和刚才的值一样,继续右移;如果 nums[right] 和刚才的值一样,继续左移。防止重复解。
      • 如果 s < 0:和太小,需要变大,left 右移。
      • 如果 s > 0:和太大,需要变小,right 左移。

3. 代码实现 (Python 3)

from typing import List

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        # 1. 基础边界检查:如果数组少于 3 个数,不可能组成三元组
        if not nums or len(nums) < 3:
            return []
        
        # 2. 排序:这是使用双指针和方便去重的前提
        # 时间复杂度 O(N log N)
        nums.sort()
        
        n = len(nums)
        result = []
        
        # 3. 遍历第一个数 nums[i]
        # 只需要遍历到 n-2,因为后面至少要留两个数给 left 和 right
        for i in range(n - 2):
            # --- 剪枝优化 ---
            # 如果当前数字大于 0,由于数组已排序,后面的数字也都大于 0
            # 三数之和一定大于 0,不可能等于 0,直接结束循环
            if nums[i] > 0:
                break
            
            # --- 第一个数去重 ---
            # 如果当前数字和前一个数字相同,说明作为第一个数的情况已经处理过了
            # 跳过本次循环,避免重复三元组
            if i > 0 and nums[i] == nums[i - 1]:
                continue
            
            # 4. 双指针寻找剩下的两个数
            # left 指向 i 的下一个位置,right 指向数组末尾
            left = i + 1
            right = n - 1
            
            while left < right:
                total = nums[i] + nums[left] + nums[right]
                
                if total == 0:
                    # 找到满足条件的三元组
                    result.append([nums[i], nums[left], nums[right]])
                    
                    # --- 第二、三个数去重 ---
                    # 找到解后,移动指针前,先跳过所有重复的 left 和 right 值
                    # 例如:[-2, 0, 0, 2, 2],找到 [-2, 0, 2] 后,跳过中间的 0 和 2
                    while left < right and nums[left] == nums[left + 1]:
                        left += 1
                    while left < right and nums[right] == nums[right - 1]:
                        right -= 1
                    
                    # 移动到下一个不同的数字
                    left += 1
                    right -= 1
                    
                elif total < 0:
                    # 和太小,需要增大,左指针右移
                    left += 1
                else:
                    # 和太大,需要减小,右指针左移
                    right -= 1
                    
        return result

4. 图解演示

假设输入 nums = [-1, 0, 1, 2, -1, -4]

  1. 排序后[-4, -1, -1, 0, 1, 2]
  2. 第一轮 (i=0): nums[i] = -4
    • left 指向 -1 (索引 1), right 指向 2 (索引 5)。
    • 和 = -4 + (-1) + 2 = -3 < 0。left 右移。
    • ... 经过多次移动,无法找到和为 0 的组合(因为 -4 太小了)。
  3. 第二轮 (i=1): nums[i] = -1
    • left 指向 -1 (索引 2), right 指向 2 (索引 5)。
    • 和 = -1 + (-1) + 2 = 0。找到解 [-1, -1, 2]
    • 记录结果,移动指针并去重。
  4. 第三轮 (i=2): nums[i] = -1
    • 发现 nums[2] == nums[1]跳过(去重逻辑生效)。
  5. 第四轮 (i=3): nums[i] = 0
    • left 指向 1, right 指向 2
    • 和 = 0 + 1 + 2 = 3 > 0。right 左移。
    • left 指向 1, right 指向 1。循环结束。
    • (注:实际逻辑中会找到 [-1, 0, 1],此处为简化演示)。

5. 复杂度分析

  • 时间复杂度: \(O(N^2)\)
    • 排序消耗 \(O(N \log N)\)
    • 外层循环遍历 \(N\) 次,内层双指针遍历 \(N\) 次。总共 \(N \times N = N^2\)
    • \(N^2\) 远大于 \(N \log N\),所以主导项是 \(O(N^2)\)
    • 对于 \(N=3000\)\(N^2 = 9 \times 10^6\),完全可以在 1 秒内完成。
  • 空间复杂度: \(O(1)\)\(O(N)\)
    • 取决于排序算法的实现(Python 的 sort 通常是 Timsort,空间复杂度 \(O(N)\))。
    • 如果不算存储结果的空间,算法本身只用了几个指针变量,是常数级别。

6. 常见坑点与注意事项

  1. 去重逻辑的位置
    • 外层循环去重:必须在 i > 0 时判断 nums[i] == nums[i-1]。不能在 i=0 时判断,否则 i-1 越界。
    • 内层指针去重:必须在找到 total == 0 之后 进行。如果在 total != 0 时去重,可能会漏掉某些组合(虽然在这个特定题目逻辑下通常不会,但标准写法是在找到解后跳过重复值)。
  2. 指针移动顺序
    • 在找到解后,先执行 while 循环跳过重复值,最后再执行 left += 1right -= 1 指向新的值。如果顺序反了,逻辑会变复杂。
  3. 剪枝
    • if nums[i] > 0: break 这个优化非常关键,能大幅减少不必要的计算,特别是在数组中有很多正数的时候。

7. 总结

这道题是面试中非常经典的数组双指针题目。

  • 核心技巧:排序 + 双指针。
  • 核心难点:如何优雅地去除重复三元组。


posted @ 2026-03-03 17:04  MoonOut  阅读(86)  评论(0)    收藏  举报