LeetCode - 15 三数之和

目录

题目来源

题目描述

示例

提示

题目解析

算法源码


题目来源

15. 三数之和 - 力扣(LeetCode)

题目描述

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

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

注意:答案中不可以包含重复的三元组。

示例

示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
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:

输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。

示例 3:

输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。

提示

  • 3 <= nums.length <= 3000
  • -10^5 <= nums[i] <= 10^5

题目解析

本题需要从nums数组中找出所有和为0的三元组,且找出的三元组不能重复。

本题可以分解为两部分逻辑:

  1. 找到和为0的三元组
  2. 三元组去重

找和为0的三元组,最简单的方法就是三重for暴力枚举,时间复杂度O(n^3),n = nums.length(),结合备注的nums长度,可以得出暴力法会超时。

因此,我们需要一种更优的算法。

当前最优算法思路为:

首先,将nums数组进行升序,

然后,利用三个指针 i, l, r 来选择三个数,如果i, l,r指针指向的三数之和为0,则找到一个符合要求的三元组。

三指针指向的元素值的大小关系:nums[i] <= nums[l] <= nums[r]

上面三指针的运动逻辑其实可以概括,

首先确定三元组的最小值nums[i],然后找到一个与之匹配的nums[l],nums[r]

这样的话,就将三指针运动 转为了 双指针运动。

我们通过下面 示例1 图示来理解运动过程:

nums = [-1,0,1,2,-1,-4] 经过升序后,变为nums = [-4,-1,-1,0,1,2]

如上图,我们分别找了:

  • 三元组中最小值为-4,即 i 固定指向 0
  • 三元组中最小值为-1,即 i 固定指向 1
  • 三元组中最小值为-1,即 i 固定指向 2
  • 三元组中最小值为0,即 i 固定指向 3

的三元组情况。而针对上面情况,初始时l,r指针固定为:

  • l = i + 1
  • r = nums.length - 1

由于nums已经升序,因此,如果i, l, r三元组之和sum

  • sum > 0,则说明三元组之和大了,我们应该减小它,此时
  1. i 指针是固定的,因此不能移动 i 指针
  2. l 指针只能向右移动,因为 l 不能比 i 小,但是 l 指针右移只会增大三元组之和,因此不能移动 l 指针
  3. r 指针只有向左移动,而 r 指针向左移动的话,会导致nums[r]减少,因此三元组之和也会减少,所以我们左移 r 指针,即r--,可以减少三元组之和
  • sum < 0,则说明三元组之和小了,我们应该增大它,此时我们应该右移 l 指针,即 l++,来增大三元组之和
  • sum == 0,则说明当前i, l, r指向的三个数就是一个和为0的三元组

上面就是找和为0的三元组的逻辑。


下面就是去重逻辑的实现了

我们通过示例一可以发现,三指针运动过程中,产生了重复的三元组,如下图所示

那么如何才能去重呢?

最简单的方法,其实就是将所有和为0的三元组求出后,对比去重,但是效率非常低,不推荐。

更高效的去重策略是,发现规律,通过上面图示,我们可以发现:

重复的两个三元组,如果基于后面一个来看的话,那么变化的只有 i 指针,且nums[i] == nums[i-1],而l,r位置未发生改变。

如果我们把nums[i]和nums[i-1]看出一个整体的话,那么这两个重复二元组就是同一个。

且他们对于的l,r指针的运动逻辑也是一致的。因此后续会产生重复的二元组。

因此,我们可得去重结论,如果 num[i] == nums[i-1],则后续的l,r指针运动就可以不做了,因为会产生重复的二元组。


如果上面的 i 指针会产生重复二元组外,对于 l, r 指针同样会产生重复二元组。

比如下面例子:

上面例子中,产生了多个重复的三元组

但是实际上,

如果 nums[l] == nums[l+1],则可以直接 l++ 

如果 nums[r] == nums[r-1],则可以直接 r--

即运动过程如下:

即 nums[l] == nums[l+1] 的话,则 i, l, r 其实和 i, l+1,r 三元组是重复的,因此,我们可以直接跳过l+1的三元组,选择l++

    nums[r] == nums[r-1] 的话,则i ,l , r其实和i, l, r-1 三元组是重复的,因此,我们可以直接跳过r-1的三元组,选择r-- 


另外本题还有一个剪枝优化,

如果 nums[i] > 0的话,由于nums[i]是三元组中最小值,因此nums[i] > 0的话,则nums[l],nums[r]必然也大于0,三元组之和也大于0。

并且nums已经升序,因此nums[i+1]>0也是必然的,即nums[i+1]对应的三元组也是大于0的。

因此如果nums[i] > 0了,则可以直接break掉整个三指针运动。

JS算法源码

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function (nums) {
  const ans = [];

  nums.sort((a, b) => a - b);

  for (let i = 0; i < nums.length - 2; i++) {
    // 剪枝
    if (nums[i] > 0) break;

    // 去重
    if (i > 0 && nums[i] == nums[i - 1]) continue;

    let l = i + 1;
    let r = nums.length - 1;

    while (l < r) {
      const sum = nums[i] + nums[l] + nums[r];

      if (sum > 0) {
        r--;
      } else if (sum < 0) {
        l++;
      } else {
        ans.push([nums[i], nums[l], nums[r]]);

        // 去重
        while (l + 1 < r && nums[l] == nums[l + 1]) l++;
        // 去重
        while (r - 1 > l && nums[r] == nums[r - 1]) r--;
        l++;
        r--;
      }
    }
  }

  return ans;
};

三数之和 - 提交记录 - 力扣(LeetCode)

Java算法源码

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

class Solution {
  public List<List<Integer>> threeSum(int[] nums) {
    List<List<Integer>> ans = new ArrayList<>();

    Arrays.sort(nums);

    for (int i = 0; i < nums.length - 2; i++) {
      // 剪枝优化
      if (nums[i] > 0) break;

      // 去重
      if (i > 0 && nums[i] == nums[i - 1]) continue;

      int l = i + 1;
      int r = nums.length - 1;

      while (l < r) {
        int sum = nums[i] + nums[l] + nums[r];

        if (sum > 0) {
          r--;
        } else if (sum < 0) {
          l++;
        } else {
          ArrayList<Integer> tmp = new ArrayList<>();
          Collections.addAll(tmp, nums[i], nums[l], nums[r]);
          ans.add(tmp);

          // 去重
          while (l + 1 < r && nums[l] == nums[l + 1]) l++;
          // 去重
          while (r - 1 > l && nums[r] == nums[r - 1]) r--;

          l++;
          r--;
        }
      }
    }

    return ans;
  }
}

三数之和 - 提交记录 - 力扣(LeetCode)

Python算法源码

class Solution(object):
    def threeSum(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        ans = []

        nums.sort()

        for i in range(len(nums) - 2):
            # 剪枝
            if nums[i] > 0:
                break

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

            l = i + 1
            r = len(nums) - 1

            while l < r:
                total = nums[i] + nums[l] + nums[r]

                if total > 0:
                    r -= 1
                elif total < 0:
                    l += 1
                else:
                    ans.append([nums[i], nums[l], nums[r]])

                    # 去重
                    while l + 1 < r and nums[l] == nums[l + 1]:
                        l += 1

                    # 去重
                    while r - 1 > l and nums[r] == nums[r - 1]:
                        r -= 1

                    l += 1
                    r -= 1

        return ans

三数之和 - 提交记录 - 力扣(LeetCode)

猜你喜欢

转载自blog.csdn.net/qfc_128220/article/details/131126492