一天一大 leet(三数之和)难度:中等 DAY-12

20200612

一天一大 leet(三数之和)难度:中等 DAY-12_第1张图片

题目(难度:中等):

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

注意

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

示例

给定数组 nums = [-1, 0, 1, 2, -1, -4],

满足要求的三元组集合为:
[
  [-1, 0, 1],
  [-1, -1, 2]
]

抛砖引玉

一天一大 leet(三数之和)难度:中等 DAY-12_第2张图片

  • 先排序

  • 特殊情况排除

    1. 数组的长度小于 3
    2. 数组的最后一项小于 0(排序之后)
  • 第一次循环得到的结果作为第一个数,当第一个数

  • 当第一个数大于 0,则说明之后不会有与他组合满足条件的数了

  • 第二个数从第一个之后依次向后查找

  • 第三个数从最后一个数依次向前查找

  • 当第二个数的指针大于最后一个数的指针时终止循环

  • 当数组总存在重复的数组时,按照上面的逻辑会有重复的答案出现

  • 初始化一个 map 去满足条件的任何两个数组合作为 map 的 key 来
    当判断筛选出来的两个数组合的 key 在 map 中出现过则说明该组合已经放到结果数组中了

a+b => b+a a+b
/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function (nums) {
  let end = nums.length,
    _result = []
  nums.sort((a, b) => a - b)
  if (nums.length < 3 || nums[end - 1] < 0) return []
  let secondMap = new Map()
  nums.some((first, i) => {
    if (first > 0) return true
    let second = i + 1
    let last = end - 1
    while (second < last) {
      if (first + nums[second] > 0) break
      let sum = first + nums[second] + nums[last]
      if (
        sum === 0 &&
        !(
          secondMap.has(nums[second] + '-' + nums[last]) ||
          secondMap.has(nums[last] + '-' + nums[second])
        )
      ) {
        _result.push([first, nums[second], nums[last]])
        secondMap.set(nums[second] + '-' + nums[last], second)
      }
      if (sum <= 0) {
        second++
      } else {
        last--
      }
    }
  })
  return _result
}

官方答案

排序 + 双指针

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function (nums) {
  let sortArr = [],
    _result = [],
    n = nums.length
  nums.sort((a, b) => a - b)

  // 枚举 a
  for (let first = 0; first < n; ++first) {
    // 需要和上一次枚举的数不相同
    if (first > 0 && nums[first] == nums[first - 1]) {
      continue
    }
    // c 对应的指针初始指向数组的最右端
    let third = n - 1
    let target = -nums[first]
    // 枚举 b
    for (let second = first + 1; second < n; ++second) {
      // 需要和上一次枚举的数不相同
      if (second > first + 1 && nums[second] == nums[second - 1]) {
        continue
      }
      // 需要保证 b 的指针在 c 的指针的左侧
      while (second < third && nums[second] + nums[third] > target) {
        --third
      }
      // 如果指针重合,随着 b 后续的增加
      // 就不会有满足 a+b+c=0 并且 b
      if (second == third) {
        break
      }
      if (nums[second] + nums[third] == target) {
        _result.push([nums[first], nums[second], nums[third]])
      }
    }
  }
  return _result
}

高手在民间

  • 数组从小到大排序
  • 固定值从最小到最大循环
  • start 和 end 在固定值右侧移动寻找 start + end = -(固定值)
  • 如果三数和小于 0,说明 start 对应值太小,应该右移,反之 end 左移
  • 如果三数和等于零就记录下来, L 右移,注意如果 L 的后一个和当前值相等就需要跳过,参考[0,0,0,0]
  • L 不能超过 R
  • 此时 固定值 i 右移,注意后一个 i 和当前 i 应该不相等,相等需要跳过,参考[-4,-1,-1,0,1,2]的-1
const res = []
let numsLen = nums.length
if (numsLen < 3) return []

nums.sort((a, b) => a - b)
let lastNum = null
for (let i = 0; i < numsLen; i++) {
  if (nums[i] > 0) break
  if (nums[i] === lastNum) continue

  let L = i + 1
  let R = numsLen - 1
  while (L < R) {
    const sum = nums[i] + nums[L] + nums[R]
    if (sum === 0) {
      res.push([nums[i], nums[L], nums[R]])
      while (nums[L] === nums[L + 1]) L++
      while (nums[R] === nums[R - 1]) R--
      L++
      R--
    } else if (sum > 0) {
      R--
    } else {
      L++
    }
  }
  lastNum = nums[i]
}
return res
  • 外层循环主指针 i 遍历数组。内层循环,双指针去寻找满足三数和等于 0 的项
  • 因为不能有重复的解,为了简化操作,我们先对数组预排序,则我们判断一个元素是否重复,只需看它和它之前位置的元素是否相等即可

双指针的移动时,避免出现重复解

  • 得到一个解后,需要左右指针向“内”移动,为了避免指向重复的元素

  • 左指针要在 left < right 的前提下,一直向右移动到不重复的元素上

  • 右指针要在 left < right 的前提下,一直向左移动到不重复的元素上

  • 经过了排序,如果外层遍历的数已经大于 0,则另外两个数一定大于 0,sum 不会等于 0,所以这种情况直接 break 结束遍历

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function (nums) {
  nums.sort((a, b) => a - b)
  const res = []
  for (let i = 0; i < nums.length - 2; i++) {
    let n1 = nums[i]
    if (n1 > 0) break
    if (n1 == nums[i - 1] && i > 0) continue // 遍历到重复的数,跳过
    let left = i + 1
    let right = nums.length - 1
    while (left < right) {
      let n2 = nums[left]
      let n3 = nums[right]
      if (n1 + n2 + n3 === 0) {
        res.push([n1, n2, n3])
        while (left < right && nums[left] === n2) left++ //直到指向不一样的数
        while (left < right && nums[right] === n3) right--
      } else if (n1 + n2 + n3 < 0) {
        left++
      } else {
        right--
      }
    }
  }
  return res
}

菜鸡的自白

看了别人的方法就会发现,循环的逻辑基本差不多的,
首先找到一个基准
再对有序的数组,有节制的循环
不同的地方(也是我可以优化的地方)主要是对有序查询第二个和第三个数据重复的问题
声明 map 记录重复不仅增加了内存的占用,也在 map 存储和查询是增加了时间成本

优化:
第二个数后面一个数增加后与上一个相同则默认重复
最后一个数也类似

博客: 小书童博客(http://gaowenju.com/)

公号: 坑人的小书童
一天一大 leet(三数之和)难度:中等 DAY-12_第3张图片

你可能感兴趣的:(一天一大leet)