【算法-LeetCode】15. 三数之和(双指针)

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

发布:2021年8月9日20:04:06

问题描述及示例

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/3sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]

示例 2:
输入:nums = []
输出:[]

示例 3:
输入:nums = [0]
输出:[]

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/3sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

提示:
0 <= nums.length <= 3000
-105 <= nums[i] <= 105

我的题解

其实整体思路可以借鉴【【算法-LeetCode】1. 两数之和_赖念安的博客-CSDN博客】中的解法,我一开始也是想用三层for循环暴力求解,但是我回想起之前写过的一个题目就是因为解法太过暴力就导致遇到某个特别长的测试用例时陷入了超时的尴尬。所以这次我怕又有这样的情况,所以就直接根据标签提示中的【双指针】作为思考方向。

为什么不尝试也像上面链接中采用做【两数之和】时借助的Map类型变量呢?看完整个过程后就能明白了。

但是我一开始确实没有想明白这双指针到底该怎么用,在大致浏览了下面的参考链接后才理解其中原理。感谢博主的分享!

参考:【微信公众号:三分钟学前端 2021-07-28】每日算法:三数之和

首先是声明一个数组变量result用于存储最终结果,并在开始遍历nums数组之前对nums进行排序(当然这里升序和降序都可以,只要后续逻辑进行相应改变即可,我选择了升序)。

然后是开始对nums数组遍历。选择当前遍历的元素nums[i]作为题目中a + b + c = 0中的a(也就是第一个数),然后再在剩下的nums[i+1] ~ nums[nums.length - 1]中寻找剩下的bc(这里也说明,只要nums[i]被遍历过了,那么它就不进入下一次的nums[i+1]的寻找bc过程了,因为寻找bc过程是从nums[i+1]开始的嘛)。到了这里基本可以应用上面说到的两数之和的思想来做了,因为完全可以把这里的0 - nums[i]当做两数之和中的target,而把要找的bc当做两数之和中我们想要的结果。

遍历某个数组元素时的第一件事就是去重,也就是保证当前遍历的nums[i]和上一次遍历的nums[i-1]不等值,否则的话就可能导致最后的结果集中有重复的结果。比如输入nums = [-1,0,1,2,-1,-4]时(排序结果为[-4, -1, -1, 0, 1, 2]),输出的结果是[[-1,-1,2],[-1,0,1],[-1,0,1]]。问题就出在那两个重复的-1上。可以借助谷歌的开发者工具进行逐步调试观察。去重的手法就是直接跳过当前遍历的这个重复元素,所以i++continue都可以。

在这里插入图片描述
不过要注意这里的去重不能简单地只写一个if判断,而要用while循环,因为一定要保证所有的后续的重复都已经去掉了,否则遇到下面的测试用例时就会因仍然存在重复现象而不能通过。

在这里插入图片描述

接下来这里我们不是像做【两数之和】中的那样借助Map类型的变量,而是采用双指针的方式。front指针一开始指向nums[i+1]rear指针一开始指向nums数组末尾。在后续的匹配过程中(也就是寻找剩下的bc的过程),这两个指针都是往两个指针的中间移动的。在保证front指针在rear指针前面的前提下(也就是front < rear为真的前提下),具体的移动逻辑为:

  • 如果frontrear两个指针指向的数组元素满足相加之和为target(target = 0 - nums[i]),则将当前遍历的元素nums[i]frontrear两个指针指向的数组元素组成的一个结果加入最终结果集合result。然后同时将front往后移动一位(front++)、rear往前移动一位(rear--),注意这里是两者都要移动,表示两个指针指向的元素已经不能再参与后续的待校验的数组元素(因为以nums[i]开头的结果可能不止一个,所以要找到一个结果后要继续在待校验数组中尝试寻找其他符合条件的bc)。当然,在移动指针时,也要进行关键的去重操作,和上面说到遍历开始时的去重有所不同,这里是保证在寻找以nums[i]开头的结果的过程中不会把等值的重复元素算作一个新的结果。

    我一开始忘了写这两个移动指针的语句,结果出现了下面的报错。应该是内存溢出之类的错误吧。

    在这里插入图片描述
    一开始我没有使用两个while循环进行去重操作,结果就如下方所示,会因出现重复结果而无法通过。同理,这里也上面提到的去重一样,不能简单地写成一个if判断。

    在这里插入图片描述

  • 如果两个指针指向的元素值之和比目标值大(nums[front] + nums[rear] > target),则试着将尾指针向前移动一位(rear--)并进行下一次是否匹配的判断。能够进行这样的操作,全是因为我们在遍历数组之前进行了排序(升序)操作。

    也就是说,由于我们的排序操作,我们可以知道:nums[rear] >= nums[rear-1],又由于当前出现了nums[front] + nums[rear] > target的结果,所以我们保持front不动,并尝试着将rear往前移动一位,判断一下nums[front] + nums[rear-1]===target是否成立,如果还是比target大,则继续这个操作。所以说这种解法中的排序操作是很关键的一步。而由于题目不要求返回数组元素的下标而是直接返回数组元素值,所以就不用像做两数之和时那样考虑元素顺序的问题。

  • 如果两个指针指向的元素值之和比目标值小(nums[front] + nums[rear] < target),则试着将尾指针向前移动一位(front++)并进行下一次是否匹配的判断。这里的判断逻辑和上面的同理。(注意这里的两个判断是互斥的,也就说,两种指针移动情况只会进行其中一种,这和上面的匹配成功后的指针移动情况是不同的)

