[LeetCode训练营]15.三数之和 的分析与优化

吹水

刚开始刷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/

猜你喜欢

转载自blog.csdn.net/qq_45415920/article/details/123025225