n sum 问题

306 阅读3分钟

从一个乱序数组中选出 n 个元素,让它们相加之和等于给定的数 sum

n >= 2

双指针解法

{
  /**
   * @description 在 nums 数组内,从下标为 start 的位置开始,选出 n 个数,这些数的和要等于 sum
   * ! 前提:nums 必须是有序数组
   * n 为正整数,且 n >= 2
   * n = 2 时,T=O(nlogn)
   * n > 2 时,T=O(n ^ (n - 1))
   */
  const _nSum = (nums: number[], start: number, n: number, sum: number) => {
    const length: number = nums.length
    if (start >= length) return []

    const result: number[][] = []

    // base case:两数之和
    if (n === 2) {
      let left: number = start
      let right: number = length - 1
      let leftValue: number
      let rightValue: number
      let sum: number
      while (left < right) {
        leftValue = nums[left]
        rightValue = nums[right]
        sum = leftValue + rightValue
        if (sum === sum) {
          result.push([leftValue, rightValue])
          while (left < right && nums[left] === leftValue) left += 1
          while (left < right && nums[right] === rightValue) right -= 1
        } else if (sum < sum) {
          while (left < right && nums[left] === leftValue) left += 1
        } else if (sum > sum) {
          while (left < right && nums[right] === rightValue) right -= 1
        }
      }
    } else {
      // n > 2
      for (let i = start; i < length; ) {
        const current = nums[i]
        const sub: number[][] = _nSum(nums, i + 1, n - 1, sum - current)
        sub.length > 0 && sub.map(a => result.push([current].concat(a)))
        while (i < length && nums[i] === current) i += 1
      }
    }

    return result
  }

  const nSum = (nums: number[], n: number, target: number) => {
    if (n < 2 || n > nums.length) return []

    // 必须传入 (a, b) => a - b
    // nums.sort() 只会让元素先被转为字符串,然后根据字典顺序排序。举例:10 排在 2 之前
    // 所以必须传入 (a, b) => a - b,才能真正让数组元素升序排序
    nums.sort((a, b) => a - b)

    return _nSum(nums, 0, n, target)
  }
  
  // 以下是测试代码

  // 两数之和
  console.log(nSum([2, 7, 11, 15], 2, 9)) // [2, 7]
  console.log(nSum([1, 1, 1, 2, 2, 3, 3], 2, 4)) // [[1, 3], [2, 2]]
  console.log(nSum([1, 3, 1, 2, 2, 3], 2, 4)) // [[1, 3], [2, 2]]
  console.log(nSum([1, 2, 3, 4], 2, 5)) // [ [ 1, 4 ], [ 2, 3 ] ]

  // 三数之和
  console.log(nSum([-1, 0, 1, 2, -1, -4], 3, 0)) // [[-1, -1, 2], [-1, 0, 1]]
  console.log(nSum([], 3, 0)) // []
  console.log(nSum([0], 3, 0)) // []
  console.log(nSum([1, 2, 3, 4, 5, 6, 7, 8, 9], 3, 5)) // []
  console.log(nSum([1, 2, 3, 4, 5, 6, 7, 8, 9], 3, 15))
  // [
  //   [ 1, 5, 9 ],
  //   [ 1, 6, 8 ],
  //   [ 2, 4, 9 ],
  //   [ 2, 5, 8 ],
  //   [ 2, 6, 7 ],
  //   [ 3, 4, 8 ],
  //   [ 3, 5, 7 ],
  //   [ 4, 5, 6 ]
  // ]

  // 四数之和
  console.log(nSum([1, 0, -1, 0, -2, 2], 4, 0)) // [[-2, -1, 1, 2], [-2, 0, 0, 2], [-1, 0, 0, 1]]
  console.log(nSum([], 4, 0)) // []
  console.log(nSum([0], 4, 0)) // []
  console.log(nSum([1, 2, 3, 4, 5, 6, 7, 8, 9], 4, 15))
  // [
  //   [ 1, 2, 3, 9 ],
  //   [ 1, 2, 4, 8 ],
  //   [ 1, 2, 5, 7 ],
  //   [ 1, 3, 4, 7 ],
  //   [ 1, 3, 5, 6 ],
  //   [ 2, 3, 4, 6 ]
  // ]
}

