Files
interview/16-LeetCode Hot 100/三数之和-改进版示例.md

16 KiB
Raw Blame History

三数之和 (3Sum) - 改进版示例

LeetCode 15. Medium

题目描述

给你一个整数数组 nums,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k,同时还满足 nums[i] + nums[j] + nums[k] == 0

请你返回所有和为 0 且不重复的三元组。

示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]

示例 2:

输入:nums = [0,1,1]
输出:[]

思路推导

暴力解法分析

def threeSum_brute(nums):
    result = []
    n = len(nums)
    for i in range(n):
        for j in range(i+1, n):
            for k in range(j+1, n):
                if nums[i] + nums[j] + nums[k] == 0:
                    # 使用集合去重
                    triplet = sorted([nums[i], nums[j], nums[k]])
                    if triplet not in result:
                        result.append(triplet)
    return result

时间复杂度: O(n³) 空间复杂度: O(1) (不考虑结果存储) 问题: n=2000时,操作次数约80亿次,超时!

优化思考 - 降维思想

观察: 固定第一个数后,问题变成"两数之和"

三数之和: nums[i] + nums[j] + nums[k] = 0
↓ 固定 nums[i]
两数之和: nums[j] + nums[k] = -nums[i]

两数之和的优化:

  • 暴力: O(n²) 遍历所有对
  • 优化: O(n) 双指针(前提:数组有序)

总复杂度: O(n) × O(n) = O(n²)

为什么排序后能用双指针?

核心原理:单调性

有序数组: [-4, -1, -1, 0, 1, 2]
                ↑   ↑      ↑
                i   left   right

如果 sum = nums[left] + nums[right] < 0:
  - 需要增大和
  - left++ → nums[left]增大(数组升序) → sum增大
  - right-- → nums[right]减小 → sum减小 ✗

如果 sum = nums[left] + nums[right] > 0:
  - 需要减小和
  - right-- → nums[right]减小 → sum减小
  - left++ → nums[left]增大 → sum增大 ✗

为什么无序数组不行?

无序: [2, -1, 0, -4, 1]
         ↑   ↑   ↑
        i   L   R

sum = 2 + (-1) + 1 = 2 > 0
应该移动哪个指针? 无法确定!

排序的三大作用

  1. 去重基础:相同数字相邻,便于跳过
  2. 双指针前提:利用单调性优化
  3. 提前终止:排序后,如果当前数>0,后面都>0

解题思路

核心思想

排序 + 双指针 + 去重

  • 排序:为双指针和去重创造条件
  • 固定一个数:将三数问题降维为两数问题
  • 双指针: O(n) 解决两数之和
  • 多重去重:避免重复结果

算法流程(详细版)

步骤1:预处理 - 排序

nums.sort()  # [-1,0,1,2,-1,-4] → [-4,-1,-1,0,1,2]

为什么排序?

原始: [-1,0,1,2,-1,-4]  →  重复:-1出现两次
排序: [-4,-1,-1,0,1,2]  →  重复:-1相邻,便于跳过

步骤2:外层循环 - 固定第一个数

for i in range(len(nums) - 2):  # ← 为什么-2?留2个数给双指针

    # 去重1:跳过重复的第一个数
    if i > 0 and nums[i] == nums[i-1]:
        continue

    # 提前终止:如果最小数>0,后面不可能和为0
    if nums[i] > 0:
        break

    # 双指针找后两个数
    left, right = i + 1, len(nums) - 1
    ...

关键点解析:

Q1: 为什么循环到 len(nums)-2?

数组: [0, 1, 2]
索引:  0  1  2

如果 i = 2 (最后一个元素):
  left = 3 → 越界!

所以 i 最大 = len(nums) - 3 = 1
循环条件: range(len(nums) - 2) → [0, 1]

Q2: 为什么判断 i > 0?

i = 0:第一个元素,没有前一个元素,不用判断重复
i > 0:后续元素,需要判断是否与前一个相同

错误写法:
if nums[i] == nums[i-1]:  # i=0时越界!

正确写法:
if i > 0 and nums[i] == nums[i-1]:  # 安全

