15. 三数之和

424 阅读3分钟

给你一个包含 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]]

解法一

自己的暴力解法,即用三个循环遍历出所有的可能的三数相加,但是不出意外的超出了力扣的时间限制。

def threeSum1(nums):
    """暴力求解法,超出时间限制"""
    n = len(nums)
    # 如果数组元素小于3个,直接返回空数组
    if n < 3: return []

    ans = []  # 记录结果
    for i in range(n):
        for j in range(i + 1, n):
            for x in range(n):
                # 当为三个不同数的时候,分别表示出来
                # 然后相加,判断是否为0
                if x != i and x != j \
                        and nums[i] + nums[j] + nums[x] == 0:
                    ans.append([nums[i], nums[j], nums[x]])


    arr = []  # 记录集合
    new_ans = []  # 记录去重后的数组
    # 遍历数组去重
    for i, lt in enumerate(ans):
        s = set()
        s.update(lt)  # 将每个数组转为集合
        if s not in arr:
            # 如果该集合不在数组里面,那么说明是新的三个数
            # 那么将该集合放到数组里面,并且添加数组到返回的new_ans里面
            arr.append(s)
            new_ans.append(lt)
    return new_ans

解法二

还是自己的解法,虽然不是最暴力的,但还是没通过LeetCode的时间限制。想看最优解的,还是直接看解法三。

主要思想:将数组分成正数,负数和零三组数据,然后再分别遍历正数和负数数组,找出所有可能的三数之和。[0, 0, 0] [0, 正, 负] [正, 正, 负] [正, 负, 负]总共无非也就这四种情况。

def threeSum2(nums):
    positive_dict = {}  # 保存正数的字典
    negative_dict = {}  # 保存负数的字典
    zero_dict = {0: 0}  # 保存0的字典,默认0个
    ans = []  # 记录结果
    for i, num in enumerate(nums):
        if num == 0:
            zero_dict[0] += 1
        elif num > 0:
            if num not in positive_dict:
                positive_dict[num] = 1
            else:
                positive_dict[num] += 1
        elif num < 0:
            if num not in negative_dict:
                negative_dict[num] = 1
            else:
                negative_dict[num] += 1

    positive_len = len(positive_dict.keys())
    negative_len = len(negative_dict.keys())

    for i in range(positive_len):
        pt1 = list(positive_dict.keys())[i]
        # 至少有一个0的情况, 就肯定有一对相反数: [1, 0, -1]
        if zero_dict[0] >= 1 and -pt1 in negative_dict:
            ans.append([pt1, -pt1, 0])
        # 没有0的情况,两个正数相同,一个负数: [1, 1, -2]
        if positive_dict[pt1] >= 2 and -pt1*2 in negative_dict:
            ans.append([pt1, pt1, -pt1*2])
        # 没有0的情况,两个正数不同,一个负数: [1, 2, -3]
        for j in range(i+1, positive_len):
            pt2 = list(positive_dict.keys())[j]
            if -(pt1 + pt2) in negative_dict:
                ans.append([pt1, pt2, -(pt1 + pt2)])

    for i in range(negative_len):
        ng1 = list(negative_dict.keys())[i]
        # 没有0的情况,两个负数相同,一个正数: [-1, -1, 2]
        if negative_dict[ng1] >= 2 and -ng1*2 in positive_dict:
            ans.append([ng1, ng1, -ng1*2])
        # 没有0的情况,两个负数不同,一个正数: [-1, -2, 3]
        for j in range(i+1, negative_len):
            ng2 = list(negative_dict.keys())[j]
            if -(ng1 + ng2) in positive_dict:
                ans.append([ng1, ng2, -(ng1 + ng2)])
    # 如果0的个数大于3,还有三个0的情况
    if zero_dict[0] >= 3: ans.append([0, 0, 0])
    return ans

解法三

参考力扣官方和精选答案,采用排序加双指针的方法。

主要思想:在解法一的三层循环上进行优化,在第一层循环保持不变的情况下,将第二层和第三层循环放到一个循环里面。

  • 每次比较nums[i] + nums[L] + nums[R]三数之和和0的大小,i是第一层循环的数的下标(在第二层循环的时候,是固定值),LR分别是指向i之后所有元素的左右指针。
  • 因为是排序后的数组,所有如果三数之和小于0,那么就肯定是nums[L]偏小了,所以L向右移动一位
  • 同理,有如果三数之和大于0,那么就肯定是nums[R]偏大了,所以R向左移动一位
def threeSum3(nums):
    """排序+双指针"""
    n = len(nums)
    res = []  # 记录返回的数组
    if not nums or n < 3: return []
    nums.sort()  # 对数组进行排序
    for i in range(n):
        # 当 nums[i] > 0时直接返回结果:因为 nums[i] >= nums[L] >= nums[R] > 0,
        # 即3个数字都大于0 ,在此固定指针 i 之后不可能再找到结果了
        if nums[i] > 0: return res
        if i > 0 and nums[i] == nums[i - 1]: continue  # 跳过重复元素
        L, R = i + 1, n - 1  # 用左右指针分别指向固定指针 i 之后元素的左右两侧
        while L < R:
            if nums[i] + nums[L] + nums[R] == 0:
                res.append([nums[i], nums[L], nums[R]])  # 当满足三数之和等于0,就记录下来
                while L < R and nums[L] == nums[L + 1]: L += 1  # 去除左边的重复元素
                while L < R and nums[R] == nums[R - 1]: R -= 1  # 去除右边的重复元素
                L += 1  # 满足了一组三数之和等于0之后,左右指针就向中间都移动一位,寻找新的组合
                R -= 1
            elif nums[i] + nums[L] + nums[R] > 0:
                R -= 1  # 如果三数之和大于0,那么就是右边的元素偏大,所以向左移动一位
            else:
                L += 1  # 反之,# 如果三数之和小于0,那么就是左边的元素偏小,所以向右移动一位

    return res
复杂度分析
  • 时间复杂度: O(n2)O(n^2),数组排序O(NlogN)O(NlogN),遍历数组O(n)O(n),双指针遍历O(n)O(n),总体O(NlogN)+O(n)O(n)O(NlogN)+O(n)∗O(n),所以时间复杂度为O(n2)O(n^2)

  • 空间复杂度: O(1)O(1)

力扣精选答案