吹水
刚开始刷LeetCode,第一节课学的数组,这是当时写的笔记[LeetCode训练营]数组
课后作业有一道中等题:15.三数之和。
正好要开技术分析会,得益于LeetCode强大的保存功能,可以找到之前提交过的源码,所以我能很方便地将我解这道题的思路分享出来。
分析题目
题目
给你一个包含 n
个整数的数组nums
,判断nums
中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
示例 2:
输入:nums = []
输出:[]
示例 3:
输入:nums = [0]
输出:[]
提示:
0 <= nums.length <= 3000
-105 <= nums[i] <= 105
读完题目,发现题目要我们找出数组中三个数加起来为0的三元组,还不能重复。
我第一反应就是直接用三层for循环来穷举符合条件的三个数,但是很显然,这个方法效率特别低,时间复杂度是O(n3),而且会出现很多相同的三元组,如果数据多的话删除这些元素也很困难。
于是我就想到了课上讲的双指针模型。很快啊,我就又想到一种解题思路了。
第一种方法
我们先排序然后再来找规律。
然后用for循环遍历数组nums,获得左边的数,然后再用对撞指针判断获得中间和右边的数。这样,由于遍历了一遍for循环,再用双指针,时间复杂度为O(n2),效率高上不少。
很快啊(不到15分钟),我就写出了第一版的代码。
PS:对撞指针是指在有序数组中,将指向最左侧的索引定义为左指针(left),最右侧的定义为右指针(right),然后从两头向中间进行数组遍历。
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
target = list()
length = len(nums)
for i in range(length - 2): #因为是三数之和,所以左边的数读取到倒数第三就可以了
start = i + 1 #左指针
end = length - 1 #右指针
while (start < end):
sum = nums[i] + nums[start] + nums[end]
if (sum > 0):
end -= 1
elif (sum < 0):
start += 1
else:
target.append([nums[i],nums[start],nums[end]])
start += 1 #这里start+1或者end-1都是可以的,不写会死循环
#由于某种奇怪的原因(反正程序能跑起来就不管了)会出现重复的元素,好在元素较少
finall = list()
for i in target:
if i not in finall:
finall.append(i)
return finall
第一版跑起来了,不过最后的程序运行时间和内存消耗却惊到我了,所以我决定参考下答案是怎么做的。
第二种方法(答案)
以下是答案的解释:
可以发现,如果我们固定了前两重循环枚举到的元素 a 和 b,那么只有唯一的 c 满足 a+b+c=0。当第二重循环往后枚举一个元素 b’ 时,由于 b’ > b,那么满足 a+b’+c’=0 的 c’
一定有 c’ < c,即 c’ 在数组中一定出现在 c 的左侧。也就是说,我们可以从小到大枚举 b,同时从大到小枚举 c,即第二重循环和第三重循环实际上是并列的关系。
有了这样的发现,我们就可以保持第二重循环不变,而将第三重循环变成一个从数组最右端开始向左移动的指针,从而得到下面的伪代码:
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
target = list()
length = len(nums)
for i in range(length-2):
if i > 0 and nums[i] == nums[i - 1]:
continue
oppsite = -nums[i]
end = length - 1
for j in range(i+1, length-1):
if j > i + 1 and nums[j] == nums[j - 1]:
continue
while (j < end and nums[j] + nums[end] > oppsite):
end -= 1
if (j == end):
break
if (nums[j] + nums[end] == oppsite):
target.append([nums[i],nums[j],nums[end]])
return target
和我的方法大同小异,所以时间复杂度也为O(n2)。
第三种方法
但是为什么时间复杂度和空间复杂度相差了将近10倍?
我开始对比源码,发现他进入循环的时候有两个个判断是否和上一个元素相同的操作,这个操作可以极大的降低程序运行时间。经过测试,如果只在for循环下面加关于 i 的判断,程序只需要2700ms左右;如果加上两处判断,程序能做到600ms内完成。
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
target = list()
length = len(nums)
for i in range(length - 2):
if (i > 0 and nums[i] == nums[i - 1]): #第一处判断,剔除与上一次相同的元素
continue
start = i + 1
end = length - 1
while (start < end):
sum = nums[i] + nums[start] + nums[end]
if (sum > 0):
end -= 1
elif (sum < 0):
start += 1
else: #找到一个符合条件的三元组
target.append([nums[i],nums[start],nums[end]])
# 第二处判断,去除与上次相同的元素
while (start+1 < end and nums[start] == nums[start+1]): #左指针
start += 1
while (end-1 > start and nums[end] == nums[end-1]): #右指针
end -= 1
start += 1 #这里start+1或者end-1都是可以的,不写会死循环
return target
意外发现改完之后执行时间比答案还快了几十ms,而且空间复杂度没有变化。
参考自:
https://leetcode-cn.com/problems/3sum/solution/san-shu-zhi-he-by-leetcode-solution/