三数之和

题目介绍

给你一个包含 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]
输出:[]

方法:双指针法

  • 暴力法搜索时间复杂度为O(N^3),要进行优化,可通过双指针动态消去无效解来提高效率。
  • 双指针的思路,又分为左右指针和快慢指针两种。我们这里用的是左右指针。左右指针,其实借鉴的就是分治的思想,简单来说,就是在数组头尾各放置一个指针,先让头部的指针(左指针)右移,移不动的时候,再让尾部的指针(右指针)左移:最终两个指针相遇,那么搜索就结束了。

(1)双指针法铺垫: 先将给定 nums 排序,复杂度为 O(NlogN)。
首先,我们可以想到,数字求和,其实跟每个数的大小是有关系的,如果能先将数组排序,那后面肯定会容易很多。
之前我们搜索数组,时间复杂度至少都为O(N^2),而如果用快排或者归并,排序的复杂度,是 O(NlogN),最多也是O(N^2)。所以增加一步排序,不会导致整体时间复杂度上升。
在这里插入图片描述
(2)初始状态,定义左右指针L和R,并以指针i遍历数组元素。
在这里插入图片描述

  • 固定 3 个指针中最左(最小)数字的指针 i,双指针 L,R 分设在数组索引 (i, len(nums)) 两端,所以初始值,i=0;L=i+1;R=nums.length-1
  • 通过L、R双指针交替向中间移动,记录对于每个固定指针 i 的所有满足 nums[i] + nums[L] + nums[R] == 0 的
    L,R 组合。

两个基本原则:

  • 当 nums[i] > 0 时直接break跳出:因为 nums[R] >= nums[L] >= nums[i] > 0,即 3个数字都大于 0 ,在此固定指针 i 之后不可能再找到结果了。
  • 当 i > 0且nums[i] == nums[i - 1]时,即遇到重复元素时,跳过此元素nums[i]:因为已经将 nums[i -1] 的所有组合加入到结果中,本次双指针搜索只会得到重复组合。

(3)固定i,判断sum,然后移动左右指针L和R。
L,R 分设在数组索引 (i, len(nums)) 两端,当L < R时循环计算当前三数之和:sum = nums[i] + nums[L] + nums[R]
并按照以下规则执行双指针移动:

  • 当sum < 0时,L ++并跳过所有重复的nums[L];
    在这里插入图片描述

在这里插入图片描述

  • 由于sum<0,L一直右移,直到跟R重合。如果依然没有结果,那么i++,换下一个数考虑。

换下一个数,i++,继续移动双指针:
在这里插入图片描述
初始同样还是L=i+1,R=nums.length-1。同样,继续判断sum。

  • 找到一组解之后,继续移动L和R,判断sum,如果小于0就右移L,如果大于0就左移R:
    在这里插入图片描述
    找到一组解[-1,-1,2],保存,并继续右移L。判断sum,如果这时sum=-1+0+2>0,(R还没变,还是5),那么就让L停下,开始左移R。
    在这里插入图片描述
    如果又找到sum=0的一组解,把当前的[-1,0,1]也保存到结果数组。继续右移L。
  • 如果L和R相遇或者L>R,代表当前i已经排查完毕,i++;如果i指向的数跟i-1一样,那么直接继续i++,考察下一个数;
    在这里插入图片描述
  • 这时i=3,类似地,当sum > 0时,R左移R -= 1,并跳过所有重复的nums[R];
    在这里插入图片描述
  • L和R相遇,结束当前查找,i++。
    在这里插入图片描述
    当 nums[i] > 0 时直接break跳出:过程结束。所以,最终的结果,就是[-1,-1,2],[-1,0,1]。
    代码演示如下:
// 方法三:双指针法
 public List<List<Integer>> threeSum(int[] nums){
      int n = nums.length;

      List<List<Integer>> result = new ArrayList<>();

      // 0. 先对数组排序
      Arrays.sort(nums);

      // 1. 遍历每一个元素,作为当前三元组中最小的那个(最矮个做核心)
      for (int i = 0; i < n; i++){
          // 1.1 如果当前数已经大于0,直接退出循环
          if (nums[i] > 0 ) break;

          // 1.2 如果当前数据已经出现过,直接跳过,因为不可能中间隔了几个数据之后,还有数据相等,因为已经排好序的
          if ( i > 0 && nums[i] == nums[i-1] ) continue;

          // 1.3 常规情况,以当前数做最小数,定义左右指针
          int lp = i + 1;
          int rp = n - 1;
          // 只要左右指针不重叠,就继续移动指针
          while (lp < rp){
              int sum = nums[i] + nums[lp] + nums[rp];
              // 判断sum,与0做大小对比
              if (sum == 0){
                  // 1.3.1 等于0,就是找到了一组解
                  result.add(Arrays.asList(nums[i], nums[lp], nums[rp]));
                  lp ++;
                  rp --;
                  // 如果移动之后的元素相同,直接跳过,因为两个数一样的话,三个数已经确定了两个了,会重复
                  while (lp < rp && nums[lp] == nums[lp - 1]) lp++;
                  while (lp < rp && nums[rp] == nums[rp + 1]) rp--;
              }
              // 1.3.2 小于0,较小的数增大,左指针右移
              else if(sum < 0)
                  lp ++;
              // 1.3.3 大于0,较大的数减小,右指针左移
              else
                  rp --;
          }
      }
      return result;
  }

复杂度分析:

  • 时间复杂度 O(N^2):其中固定指针k循环复杂度 O(N),双指针 i,j 复杂度
    O(N)。比暴力法的O(n^3),显然有了很大的改善。
  • 空间复杂度 O(1):指针使用常数大小的额外空间。
  • 尽管时间复杂度依然为O(n^2),但是过程中避免了复杂的数据结构,空间复杂度仅为常数级O(1),可以说,双指针法是一种很巧妙、很优雅的算法设计。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值