定义:
又称折半查找,核心思想是:在有序序列中,不断缩小搜索区域,降低查找目标元素的难度,时间复杂度:O(logn)
。
思路:不断缩小搜索区域。
常用做法:
- 确定初始作左右边界 left right;
- 获取中间值 middle = left+((right-left)>>1);
- 比较目标值与中间值的大小;
- 中间值大,说明目标在中间的左边,需要缩小right的值,默认是right = middle( \pm 1);
- 相等 中间值即为目标值,返回;
- 中间值小,说明目标值在中间值的右边,需要扩大left的值,常用是left = middle( \pm 1);
- 当比较这个区间都目标值都不在其中,说明目标在区间之外,此时目标值与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;
};
复制代码
解释
-
还有一种做法,构建双闭合区间,即
nums[right]
也是取出有效内容,那么初始条件下right = len-1
,或造成以下结果:- while条件要从
left<right
改为left<=right
因为是双闭合区间; - 扩大 缩小区域时left、right可能需要对middle进行 \pm 1;
- 当所在区间均不满足时 ,为
return right + 1
;
- while条件要从
-
取中间值
>>
js中32 位数字中的所有有效位整体右移,再使用符号位的值填充空位。移动过程中超出的值将被丢弃;>>1
表示右移1位,即缩小2倍,优势是取整没有小数;left+(right-left)/2
一般情况下与(left+right)/2
一样,但是当left 与right均为很大的数时候,left+right 可能会超过系统最大值,从而报错,而left + ( right - left) /2
则不会超过。
-
二次比较的时候不要有重复项,即当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较二分查找可能存在较大波动,性能不一定比二分法好。
结论
- 二分法非常适合对于有序序列;
- 插值法适用于均匀数列;