详解二分查找

1,568 阅读1分钟

二分查找适用于 「有序」 的数据集合。

它的思路是:在有序数组nums中寻找目标值target,初始查找范围为整个数组nums。每次取查找范围的中点mid,比较nums[mid]target的大小,如果相等则mid就是要寻找的下标,如果不相等则将查找范围缩小一半。

作为最基础又常考的搜索算法,二分查找有很多细节需要我们注意。

  • while里到底用<=还是<
  • right 的取值是nums.length还是nums.length - 1
  • 到底要给mid加一还是减一
  • 为防止溢出,应该怎么计算mid值。

二分查找的常用场景

有序数组中查找某个值

1. 循环迭代法

const binarySearch = (nums, target) => { // 假定 nums是升序数组 
  let left = 0;
  let right = nums.length; // 注意 
  
  while (left < right) { // 注意 
    let mid = left + Math.floor((right - left) / 2); // 防止溢出 
    if (nums[mid] == target) { 
      return mid; 
    } else if (nums[mid] > target) { 
      right = mid; // 注意 
    } else if (nums[mid] < target) { 
      left = mid + 1; // 注意 
    } 
  } 
  return -1; 
}

需注意的几点:

  1. 对于 while 循环中的条件,个人偏向用<,因为这时对应的搜索区间是 [left, right) 左闭右开! 那么有什么好处呢?

while(left <= right)对应的是两端闭的区间[left, right],当区间为空时 left 和 right 不相等,只有一个是正确答案,返回时容易出错。而while(left < right)对应的搜索区间为[left, right),区间为空时 left 等于 right,「返回left或right均可」

  1. right取值等于nums.length。 因为while循环条件选用的是<,搜索区间如下图。因此要令right = nums.length,搜索区间为[left, right),才能包含全部的数组元素。

image.png

  1. 边界收缩 基于已知信息下,left 和 right 的收缩应该 「最大限度地收缩」 搜索区间,否则会出现死循环。

2. 递归法

const findIndex = (left, right, nums, target) => {
    if (left >= right)  return -1;
    
    // 递归 取代的就是 while循环
    let mid = left + Math.floor((right - left) / 2);
    if (nums[mid] == target) {
        return mid;
    } else if (nums[mid] < target) {
        return findIndex(mid + 1, right, nums, target); // 注意 return
    } else {
        return findIndex(left, mid, nums, target); // 注意 return
    }
}

const binarySearch = (nums, target) => { 
  return findIndex(0, nums.length, nums, target);
}

复杂度分析

  • 时间复杂度:O(logn),n是数组的长度
  • 空间复杂度:O(1)

寻找左侧边界

nums = [1, 4, 9, 11, 11, 12, 15], target = 11,返回下标 3。

const leftBound = function(nums, target) {
  let left = 0;
  let right = nums.length;

  while (left < right) {
    let mid = left + Math.floor((right - left) / 2);
    if (nums[mid] == target) {
      right = mid;     // 注意
    } else if (nums[mid] > target) {
      right = mid;
    } else if (nums[mid] < target) {
      left = mid + 1;
    }
  } 
  return right;  // 注意,left或right均可
}

寻找右侧边界

nums = [1, 4, 9, 11, 11, 12, 15], target = 11,返回下标 4。

const rightBound = function(nums, target) {
  let left = 0;
  let right = nums.length;

  while (left < right) {
    let mid = left + Math.floor((right - left) / 2);
    if (nums[mid] == target) {
      left = mid + 1;    // 注意
    } else if (nums[mid] > target) {
      right = mid;
    } else if (nums[mid] < target) {
      left = mid + 1;
    }
  } 
  if (left == 0) {
    return -1
  }
  return left - 1;  // 注意!由于判断条件 left = mid + 1
}

最后返回的值是left(right)left(right)加减某个数,不放心的话可以实例验证一下。

704. 二分查找 [迭代 / 递归]

153. 寻找旋转排序数组中的最小值

题目 153. 寻找旋转排序数组中的最小值

该题借助图线更加直观,它实际上就是单调递增直线做旋转后的几种情况。假设原数组 [0, 1, 2, 3, 4],那么当它旋转时中间端点值的情况有:

  1. 没有旋转时,左端点值 < 中间端点值 < 右端点值,此时最小值在左边、应该收缩右边界;
  2. 当旋转成 [2, 3, 4, 0, 1] 时,左端点值 < 中间端点值 > 右端点值,此时最小值在右侧、应该收缩左边界;
  3. 当旋转成 [4, 0, 1, 2, 3] 时,左端点值 > 中间端点值 < 右端点值,此时最小值在左侧,应该收缩右边界。

合并几种情况发现:当中值 < 右端点值时,收缩右边界;中值 > 右端点值时,收缩左边界

另外,不需要考虑 中值 == 右端点值 的情况,因为数组中的元素值互不相同。如果相同时,那一定是同一个元素,直接返回即可。

const findMin = (nums) => {
  let left = 0;
  let right = nums.length - 1;  // 区别于二分,nums[right]要存在,所以 right 不能越界
  
  while (left < right) {
    let mid = left + Math.floor((right - left) / 2);
    if (nums[mid] > nums[right]) {  // 收缩左边界
      left = mid + 1;
    } else if (nums[mid] < nums[right]){ // 收缩右边界。
      right = mid; // 注意,和循环条件对应!!!
    }
  }
  return nums[left]; // nums[right]也可以
};

时间复杂度:O(logn)。

空间复杂度:O(1)。

69. x 的平方根:返回值

题目69. x 的平方根

x 平方根的整数部分 ans 是满足 k^2 <= x 的最大 k 值。所以可以对 k 进行二分查找!

跳出来的时候一定是在平方根附近的,最后判断一下如果平方大于 x 的话就返回它前面的一个值,否则就正常返回就行了。

const mySqrt = (x) => {
  if (x < 2) return x;
  let left = 0, right = x;
  
  while (left < right) { 
    let mid = left + Math.floor((right - left) / 2);
    if (mid * mid == x) {
      return mid;
    } else if (mid * mid > x) {
      right = mid;
    } else {
      left = mid + 1;
    }
  } 

  return (left * left > x) ? left - 1 : left; // 注意
}

参考blog

知乎二分查找的左闭右开解释

leetcode二分查找题解