更新:2021年8月11日11:40:24

感觉用纯语言描述还是有点不形象,所以还是做了个示意图,但是不是全过程,而是遍历到i=1的那个状态。就不做动态图了,太麻烦……

在这里插入图片描述
在上面的【待校验(匹配)元素】中,完全可以套用做【两数之和】时的逻辑,只不过也要考虑去重的问题。

【更新结束】

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function(nums) {
  // result用于存储最终结果
  let result = [];
  // 先将nums排序,这一步很关键,当然升序和降序都可以,只要后续逻辑进行相应改变即可
  nums.sort((a, b) => a - b);
  // 遍历nums数组,注意i的结束条件,因为要留两个位置给front和rear指针,
  // 其实这里按平常的 i < nums.length 来写也可以,但是会多两次无意义的遍历
  for(let i = 0; i < nums.length - 2; i++) {
    // 去重操作,又是关键的一步,避免等值的两个数组元素重复参与,i++ 可用continue代替
    while(nums[i] === nums[i-1]) {
      i++;
    }
    // target 相当于是两数之和题目中的target,从这里开始就可以套用两数之和的题目的逻辑了
    let target = 0 - nums[i];
    // 前部的指针
    let front = i + 1;
    // 尾部的指针,注意:在一次遍历中,front和rear都是朝着两个指针的中间移动
    let rear = nums.length - 1;
    // 若前指针保持在尾指针之前,则说明待校验的数组元素中还有可能有符合条件的元素
    while(front < rear) {
      if(nums[front] + nums[rear] === target) {
        // 如果当前遍历的元素nums[i]和front、rear两个指针指向的数组元素满足要求则将其加入结果集合
        result.push([nums[i], nums[front], nums[rear]]);
        // 下面两个while循环也都是用于去重,注意这个去重和上面那个去重不同
        while(front < rear && nums[front] === nums[front+1]) {
          front++;
        }
        while(front < rear && nums[rear] === nums[rear-1]) {
          rear--;
        }
        // front和rear指针都在该次遍历中被用过一次了,所以要同时将两个指针向中间移动一步
        rear--;
        front++;
      } else if(nums[front] + nums[rear] > target) {
        // 如果两个指针指向的元素值之和比目标值大,则试着将尾指针向前移动一位
        rear--;
      } else {
      	// 否则将前指针向后移动一位,注意这两种情况下的移动不能同时做
        front++;
      }
    }
  }
  return result;
};


提交记录
318 / 318 个通过测试用例
状态:通过
执行用时:124 ms, 在所有 JavaScript 提交中击败了98.50%的用户
内存消耗:48 MB, 在所有 JavaScript 提交中击败了45.81%的用户
时间:2021/08/09 20:00

可以看到,我们上面的解法中并没有像【两数之和】中借助了Map类型的变量,原因就是我们上面的解法中是依赖于排序后的数组的,而本题中恰恰并不在意数组元素和下标是否对应的问题。这是两个题目最大的不同点。其实本题也可以直接把【两数之和】中的做法移植过来,但是还要考虑数组去重的问题,具体的解法可以看上面提到的公众号链接或下方的【有关参考】。

官方题解

更新:2021年7月29日18:43:21

因为我考虑到著作权归属问题,所以【官方题解】部分我不再粘贴具体的代码了,可到下方的链接中查看。

更新:2021年8月9日20:08:05

参考:三数之和 - 三数之和 - 力扣(LeetCode)

【更新结束】

有关参考

更新:2021年8月9日20:06:24
参考:【微信公众号:三分钟学前端 2021-07-28】每日算法:三数之和
参考:浅谈js数组中的length属性 - 小宁同学 - 博客园
更新:2021年8月10日20:25:01
参考:【算法-LeetCode】1. 两数之和_赖念安的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值