聊聊二分法

301 阅读2分钟

这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战

二分法

二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。注意必须要是有序排列,但有一种特殊情况可以不必须有序排列,即前一节介绍的商品选取,从一堆标准重量为10的商品中查找出唯一的次品,这种特殊的数据情况也可以使用二分查找。

寻找一个数(基本的二分搜索)

这个场景是最简单的,可能也是大家最熟悉的,即搜索一个数,如果存在,返回其索引,否则返回 -1。

function  numSearch(nums, target) {
    let left = 0; 
    let right = nums.length - 1; // 注意

    while(left <= right) { // 注意
        let mid = (right + left) / 2;
        if(nums[mid] == target)
            return mid; 
        else if (nums[mid] < target)
            left = mid + 1; // 注意
        else if (nums[mid] > target)
            right = mid - 1; // 注意
        }
    return -1;
}

注意的点

  • 我们的循环条件中包含了 left == right的情况,则我们必须在每次循环中改变 left 和 right的指向,以防止进入死循环

循环终止的条件包括:

  • 找到了目标值
  • left > right (这种情况发生于当left, mid, right指向同一个数时,这个数还不是目标值,则整个查找结束。)
  • left + ((right -left) >> 1) 其实和 (left + right) / 2是等价的,这样写的目的一个是为了防止 (left + right)出现溢出,一个是用右移操作替代除法提升性能。
  • left + ((right -left) >> 1) 对于目标区域长度为奇数而言,是处于正中间的,对于长度为偶数而言,是中间偏左的。因此左右边界相遇时,只会是以下两种情况:
  • left/mid , right (left, mid 指向同一个数,right指向它的下一个数)
  • left/mid/right (left, mid, right 指向同一个数)
  • 即因为mid对于长度为偶数的区间总是偏左的,所以当区间长度小于等于2时,mid 总是和 left在同一侧。

二分查找左边界

利用二分法寻找左/右边界是二分查找的一个变体,应用它的题目常常有以下几种特性之一:

  1. 数组有序,但包含重复元素
  2. 数组部分有序,且不包含重复元素
  3. 数组部分有序,且包含重复元素
// 查找左边界
  function searchLeft( nums, target) {
        let left = 0;
        let right = nums.length - 1;
        while (left < right) {
            let mid = left + (right - left) / 2;
            if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        return nums[left] == target ? left : -1;
    }

注意的点

  • 循环条件: left < right
  • 中间位置计算: mid = left + ((right -left) >> 1)
  • 左边界更新:left = mid + 1
  • 右边界更新: right = mid
  • 返回值: nums[left] == target ? left : -1

二分查找右边界

 function searchRight(nums, target) {
        let left = 0;
        let right = nums.length - 1;
        while (left < right) {
            int mid = left + parseInt((right - left) /2) + 1;
            if (nums[mid] > target) {
                right = mid - 1;
            } else {
                left = mid;
            }
        }
        return nums[right] == target ? right : -1;
    }

注意的点

中间位置的计算变了,我们在末尾多加了1。这样,无论对于奇数还是偶数,这个中间的位置都是偏右的。