Q3: 为什么break而不是continue?

排序后: [-4, -1, -1, 0, 1, 2]
                     ↑
                    i=3, nums[i]=0 > 0

后续: [1, 2] 都 > 0
三数和: 0 + 1 + 2 = 3 > 0

结论:后面不可能有和为0的组合,直接退出

步骤3:内层双指针 - 两数之和

while left < right:
    current_sum = nums[i] + nums[left] + nums[right]

    if current_sum == 0:
        result.append([nums[i], nums[left], nums[right]])

        # 去重2:跳过重复的left
        while left < right and nums[left] == nums[left+1]:
            left += 1

        # 去重3:跳过重复的right
        while left < right and nums[right] == nums[right-1]:
            right -= 1

        # 继续寻找下一组
        left += 1
        right -= 1

    elif current_sum < 0:
        left += 1  # 需要更大的和
    else:
        right -= 1  # 需要更小的和

为什么找到答案后要同时移动两个指针?

数组: [-2, 0, 1, 1, 2]
        i  L  R

找到: -2 + 0 + 2 = 0 ✓

如果只移动L: L=2, nums[L]=1
  sum = -2 + 1 + 2 = 1 > 0 → R--
  但这样会错过可能的组合

正确:同时移动
  L=1, R=3: -2 + 1 + 1 = 0 ✓ (找到第二个)

关键细节说明

细节1:去重逻辑的三重保障

# 去重1:外层循环,跳过重复的第一个数
if i > 0 and nums[i] == nums[i-1]:
    continue

# 去重2:找到答案后,跳过重复的left
while left < right and nums[left] == nums[left+1]:
    left += 1

# 去重3:找到答案后,跳过重复的right
while left < right and nums[right] == nums[right-1]:
    right -= 1

示例:

输入: [-2, -1, -1, 0, 1, 1, 2]

第一轮: i=0, nums[i]=-2
  双指针找到: [-2, 0, 2], [-2, 1, 1]

第二轮: i=1, nums[i]=-1
  双指针找到: [-1, -1, 2], [-1, 0, 1]

第三轮: i=2, nums[i]=-1
  与i=1相同 → 跳过 (去重1)

第四轮: i=3, nums[i]=0
  双指针找到: [0, -1, 1] → 已存在

第五轮: i=4, nums[i]=1
  与i=3相同 → 跳过 (去重1)

结果: [[-2,0,2], [-2,1,1], [-1,-1,2], [-1,0,1]]

细节2:为什么 left < right 而不是 <=?

while left < right:  # 正确
    ...

while left <= right:  # 错误
    ...

原因:

left = right 时,只有一个元素
两数之和需要两个不同的元素
所以 left < right,不允许相同位置

细节3:为什么先去重再移动?

# 正确顺序
while left < right and nums[left] == nums[left+1]:
    left += 1  # 先跳过所有重复
left += 1      # 再移动到新元素

# 错误顺序
left += 1          # 先移动
while left < right and nums[left] == nums[left-1]:
    left += 1      # 可能漏掉某些重复

边界条件分析

边界1:数组长度不足

输入: [0, 1]
输出: []

分析: len(nums) = 2
循环: range(2-2) = range(0) → 不执行

边界2:全部为0

输入: [0, 0, 0, 0]
输出: [[0, 0, 0]]

排序: [0, 0, 0, 0]

i=0:
  left=1, right=3
  sum = 0+0+0 = 0 ✓ → result = [[0,0,0]]
  去重left: left=1,2,3 (跳过所有0)
  去重right: right=3,2,1
  left >= right,退出内层循环

i=1:
  nums[1] == nums[0] → 跳过

最终: [[0, 0, 0]]

边界3:没有答案

输入: [0, 1, 2]
输出: []

排序: [0, 1, 2]

i=0:
  left=1, right=2
  sum = 0+1+2 = 3 > 0 → right--

  left=1, right=1 → left >= right,退出

i=1:
  nums[1] = 1 > 0 → break

最终: []

复杂度分析(详细版)

时间复杂度

1. 排序: O(n log n)
   - 快速排序平均情况

