二分查找做题总结

229 阅读3分钟

前言

二分查找也叫折半查找,是一种效率较高的查找办法。我们在做二分查找题目的时候通常会搞混判断条件,不知道如何写判断条件,下面的内容会对二分查找的判断条件进行分析

二分查找思想

要讨论二分查找首先我们要明白其算法思想

  • 二分查找的前提是在一个有序数组中
  • 所谓二分查找就是通过缩小区间将我们要找的目标值缩小在某一区间范围内

介绍完二分查找的思想,下面我给大家介绍一下二分查找的三种写法

二分查找的三种写法

  1. 利用二分法查找精确值

    我们可以通过二分查找来搜素数组中是否存在目标值target

    例:
    
    给定一个有序数组nums(数组内元素不重复)和一个目标值target,查找数组中值为target的元素下标,若不存在则返回-1
    
    输入: nums = [-1,0,3,5,9,10,12], target = 9
    输出: 4
    
  • 代码

    function binarySearch(nums, target) {
      let left = 0, right = nums.length - 1, mid
      while(left <= right) {
        mid = left + Math.floor((right - left) / 2)
        // 中间值小于目标值,收缩左区间
        if(nums[mid] < target) left = mid + 1
        // 中间值等于目标值,返回下标
        else if(nums[mid] === target) return mid
        //中间值大于目标值,收缩右区间
        else right = mid - 1
      }
      return -1
    }
    
  1. 利用二分法查找模糊值

    例:
    找到有序数组中target第一次出现的位置
    输入: nums = [1,2,2,5,5,6,7], target = 5
    输出: 2
    

    例子中5出现了两次,下标分别为3和4 这时候我们将方法一的思想的套用在本题上我们会发现返回的是2,而我们发现数组中第一次出现5的位置是3

  • 代码

    function binarySearch(nums, target) {
      let left = 0, right = nums.length - 1, mid
      while(left <= right) {
        mid = left + Math.floor((right - left) / 2)
        // 中间值小于目标值,收缩左区间
        if(nums[mid] < target) left = mid + 1
        //中间值大于目标值,收缩右区间
        else right = mid - 1
      }
      return mid // 2
    }
    

    为什么会出现这种情况呢?我们发现在第一次循环中nums[mid] >= target, right = mid - 1这一步,我们就已经将答案排除在区间之外了

    所以我们在做二分查找的题目时要注意我们每次缩小范围的时候不能将潜在答案排除在外

    下面我们加上这个限制条件对代码进行修改

  • 代码(修改后)

    var binarySearch = function (arr, target) {
    let left = 0, right = arr.length - 1, mid
    while (left < right) {
      mid = left + Math.floor((right - left) / 2)
      //  nums[mid]大于等于target,收缩右区间
      if (arr[mid] >= target) {
        // 此时arr[mid] >= target可能是第一次出现的target的位置,我们缩小范围时不能排除潜在答案,故令right = mid而不是mid - 1
        right = mid
      }
      // 反之收缩左区间
      else {
        // 此时arr[mid] < target所以此时下标mid必定不是target第一次出现的位置,故我们缩小范围时可以令left = mid + 1
        left = mid + 1
      }
    }
      // 此时right就是target第一次出现的位置
      return right
    }
    

    所以当我们不知道什么情况下该用right = mid - 1 或 right = mid时,我们要考虑的是我们将边界缩小为这种状态会不会将答案排除在外,若不会我们就可以令right = mid - 1,反之right = mid

  1. 万能法

    使用万能法可以有效避免我们不知道什么时候用left < right || left <= right; right = mid - 1 || right = mid的问题

  • 代码

    // 万能法
    function binarySearch(nums, target) {
      let left = -1, right = nums.length, mid
      while(left + 1 < right) {
        mid = left + Math.floor((right - left) / 2)
        if(nums[mid] < target) left = mid
        else right = mid
      }
      // 返回值按题目要求自行选择
      return 
    }
    

    使用万能法的好处就是最后的结果是在[left, right]中,我们可以根据题目要求返回一个值作为答案

实战

下面我将用几道leetcode上的题目来说明在不同的题目中我们使用哪种方法来解决问题

LeetCode 35. 搜索插入位置

题目大意为,在有序数组中查找是否存在目标值target,存在返回其索引,若不存在则返回其按顺序插入数组的位置

我们可以转换题意为返回数组中第一个大于等于target元素的位置,这不就很明显是利用二分法查找模糊值吗?所以我们可以使用方法二的模板来写出如下代码

  • 代码

    /**
    * @param {number[]} nums
    * @param {number} target
    * @return {number}
    */
    var searchInsert = function(nums, target) {
        // 这里要注意如果target大于nums中最大的元素则它将作为第n + 1个元素插入在索引n中,所以我们这里可以直接令right = nums.length
        let left = 0, right = nums.length,mid
        while(left < right) {
            mid = left + Math.floor((right - left) / 2)
            // nums[mid]小于target 且 left = mid + 1不会使答案被排除在外
            if(nums[mid] < target) left = mid + 1
            // nums[mid]大于target 且 right = mid - 1会使答案被排除在外,所以right = mid
            else if(nums[mid] > target) right = mid
            else return mid
        }
        // 也可以在返回时做一次判断若nums[right] > target则返回right,反之返回right + 1
        // return nums[right] > target ? right : right + 1
        return right
    };
    

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

题目就是要求我们查找元素在数组中出现的第一个和最后一个位置,返回其索引,换句话讲就是找出最后一个小于target的元素索引(index1)和第一个大于target的元素索引(index2),最后返回 [index1+1,index2-1]

如果我们理不清边界条件可以直接使用万能法和方法一来解决本题

  • 代码
    /**
    * @param {number[]} nums
    * @param {number} target
    * @return {number[]}
    */
    var searchRange = function(nums, target) {
      let left = -1
      let right = nums.length
      let right2 = nums.length
      let mid
      // 先用方法一查找判断数组中是否存在目标元素
      let isExist = binarySearch(nums, target, left, right, mid)
      if (isExist === -1) return [-1, -1]
      // 找到最后一个小于target的索引 -> left
      while(left + 1 < right) {
          mid = left + Math.floor((right - left) / 2)
          if(nums[mid] < target) left = mid
          else right = mid
      }
      // 此时左边界确定了为right
      // 找到第一个大于target的索引 -> right2 -> right2 - 1 = right
      while(right + 1 < right2) {
        mid = right + Math.floor((right2 - right) / 2)
        if(nums[mid] <= target) right = mid
        else right2 = mid
    }
      return [left + 1, right]
    };
    
    //  二分查找
    var binarySearch = function (arr, target, left, right, middle) {
      while (left <= right) {
        middle = Math.floor((right + left) / 2)
        if (arr[middle] === target) return middle
        //  如果middle指向的元素大于目标元素,代表目标元素在左半区间中 
        if (arr[middle] > target) {
          right = middle - 1
        }
        else {
          left = middle + 1
        }
      }
      return -1
    }
    

总结

  1. 我们使用二分查找的前提是要在一个有序数组中
  2. 二分查找每次循环都要缩小范围,且不能将潜在答案排除在范围之外
  3. 若要精确查找某个值是否存在使用方法一
  4. 若查找模糊值使用方法二
  5. 万能法适用于所有情况但最终结果是两个值,我们要根据题意返回其中一个