二分查找

162 阅读5分钟

二分查找

推荐刷题

在做过上面的题后,我发现了二分查找的一个可以公用的做法

target目标值

left左边界

right右边界

leetcode704.二分查找找到target目标值的下标,没有则返回-1

leetcode35.搜索插入位置找到target目标值的下标,没有则返回它应该在数组中的位置

leetcode34. 在排序数组中查找元素的第一个和最后一个位置找到target目标值的首次和最后一次出现的下标值,没有则返回-1,-1

leetcode69. sqrt(x)返回一个数的算数平方根,向下取舍

leetcode367. 有效的完全平方数判断一个数是否是有效的完全平方数

可以看到所有的问题都存在着target不存在的情况

我们可以把所有的问题从寻找target变成寻找寻找第一个大于或者小于target的下标值

而且例如34题,是寻找target的首次出现、最后一次出现的下标值,这就等同于我们变化后的正常解法。

我们最后这种的做法最后得出的是target应该在数组中存在的位置
再根据题目要求返回相应的东西即可

leetcode704.二分查找

leetcode704.二分查找

常规做法

二分查找的关键点有几个:一个是中间值的求导,一个是循环判断,以及左右边界值的赋值

二分查找的步骤:

  1. 找出区间、遍历循环(反复循环数组的一半长度)
  2. 求出中间值
  3. 中间值与目标值判断、左右边界重新赋值

中间值的求导

二分查找需要用到left = 0right = list.length - 1,两个边界值
// 位运算
let mid = list.length >> 1
用这个方法时要注意,在二分查找中,找中间值指的是所选区间的中间索引值,上诉方法只能求数组的中间值,如果要用次方法求所选区间的中间值,用下列的表达式
let mid = ((right - left) >> 1) + left

// left + (right-left) / 2 这种写法可以避免发生越界问题
//向下取值
// Math.floor()
let mid = Math.floor(left + (right - left) / 2)


循环判断

声明时right的赋值

while()循环判断

循环内部right的赋值

左闭右和
let l = 0, r = nums.length - 1
  while(l <= r){
    let mid = Math.floor(l + (r - l) / 2)
    if(nums[mid] === target){
      return mid - 1
    }else if(nums[mid] > target){
      r = mid
    }else{
      l = mid + 1
    }
  }
  return -1
 // whlie()里使用等号,
左闭右开
let l = 0, r = nums.length
  while(l < r){
    let mid = Math.floor(l + (r - l) / 2)
    if(nums[mid] === target){
      return mid
    }else if(nums[mid] > target){
      r = mid
    }else{
      l = mid + 1
    }
  }
  return -1
 // whlie()里使用等号,

变化后的解法

  let l = 0;
  let r = nums.length - 1
  while(l <= r){
    mid = Math.floor(l + (r-l)/2)
    if(nums[mid] >=target){
      r = mid - 1
    }else {
      l = mid + 1;
    }
  }
  let mm = l > r ? l : r
  return nums[mm] === target ? mm : -1
分析一下这样的做法:

当我们内部if判断条件变成nums[mid] >=target后,这样出现的情况就是我们循环的最后一定是剩下两个数
假如存在target,倒数第二次循环剩余的两个值剩余的两个值是[小于target,target],最后一次是[target,target]
假如不存在target,剩余的两个值是[大于target,大于target]

并且注意到我们求mid值的一个细节,Math.floor()是向下取舍,所以最后一次循环只有两个数的时候,假设是l,r
right = r ,left = l, mid = l,mid值一定是等于此时的left

最后的结果:
假如target存在,倒数第二次循环剩余的两个值[小于target,target],此时的nums[mid]是小于target
那么l = mid + 1,仍满足l<=r循环条件,剩余的两个值是[target,target],下一次判断就是r = mid - 1
此时left就是target的位置,而right就是第一个小于target最后出现的位置。

分析:
假如target不存在,剩余的两个值[大于target,大于target],此时的nums[mid]是大于target
那么r = mid - 1,不满足循环条件,退出循环,此时left就是target应该存在位置
因为没有target,现在就是第一个大于target的数值的位置
而right就是第一个小于target最后出现的位置。
这样来看,又等同于target存在的情况了

结论:
所以不管target存在与否,循环结束后,right的位置就是第一个小于target最后出现的位置
left就是target应该存在的位置,假如没有target,则代表是第一个大于target的数值的位置

leetcode35.搜索插入位置

leetcode35.搜索插入位置

常规做法

假如数组中没有存在target,我们循环的最后一定是剩下两个数

并且注意到我们求mid值的一个细节,Math.floor()是向下取舍,所以最后一次循环,只有两个数的时候,mid值一定是等于此时的left

