【Python刷力扣hot100】15. 3Sum
问题
给定一个整数数组 nums,返回所有满足条件的三元组[nums[i], nums[j], nums[k]],使得i != j 、 i != k 、 j != k,且nums[i] + nums[j] + nums[k] == 0。
注:解集不能包含重复的三元组。
例1:
Input: nums = [-1,0,1,2,-1,-4]
Output: [[-1,-1,2],[-1,0,1]]
Explanation:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0.
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0.
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0.
不同的三元组为 [-1,0,1] 和 [-1,-1,2]。
需注意,输出结果的顺序以及三元组内部元素的顺序均不影响最终结果。
例2:
Input: nums = [0,1,1]
Output: []
Explanation: 唯一可能的三元组,其和不等于 0。
例3:
Input: nums = [0,0,0]
Output: [[0,0,0]]
Explanation: 唯一可能的三元组,其和等于 0。
约束:
3 <= nums.length <= 3000105 <= nums[i] <= 105
解1:暴力
记数组长度为n
写3层for循环,遍历。时间复杂度为O(n3)O(n^3)O(n3),空间复杂度为O(1)O(1)O(1)。
for i in range(n):for j in range(n):for k in range(n):if nums[i]+nums[j]+nums[k]==0):res.append([nums[i],nums[j],nums[k]])
优化遍历次数,变为n(n−1)(n−2)6\frac{n(n-1)(n-2)}{6}6n(n−1)(n−2),但时间复杂度仍为O(n3)O(n^3)O(n3),空间复杂度为O(1)O(1)O(1)。
n = len(nums)
res = []
for i in range(n):for j in range(i + 1, n): # j 从 i+1 开始,保证 j > ifor k in range(j + 1, n): # k 从 j+1 开始,保证 k > jif nums[i] + nums[j] + nums[k] == 0:res.append([nums[i], nums[j], nums[k]])
解2:排序+双指针
先将数组进行排序,之后使用双指针,将后两层for循环的复杂度从O(n2)O(n^2)O(n2)优化到O(n)O(n)O(n)。对后两层循环,原先是遍历2次数组的时间复杂度,现在是遍历一次数组的时间复杂度。
i表示第一层循环下标,对每个i,初始状态下,左指针l指向i+1,右指针r指向n-1
算法流程如下:
- 先对数组进行排序(从小到大)
- 遍历数组,核心改进在后两层遍历
- 若第一个数
nums[i]>0,则说明后面的nums[l]>0 , nums[r]>0,之后的nums[i]+nums[j]+nums[k]都大于0,所以此时就跳出循环。 - 跳过重复元素,避免重复解。
i,l,r在移动时都要跳过重复元素 - 对每层
i,判断nums[i]+nums[l]+nums[r]- 若
nums[i]+nums[l]+nums[r]==0,则产生一个解,加入list,之后移动l和r - 若
nums[i]+nums[l]+nums[r]>0,则说明当前值过大,移动r,从而减小数值 - 若
nums[i]+nums[l]+nums[r]<0,则说明当前值过小,移动l,从而增大数值
- 若
- 若第一个数
对边界情况要加特殊判断,如数组为空或者数组元素数量小于3的时候,显然是无解的。但这里题目中给了
3 <= nums.length <= 3000,所以这里不用特判。
时间复杂度O(n2)O(n^2)O(n2):排序O(nlogn)O(nlogn)O(nlogn),用i遍历数组O(n)O(n)O(n),用l和r遍历数组O(n)O(n)O(n),所以遍历数组为O(n2)O(n^2)O(n2),总时间复杂度为O(nlogn+n2)=O(n2)O(nlogn+n^2)=O(n^2)O(nlogn+n2)=O(n2)
空间复杂度O(1)O(1)O(1):随着问题规模(数组长度n)的增大,所需要的额外空间是不变的,只存储有限数量的变量即可。
class Solution:def threeSum(self, nums: List[int]) -> List[List[int]]:ans = []n = len(nums)nums.sort() # 排序for i in range(n):if nums[i] > 0: # 特判return ansif i > 0 and nums[i] == nums[i - 1]: # 特判continuel = i + 1r = n - 1while l < r: # 双指针if nums[i] + nums[l] + nums[r] == 0:ans.append([nums[i], nums[l], nums[r]])while l < r and nums[l] == nums[l + 1]:l = l + 1while l < r and nums[r] == nums[r - 1]:r = r - 1l = l + 1r = r - 1elif nums[i] + nums[l] + nums[r] > 0:r = r - 1else:l = l + 1return ans
思考
while l < r and nums[l] == nums[l + 1]:l = l + 1
while l < r and nums[r] == nums[r - 1]:r = r - 1
为什么这里的while要加个l<r?不加的话会出什么问题?举个反例
这个问题中,双指针去重时不加 l < r 会导致数组越界,结合题目背景(存在重复元素、三元组和为0的场景),可以举以下典型反例。
反例场景:数组包含连续重复的“0”(如 nums = [0,0,0,0])
- 排序后数组:
[0,0,0,0],n=4。 - 第一次循环(i=0):
nums[i] = 0(不大于0,不触发提前返回),且i=0无前置元素,进入双指针逻辑。- 初始
l=1,r=3,三数和0+0+0=0,满足条件,将[0,0,0]加入结果集。
- 进入第一个去重循环(不加
l < r):- 代码变为
while nums[l] == nums[l + 1],此时l=1:- 第一次判断:
nums[1] == nums[2](0==0),成立,l变为 2。 - 第二次判断:
nums[2] == nums[3](0==0),成立,l变为 3。 - 第三次判断:
nums[3] == nums[4],但数组最大索引为 3(长度为4),nums[4]超出范围,直接抛出IndexError: list index out of range。
- 第一次判断:
- 代码变为
参考
https://leetcode.cn/problems/3sum