回溯算法

{
  const _nSum = (
    array: number[],
    start: number,
    n: number,
    sum: number,
    _result: number[], // 某个可行解
    result: number[][] // 全部可行解
  ) => {
    if (
      _result.length === n &&
      _result.reduce(
        (accumulator: number, current: number) => accumulator + current,
        0
      ) === sum
    ) {
      return result.push([..._result])
    }

    for (let i = start; i < array.length; i++) {
      _result.push(array[i])
      _nSum(array, i + 1, n, sum, _result, result)
      _result.pop()
    }
  }

  const nSum = (array: number[], n: number, sum: number) => {
    const result: number[][] = []
    _nSum(array, n, sum, 0, [], result)
    return result
  }
  
  // 以下是测试代码

  // 两数之和
  console.log(nSum([2, 7, 11, 15], 2, 9)) // [2, 7]
  console.log(nSum([1, 1, 1, 2, 2, 3, 3], 2, 4))
  // [
  //   [ 1, 3 ], [ 1, 3 ],
  //   [ 1, 3 ], [ 1, 3 ],
  //   [ 1, 3 ], [ 1, 3 ],
  //   [ 2, 2 ]
  // ]
  console.log(nSum([1, 3, 1, 2, 2, 3], 2, 4)) // [ [ 1, 3 ], [ 1, 3 ], [ 3, 1 ], [ 1, 3 ], [ 2, 2 ] ]
  console.log(nSum([1, 2, 3, 4], 2, 5)) // [ [ 1, 4 ], [ 2, 3 ] ]

  // 三数之和
  console.log(nSum([-1, 0, 1, 2, -1, -4], 3, 0)) // [ [ -1, 0, 1 ], [ -1, 2, -1 ], [ 0, 1, -1 ] ]
  console.log(nSum([], 3, 0)) // []
  console.log(nSum([0], 3, 0)) // []
  console.log(nSum([1, 2, 3, 4, 5, 6, 7, 8, 9], 3, 5)) // []
  console.log(nSum([1, 2, 3, 4, 5, 6, 7, 8, 9], 3, 15))
  // [
  //   [ 1, 5, 9 ],
  //   [ 1, 6, 8 ],
  //   [ 2, 4, 9 ],
  //   [ 2, 5, 8 ],
  //   [ 2, 6, 7 ],
  //   [ 3, 4, 8 ],
  //   [ 3, 5, 7 ],
  //   [ 4, 5, 6 ]
  // ]

  // 四数之和
  console.log(nSum([1, 0, -1, 0, -2, 2], 4, 0)) // [[-2, -1, 1, 2], [-2, 0, 0, 2], [-1, 0, 0, 1]]
  console.log(nSum([], 4, 0)) // []
  console.log(nSum([0], 4, 0)) // []
  console.log(nSum([1, 2, 3, 4, 5, 6, 7, 8, 9], 4, 15))
  // [
  //   [ 1, 2, 3, 9 ],
  //   [ 1, 2, 4, 8 ],
  //   [ 1, 2, 5, 7 ],
  //   [ 1, 3, 4, 7 ],
  //   [ 1, 3, 5, 6 ],
  //   [ 2, 3, 4, 6 ]
  // ]
}

总结

解法双指针回溯
是否需要排序
算法复杂度n 等于 2: nlog(n);n 大于 2:n ^ (n - 1)组合数 n 中选 k 个
是否能够过滤仅是排列顺序不同但是元素相同的组合(注 1)不能

注 1:将 [1, 3][3, 1] 视为相同的解,最终结果只保留其中一个

参考链接