找出一个有序数组中和为n的两个数

139 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第8天,点击查看活动详情

1、事例

数组[1, 2, 3, 4, 5, 7, 9, 14]找出和为13的两个数 => [4, 9]

2、分析问题

  1. 这是一个数组
  2. 数组中之存在一组可以满足和为13的两个数
  3. 这是一个递增的数组
  4. 输出结果是一个数组

如果只是为了满足找出两数之和,最简单最先冒出我们脑子的想法可能就是双重遍历,找出满足条件的两个数,然后用数组的形式输出,但是这中方式是否会存在复杂度的问题呢?大概猜想起时间复杂度可能是O(n^2),而这种复杂度一般是“不可用”的。那么我们先来写一下用这种方式来进行一下,最后看一下如何优化或者能否找到其他的解题方法。

3、解题一

  1. 循环嵌套代码
//嵌套循环
function findTwoNumbers1(arr: number[], n:number): number[]{
  const res: number[] = [];

  //获取数组长度
  const leng = arr.length;
  //如果数组长度为0,那么直接返回空数组
  if(leng === 0) return res
  //开始嵌套循环
  for(let i = 0; i < leng; i++){
    const startVal = arr[i];
    let hasFind = false;
    for(let j = i + 1; j < leng; j++){//这里j从i+1开始,因为之前的硬计算过了,这样写也可以减少一些循环
      const endVal = arr[j];
      const val = startVal + endVal;
      if(val == n){
        res.push(startVal, endVal)
        hasFind = true
        break //及时跳出循环
      }
    }

    if(hasFind) break; //及时跳出循环
  }
  return res
}

//功能测试
const arrArr = [1, 2, 3, 4, 5, 7, 9, 14];
console.log(findTwoNumbers1(arrArr, 13)) //[4,9]  符合预期
  1. 复杂度分析 前端重时间轻空间,我们直接看时间复杂度,这里有两个for循环进行嵌套,虽然里边的循环是从i+1开始,不是完全的O(n^2),但是数量级依然是O(n^2),然而在数据量较大的情况下,或者更直接的说O(n^2)时间复杂度的算法是不可取的,是不可使用的,所以这种方法虽然能够实现,但是此方法非常不可取。

4、解题二

  1. 分析问题 在上边的解题中,我们考虑到了数组,考虑到了两数之和,但是有序这个条件,我们似乎并没有使用到。记得上一篇文章中提到过:凡有序,必二分,那么这里是不是可以考虑一下使用二分查找呢?如果直接使用二分查找,那么每次循环都会将数组减少一半,而这里是求两个数的和,那么如此情况下,我们必然不能直接使用二分查找。

因为是有序递增(暂时定义为递增)数组,那么是不是可以考虑用数组中第一个数min和最后一个数max相加得到val,查看一下结果呢?如果val大于n,说明min+max需要减小一些,那么就把max的值向左移动一位,如果val小于n,说明min+max需要增加一些,那么就把min向右移动一位。这样做是不是也可以呢?

  1. 按照上边的想法,我们来写一下代码
function findTwoNumbers2(arr: number[], n: number): number[]{
  const res: number[] = [];
  const length = arr.length;
  
  let i = 0;
  let j = length - 1;

  //当i<j的时候就一直循环相加做对比
  while(i < j){
   const min = arr[i];
   const max = arr[j];
   const val = min + max;
    if(val > n){
      //j需要向左移动
      j--
    }else if(val < n){
      //i需要向右移动
      i++
    }else{
      res.push(min)
      res.push(max)
      break;
    }
  }
  return res;
}
//功能测试
console.log(findTwoNumbers2(arrArr, 13)) //[4,9]  符合预期

这种写法,其实利用了二分的思想,是有一种双指针的应用。 这里为什么没有判断length==0呢?因为在while循环中,如果length是0后者1的话,不会进入while循环,会直接返回空数组res:[];这样看来是不是解题一种的length==0的判断也能去掉呢?

  1. 复杂度分析 时间复杂度是O(n),因为这里只有一个循环。不存在时间复杂度为O(n)的js方法,所以综合考虑其时间复杂度是O(n); 空间复杂度也是O(n)。

5、性能对比

const arrTest3 = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20];
const target3 = 39;
console.time("findTwoNumbers1")
for(let i=0; i< 1000 * 10000; i++){
  findTwoNumbers1(arrTest3,target3)
}
console.timeEnd("findTwoNumbers1")//findTwoNumbers1: 1783.185791015625 ms

console.time("findTwoNumbers2")
for(let i=0; i< 1000 * 10000; i++){
  findTwoNumbers2(arrTest3,target3)
}
console.timeEnd("findTwoNumbers2")//findTwoNumbers2: 59.72412109375 ms

通过执行时间的对比,我们可以发现二者根本不是一个数量级的。所以我们可以看到解题二的方法更好,也能证明O(n^2)的时间复杂度不可取;

6、总结

  1. 凡有序,必二分。这里更确切的指代二分的思想,不一定是二分查找
  2. 遇到嵌套循环,可以考虑是否可以改成双指针的方法。