二分查找及改进

1,707 阅读3分钟

定义:

又称折半查找,核心思想是:在有序序列中,不断缩小搜索区域,降低查找目标元素的难度,时间复杂度:O(logn)

思路:不断缩小搜索区域。

常用做法:

  1. 确定初始作左右边界 left right;
  2. 获取中间值 middle = left+((right-left)>>1);
  3. 比较目标值与中间值的大小;
  4. 中间值大,说明目标在中间的左边,需要缩小right的值,默认是right = middle( \pm 1);
  5. 相等 中间值即为目标值,返回;
  6. 中间值小,说明目标值在中间值的右边,需要扩大left的值,常用是left = middle( \pm 1);
  7. 当比较这个区间都目标值都不在其中,说明目标在区间之外,此时目标值与left right 相关。

实战

leetcode 搜索插入位置 为例,

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

分析:既定数组为排序数组 满足二分查找的条件;

具体解法:

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
var searchInsert = function (nums, target) {
    const len = nums.length;
    let left = 0; 
    let right = len;
    // 由于nums[nums.length]会导致下标越界,所以从left到right构建的是一个[) 区域,此时left不能等于right。因为[right,right)是无效区间
    while (left < right) {
        let middle = left + ((right - left) >> 1); // 获取中间值
        if (target < nums[middle]) {
            // 中间值大于目标值,说明中间值在middle左边,缩小right的值,由于区间右侧不包含,所以新right=middle,无需+1,因为当前位置不会被二次比较
            right = middle;
        } else if (target === nums[middle]) {
            // 相等 middle即可
            return middle;
        } else {
            //目标值大于中间值,说明目标值在右边,需要扩大left的值。由于当前值不是目标值,所以left=middle+1,避免重复比较middle位置。
            left = middle + 1;
        }
    }
    // 当while循环完(此时left=right)还没有拿到目标值,说明目标值在[left,right)之外,此时返回right即可
    return right;
};

解释

  1. 还有一种做法,构建双闭合区间,即nums[right]也是取出有效内容,那么初始条件下right = len-1,或造成以下结果:

    1. while条件要从left<right 改为left<=right 因为是双闭合区间;
    2. 扩大 缩小区域时left、right可能需要对middle进行 \pm​​​​ 1;
    3. 当所在区间均不满足时 ,为return right + 1;
  2. 取中间值

    1. >> js中32 位数字中的所有有效位整体右移,再使用符号位的值填充空位。移动过程中超出的值将被丢弃;>>1 表示右移1位,即缩小2倍,优势是取整没有小数;
    2. left+(right-left)/2 一般情况下与 (left+right)/2一样,但是当left 与right均为很大的数时候,left+right 可能会超过系统最大值,从而报错,而left + ( right - left) /2 则不会超过。
  3. 二次比较的时候不要有重复项,即当middle与目标值不等的时候,给left/right 赋值的时候需要格外注意,否则会造成无限循环。如:

    ...
        承接上面code
        if (target < nums[middle]) {
                right = middle+1; // 修改部分
        }
    ...
    分析:当需要缩小区间时,把右边界定位 middle+1,看似没有问题,右边界怎么定义都行。实则问题很大,仔细分析可知:
    1,第一次时middle 已经与目标值进行比较过,二者不等;
    2,第二次时新区间为[left,middle+1)(middle为上一次的middle),有效区间为[left,middle],也就是说第二次时上一次的middle需要再比较一次,middle被重复比较了。
    以数组nums:[1,3] 目标值target:2 为例:
    left:0,right:2 ==> middle:1;
    nums[middle]:3 target:2 ==> middle> target;
    按照上文(right = middle+1)可得:新right=2;与初始条件一样,那么将陷入死循环中。GG~
    

扩展

从上面的代码可以看出,二分查找的关键是缩小查找区间,从而尽可能的减小比较次数,特别是第一次。而二分查找每次都是选取中间的那个记录关键字作为划分依据的,那为什么不可以是其他位置的关键字呢?在有些情况下,使用二分查找算法并不是最合适的。举个例子:在1-1000中,一共有1000个关键字,如果要查找关键字10,按照二分查找算法,需要从500开始划分,这样的话效率就比较低了,所以有人提出了插值法。说白了就是改变划分的比例,比如三分或者四分。

插值查找算法对二分查找算法的改进主要体现在mid的计算上,其计算公式如下:

二分查找改进中值公式

而二分查找的公式如下:

mid = low + (high-low)/2 ​

二者的主要区别是1/2这个系数,插值法主要是根据key在整个数组中所占的位置来进行确定中间值的,而二分查找则将中间值定为数组1/2 位置。仔细分析可知,当数列比较均匀时,插值法的性能会好很多;当数列极不均匀时,插值法算出的mid较二分查找可能存在较大波动,性能不一定比二分法好。

结论

  1. 二分法非常适合对于有序序列;
  2. 插值法适用于均匀数列;

参考链接

  1. 2种二分查找及2种优化方式