「算法修炼」 Leetcode - 二分法

249 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

二分法

二分查找是一个在有序数组中查找特定元素的搜索算法,尤其是面对大量的数据时,其查找效率极高,时间复杂度是log(n)

什么时候可以用二分法?

  • 前提:元素有序 (或数据具有二段性)
  • 查找某个值
  • 给一个值求插入位置
  • 判断是否存在某个值
  • 利用二分思路求最大最小值

二分法查找的思路

二分法查找的思路如下 :

  1. 首先,从数组的中间元素开始搜索,如果该元素正好是目标元素,则搜索过程结束,否则执行下一步。
  2. 如果目标元素大于/小于中间元素,则在数组大于/小于中间元素的那一半区间查找,然后重复步骤「1」的操作。
  3. 如果某一步数组为空,则表示找不到目标元素。

二分法的思路很简单,无非就是每次根据中值判断目标值是在哪个区间,然后缩减区间到原来的一半,直至找到结果。二分法最容易出错的地方在于边界和细节处理,大体逻辑并不难写出,我们往往死在细节处理上。

例如:

  • while(left < right) 还是 while(left <= right)
  • right = middle 还是 right = middle - 1

在二分查找的过程中,要循环不变量规则,就是在while循环里,寻找中每一次边界的处理都要坚持根据区间的定义来操作。

二分法的边界模板

  1. 左闭右闭的区间写法[left,right]while(left <= right)left的改变为left = mid + 1,right的改变为right = mid - 1;

  2. 左闭右开的区间写法[left,right) : while(left < right)left的改变为left = mid + 1,right的改变为right = mid;

解题套路

leetcode 题目 704.二分查找为例,分别用两种写法解答

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

  1. 左闭右闭区间[left,right]

    • 采用左闭右闭区间写法,右区间端点可以取到,nums[right]是存在的,所以right初始值为nums.length - 1,而不是nums.length
    • 同样的,在闭区间 [left,right]中,left === right是有意义的,要考虑到相等的情况,所以while循环里要用 <= ,即 while(left <= right)
    • nums[middle] > target时,当前这个nums[middle]一定不是target,所以左区间结束位置的下标是 middle - 1,而不是middle, 即 right = middle - 1
// @lc code=start
/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
var search = function(nums, target) {
  // 已知数组升序且不重复,寻找目标值
  // 考虑使用二分法求解

  // 采用写法一,左闭右闭区间
  let left = 0 ,right = nums.length - 1
  // 区间左闭右闭,所以left === right是有意义的 ,所以要用 <= ,即 left <= right
  while(left <= right){
    const middle = parseInt((left + right) / 2)
    // 分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节
    if (nums[middle] === target) { 
      // 找到目标值
      return middle
    } else if(nums[middle] > target) {
      // 当前这个nums[middle]一定不是target,所以左区间结束位置的下标就是 middle - 1, 即 right = middle - 1
      right = middle - 1
    } else if (nums[middle] < target){
      // 
      left = middle + 1
    }
  }
  return -1
};
// @lc code=end
  1. 左闭右开区间[left,right)

    • 采用左闭右开区间写法,右区间端点不能取到,nums[right]不存在,此时right初始值可以为nums.length
    • 同样的,在左闭右开区间 [left,right)中,nums[right]的情况没有意义,也就不用考虑left === right的情况,所以while循环里要用 < ,即 while(left < right)
    • nums[middle] > target时,此时左区间结束位置的下标可以是middle,而不是middle - 1 ,因为区间左闭右开,当right = middle,也不会与nums[middle]比较。
// @lc code=start
/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
 var search = function(nums, target) {
  // 采用写法二,左闭右开区间
  let left = 0 ,right = nums.length
  // 区间左闭右开,所以left === right是没有意义的 ,所以要用 < ,即 left < right
  while(left < right){
    const middle = parseInt((left + right) / 2)
    // 分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节
    if (nums[middle] === target) { 
      // 找到目标值
      return middle
    } else if(nums[middle] > target) {
      // 循环不变量规则 - 区间是左闭右开
      // 此时左区间结束位置的下标就算是 middle,因为区间左闭右开,也不会与nums[middle]比较
      right = middle 
    } else if (nums[middle] < target){
      // 
      left = middle + 1
    }
  }
  return -1
};
// @lc code=end
  1. 总结
两种写法的对比左闭右闭[left,right]左闭右开[left,right)
right初始值nums.length - 1nums.length
while循环不变量while(left ≤ right)while(left < right)
nums[middle] > targetright = middle - 1right = middle

相关题目