小白学算法 - 二分查找

220 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

先看力扣题目

力扣题链接
给定一个n个元素有序的(升序)整形数组nums和一个目标值target,写一个函数搜索nums中的target,如果目标值存在返回下标,否则返回-1。

示例1:

输入: nums = [-1,0,3,5,9,12], target = 9     
输出: 4       
解释: 9 出现在 nums 中并且下标为 4  

示例2:

输入: nums = [-1,0,3,5,9,12], target = 2     
输出: -1        
解释: 2 不存在 nums 中因此返回 -1 

提示:

  • 你可以假设 nums 中的所有元素是不重复的。
  • n 将在 [1,10000]\color{red}{[1, 10000]}之间。
  • nums 的每个元素都将在 [9999,9999]\color{red}{[-9999, 9999]}之间。

暴力破解法

看到题目,首先想到的是数组的findIndex方法,直接暴力破解,findIndex方法接受一个函数参数,返回在数组中查找的符合条件的第一个元素的索引值

const search = (nums: number[], target: number): number => {
  return nums.findIndex((n: number) => n === target)
}

使用findIndex确实可以通过测试,甚至很多数组方法都可以去实现,但这就脱离了我们的初衷了,暴力破解毕竟不是王道,肯定还有更优的解决方案,这就是我们今天要学的二分查找。

题目中数组为有序数组,同时数组值不重复,这就是使用二分查找法的前提条件,以后看到这样的描述,第一时间都要想想是不是可以用二分查找。

什么是二分查找

二分查找也称为折半查找,顾名思义,用一个中间值把一个数组分成两段(左区间,右区间 ),判断目标值出现在那个区间,循环(递归)二分(折半),直到找到目标元素或不满足条件退出,想想看,这样效率是不是高很多了。

实现二分查找

首先根据上面的思想来定义边界条件,进行 二分(折半) 操作;
1. left: 左边界值(默认等于0,也就是第一个元素的下标)
2. min: 中间值,计算方式为 Math.floor((left + right) / 2)
3. right: 一般有两种定义,等于 nums.length - 1(左闭右闭),等于 nums.length (左闭右开)

其实难点就在于边界值的定义上面

图解 “左闭右闭” 和 “左闭右开” 的区别

二分查找法.png
根据上面的图,我们用代码分别来实现,看看具体有什么不同

// 左闭右闭法 [left, right]
function search1(nums: number[], target: number) {
  let left = 0, right = nums.length - 1;
  // 这里注意:要用 <=
  // 因为在[left, right]区间里面,left === right 是有意义的
  while(left <= right) {
    let min = Math.floor((left + right) / 2)
    if (nums[min] === target) {
      return min
    } else if (nums[min] > target) { // 如果中间值大于目标值,目标值在左区间,移动右边界值
      right = min - 1 // 这里为什么要减一,可以看图
    } else if (nums[min] < target) { // 如果中间值小于目标值,目标值在右区间,移动左边界值
      left = min + 1
    }
  }
  return -1
}

// 左闭右开法 [left, right)
function search2(nums: number[], target: number) {
  let left = 0, right = nums.length;
  // 这里注意:要用 <
  // 因为在[left, right)区间里面,left === right 是没有意义的
  while(left < right) {
    const min = Math.floor((left + right) / 2)
    if (nums[min] === target) {
      return min
    } else if (nums[min] > target) { // 如果中间值大于目标值, 目标值在左区间,移动右边界值
      right = min
    } else if (nums[min] < target) { // 如果中间值小于目标值, 目标值在右区间,移动左边界值
      left = min + 1
    }
  }
  return -1
}

这里能明显的看出来,取得边界条件不一样,相应得判断条件也不一样;这里要注意的点就是,如果是闭区间,那么left === right是有意义的
搞明白了边界值的定义,那么根据边界值去做二分(折半) 操作,根据判断移动边界条件,找出目标值就好了。

递归实现

最后我们用递归来实现一下

// 左闭右闭法 [left, right]
function search1(nums: number[], target: number): number {
  const handle = (nums: number[], target: number, left: number, right: number): number => {
    if (left > right) return -1 
    let min = Math.floor((left + right) / 2)
    if (nums[min] > target) {
      return handle(nums, target, left, min - 1)
    } else if (nums[min] < target) {
      return handle(nums, target, min + 1, right)
    } else {
      return min
    }
  }
  return handle(nums, target, 0, nums.length - 1)
};

// 左闭右开法 [left, right)
function search1(nums: number[], target: number): number {
  const handle = (nums: number[], target: number, left: number, right: number): number => {
    if (left >= right) return -1 
    let min = Math.floor((left + right) / 2)
    if (nums[min] > target) {
      return handle(nums, target, left, min)
    } else if (nums[min] < target) {
      return handle(nums, target, min + 1, right)
    } else {
      return min
    }
  }
  return handle(nums, target, 0, nums.length)
};