list[mid]如果比target小,那么right = mid - 1, 如果比target大,那么left = mid + 1,我们只需要在最后return的时候比较left与right的大小就知道target应该插入的位置。

  let l = 0, r = nums.length - 1
  let mid = 0
  while(l <= r){
    mid = Math.floor(l + (r-l)/2)
    if(nums[mid]===target){
      return mid
    }else if(nums[mid] > target){
      r = mid - 1;
    }else{
      l = mid + 1
    }
  }
  return l > r ? l : r

变化后的做法

  let l = 0, r = nums.length - 1
  let mid = 0
  while(l <= r){
    mid = Math.floor(l + (r-l)/2)
    if(nums[mid] >=target){
      r = mid - 1
    }else {
      l = mid + 1;
    }
  }
  return l > r ? l : r

leetcode34. 在排序数组中查找元素的第一个和最后一个位置

leetcode34. 在排序数组中查找元素的第一个和最后一个位置

常规做法

这个问题实现数组的原生api(indexOF、lastIndexOF)一样

当数组中不止一个target的时候,如何找到第一个和最后一个值

经历过leetcode35.搜索插入位置,我们应该知道,当数组中不存在当前值时,**循环到最后一定会剩下两个数值**

现在我们改变一下比较的条件,如果当前值**大于等于**target时,right = mid - 1,并设置返回值为 mid 
设置返回值的原因是,当我们最后只剩下两个数时,mid 一定等于 left此时的值,而这个值不管是大于小于等于target
其实可以将问题拆开分析

1. 求元素的最后一个位置,不就等于求大于此元素的第一个位置再减去一

2. 而求元素的第一个位置其实和上面的右边界一样求法
const findBr = (nums,target,judge) => {
    let l = 0 , r = nums.length -1,ans = nums.length
    while(l <= r){
      let mid = Math.floor(l + (r - l) / 2)
      if(nums[mid] > target || (judge && nums[mid] >= target)){
        r = mid - 1;
      }else{
        l = mid + 1
      }
    }
    return  l > r ? l : r
}
let res = [-1,-1]
let leftBr = findBr(nums,target,true)
let rightBr = findBr(nums,target,false) - 1
if(leftBr <= rightBr && rightBr < nums.length && nums[leftBr] === target && nums[rightBr] === target)
    {
    res = [leftBr,rightBr]
    }
return res

变化后的做法

    let l = 0 , r = nums.length -1,ans = nums.length
    while(l <= r){
      let mid = Math.floor(l + (r - l) / 2)
      if(nums[mid] >= target){
        r = mid - 1;
      }else{
        l = mid + 1
      }
    }
    return  l > r ? l : r
  }

  const findRight = (nums,targe) => {
    let l = 0 , r = nums.length -1,ans = nums.length
    while(l <= r){
      let mid = Math.floor(l + (r - l) / 2)
      if(nums[mid] > target){
        r = mid - 1;
      }else{
        l = mid + 1
      }
    }
    return  l > r ? l : r
  }

  let leftBr = findLeft(nums,target) 
  let rightBr = findRight(nums,target) - 1
  return nums[leftBr] === target ? [leftBr,rightBr] : [-1,-1]

由于这种做法找到target的应存在(实存在)的位置,和第一个小于target的值的最后一次出现的位置,所以要求target最后一次出现的位置时,需要更换一下循环条件,(nums[mid] > target)

leetcode69. sqrt(x)

常规做法

leetcode69. sqrt(x)

  let l = 0;
  let r = x;
  while(l <= r){
    let mid = Math.floor(l + (r-l) / 2)
    if(mid * mid > x){
      r = mid - 1;
    }else{
      l = mid + 1
    }
  }
  return r 

变化后做法

  let r = x;
   while(l <= r){
    mid = Math.floor(l + (r-l)/2)
    if(mid * mid >=x){
      r = mid - 1
    }else {
      l = mid + 1;
    }
  }
  return l * l > x ? l - 1: l

在上面结论说到了 left就是target应该存在的位置,假如没有target,则代表是第一个大于target的数值的位置,素以最后做一个比较,然后取值

leetcode367. 有效的完全平方数

leetcode367. 有效的完全平方数

常规做法

  let l = 0;
  let r = num
  while(l <= r){
    let mid = l + ((r - l) >> 1)
    if(mid * mid === num){
      return true
    }else if(mid * mid < num){
      l = mid + 1
    }else{
      r = mid - 1
    }
  }
  return false

变化后的做法

  let l = 0;
  let r = num;
   while(l <= r){
    mid = Math.floor(l + (r-l)/2)
    if(mid * mid >=num){
      r = mid - 1
    }else {
      l = mid + 1;
    }
  }
  return l * l > num ? false: true