2. 外层循环: O(n)
   for i in range(n)

3. 内层双指针: O(n)
   while left < right
   - 每次循环 left++ 或 right--
   - 最多执行 n 次

4. 总复杂度: O(n log n) + O(n²) = O(n²)
   - n² >> n log n (当 n 较大时)
   - 渐近复杂度取最高阶

空间复杂度

1. 排序栈空间: O(log n)
   - 快速排序递归深度

2. 结果存储: O(k)
   - k 为结果数量

3. 指针变量: O(1)

4. 总复杂度: O(log n) (不考虑结果存储)

代码实现

def threeSum(nums):
    """
    三数之和 - 排序 + 双指针解法

    Args:
        nums: 输入数组

    Returns:
        所有不重复的三元组,和为0
    """
    result = []
    nums.sort()  # 步骤1:排序

    # 步骤2:外层循环,固定第一个数
    for i in range(len(nums) - 2):

        # 去重1:跳过重复的第一个数
        if i > 0 and nums[i] == nums[i-1]:
            continue

        # 提前终止:如果最小数>0,后面不可能和为0
        if nums[i] > 0:
            break

        # 步骤3:双指针找后两个数
        left, right = i + 1, len(nums) - 1

        while left < right:
            current_sum = nums[i] + nums[left] + nums[right]

            if current_sum == 0:
                # 找到答案
                result.append([nums[i], nums[left], nums[right]])

                # 去重2:跳过重复的left
                while left < right and nums[left] == nums[left+1]:
                    left += 1

                # 去重3:跳过重复的right
                while left < right and nums[right] == nums[right-1]:
                    right -= 1

                # 继续寻找下一组
                left += 1
                right -= 1

            elif current_sum < 0:
                # 和太小,需要增大 → left++
                left += 1
            else:
                # 和太大,需要减小 → right--
                right -= 1

    return result

执行过程演示

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

初始状态:
nums = [-1, 0, 1, 2, -1, -4]
排序后: [-4, -1, -1, 0, 1, 2]
result = []

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

第一轮: i = 0, nums[i] = -4
       [-4, -1, -1, 0, 1, 2]
         ↑   ↑           ↑
         i  left        right

  sum = -4 + (-1) + 2 = -3 < 0
  left++ → left = 1

       [-4, -1, -1, 0, 1, 2]
         ↑       ↑       ↑
         i      left    right

  sum = -4 + (-1) + 2 = -3 < 0
  left++ → left = 2

       [-4, -1, -1, 0, 1, 2]
         ↑        ↑      ↑
         i       left   right

  sum = -4 + (-1) + 2 = -3 < 0
  left++ → left = 3

       [-4, -1, -1, 0, 1, 2]
         ↑         ↑     ↑
         i        left  right

  sum = -4 + 0 + 2 = -2 < 0
  left++ → left = 4

       [-4, -1, -1, 0, 1, 2]
         ↑          ↑   ↑
         i         left right

  sum = -4 + 1 + 2 = -1 < 0
  left++ → left = 5

  left >= right,退出内层循环

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

第二轮: i = 1, nums[i] = -1
       [-4, -1, -1, 0, 1, 2]
              ↑   ↑      ↑
              i  left   right

  sum = -1 + (-1) + 2 = 0 ✓
  result = [[-1, -1, 2]]

  去重left: nums[2] = -1 == nums[3] = 0? No
  去重right: nums[5] = 2 == nums[4] = 1? No
  left++, right-- → left = 3, right = 4

       [-4, -1, -1, 0, 1, 2]
              ↑      ↑   ↑
              i     left right

  sum = -1 + 0 + 1 = 0 ✓
  result = [[-1, -1, 2], [-1, 0, 1]]

  去重left: nums[3] = 0 == nums[4] = 1? No
  去重right: nums[4] = 1 == nums[3] = 0? No
  left++, right-- → left = 4, right = 3

  left >= right,退出内层循环

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

第三轮: i = 2, nums[i] = -1
       [-4, -1, -1, 0, 1, 2]
                 ↑
                 i

  判断: nums[2] == nums[1] == -1? Yes
  跳过 (去重1)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

第四轮: i = 3, nums[i] = 0
       [-4, -1, -1, 0, 1, 2]
                     ↑
                     i

  判断: nums[3] > 0? Yes
  break (提前终止)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

最终结果: [[-1, -1, 2], [-1, 0, 1]]

常见错误

错误1:忘记排序

错误写法:

def threeSum(nums):
    result = []
    for i in range(len(nums)):
        left, right = i + 1, len(nums) - 1
        while left < right:
            if nums[i] + nums[left] + nums[right] == 0:
                result.append([nums[i], nums[left], nums[right]])
            # ... 双指针移动
    return result

问题:无序数组无法使用双指针

正确写法:

nums.sort()  # 先排序!
for i in range(len(nums) - 2):
    ...

错误2:去重逻辑不完整

错误写法:

if nums[i] == nums[i-1]:  # i=0时越界
    continue

正确写法:

if i > 0 and nums[i] == nums[i-1]:
    continue

错误3:指针移动条件错误

错误写法:

if current_sum == 0:
    result.append([nums[i], nums[left], nums[right]])
    left += 1  # 只移动一个指针

正确写法:

if current_sum == 0:
    result.append([nums[i], nums[left], nums[right]])
    # 去重
    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

错误4:循环边界错误

错误写法:

for i in range(len(nums)):  # 可能越界
    left, right = i + 1, len(nums) - 1

正确写法:

for i in range(len(nums) - 2):  # 留2个位置
    left, right = i + 1, len(nums) - 1

变体问题

变体1:四数之和 (LeetCode 18)

题目:找出四数之和等于target的所有组合

思路:三层循环 + 双指针

def fourSum(nums, target):
    result = []
    nums.sort()

    for i in range(len(nums) - 3):
        for j in range(i + 1, len(nums) - 2):
            left, right = j + 1, len(nums) - 1

            while left < right:
                sum = nums[i] + nums[j] + nums[left] + nums[right]

                if sum == target:
                    result.append([nums[i], nums[j], nums[left], nums[right]])
                    # 去重...
                elif sum < target:
                    left += 1
                else:
                    right -= 1

    return result

时间复杂度: O(n³)

变体2:最接近的三数之和 (LeetCode 16)

题目:找出三数之和最接近target的组合

思路:双指针 + 记录最小差值

def threeSumClosest(nums, target):
    nums.sort()
    closest = float('inf')

    for i in range(len(nums) - 2):
        left, right = i + 1, len(nums) - 1

        while left < right:
            current_sum = nums[i] + nums[left] + nums[right]

            if abs(current_sum - target) < abs(closest - target):
                closest = current_sum

            if current_sum == target:
                return target  # 完全匹配
            elif current_sum < target:
                left += 1
            else:
                right -= 1

    return closest

总结

核心要点

  1. 排序的作用

    • 去重基础(相同元素相邻)
    • 双指针前提(利用单调性)
    • 提前终止(最小数>target时退出)
  2. 降维思想

    • 三数之和 → 固定一个数 → 两数之和
    • O(n³) → O(n²)
  3. 去重策略

    • 外层循环:跳过重复的第一个数
    • 内层循环:找到答案后跳过重复的left和right
    • 多重去重确保结果唯一
  4. 双指针原理

    • 利用有序数组的单调性
    • 根据sum与target的关系单向移动指针
    • 时间复杂度从O(n²)降到O(n)

易错点

  • 忘记排序
  • 去重逻辑不完整(i>0判断)
  • 循环边界错误(len(nums)-2)
  • 找到答案后只移动一个指针
  • 提前终止条件用continue而非break

最优解法

排序 + 双指针

  • 时间复杂度: O(n²)
  • 空间复杂度: O(log n) (排序栈空间)

P7加分项

深度理解:

  • 排序的三大作用(去重、双指针、提前终止)
  • 双指针的原理(单调性)
  • 降维思想(三维→二维)

实战扩展:

  • 大数据场景:外部排序 + 分段处理
  • 分布式场景:MapReduce框架
  • 业务场景:推荐系统、用户画像匹配

变体题目: