持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情
二分查找的概念
如果有一个数组,我们要在里面查找某一个目标值存不存在,最简单的办法,就是遍历一遍,遍历的时候,直接就能看出目标值是否存在于数组中。
但是我们如果给定这个数组是已经排好序的,那么就不用遍历每一个元素。
大概的思路就是,先找到数组中间位置的元素,拿这个元素与目标值比较,如果刚好等于目标值,则退出。
如果中间元素比目标值大,说明目标值肯定出现在前面;
如果中间元素比目标值小,说明目标值肯定出现在后面。
以此类推,循环即可。
这就是二分查找的思路。
力扣35题描述
这里直接copy过来原题(35. 搜索插入位置 - 力扣(LeetCode)):
- 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
注意原题中,确保了这个数组无重复数组,我们这里去掉这个限定,也就是说数组元素可以重复。
这个题非常具有代表性,可以说这一题的解法非常容易扩展到其他二分的场景中。所以我们先来分析这一题如何正确的使用代码来进行二分查找。
之所以这个题妙,妙就妙在,这个题包含了目标值不存在于数组中的情况, 此时我们需要返回,插入这个目标值之后,这个目标值的下标。
- 比如说这个目标值比第一个值还要小,我们就返回下标0,因为插入之后,这个目标值肯定是位于下标0的位置。
- 比如说这个目标值比最后一个值还要大,我们就返回数组长度,因为插入之后,这个目标值肯定是位于最后的位置。
这里还要关注一点,因为有重复元素,我们先规定一下,如果目标值刚好与某个重复元素相等,此时应该返回第一个下标。
比如说在
这个数组中,查找目标3,应该返回第一个3的下标,也就是。
针对上述问题的代码思路
现在撇开二分不谈,我们给出一个思考题:
上述数组为data, 查找目标记作target, 结果输出记作ans。
问题1:data[index] < target
如果在下标index处, 有data[index] < target,即此处的值比目标值要大。
此时ans还有没有可能等于index,或者比index还要小?
答案是:绝无可能,ans一定是在index右边。
因为这个题的题意是要找,插入位置的下标,试想,如果把这个目标值放入index的位置,那么势必该位置原有的元素会往后挪一位,此时已经违背了有序数组这个前提了。
有了上面的结论,在更新前后查找下标的时候,思路将会十分清晰。
再来思考一个相应的问题:
问题2:data[index] > target
如果在下标index处, 有data[index] > target,即此处的值比目标值要大。
此时ans是否一定在index左边?
答案:ans可能在index左边,也有可能ans==index。
来看这个例子, 数组, 目标值,此时目标值应该是顶替的位置,所以ans = 2。
我们发现,即使在原有的数组里,下标的元素大于目标值, 但是目标值依然可能就插入在这个位置。
问题3:data[index] == target
这种情况下,说明 index 出的元素刚好是我们的目标值,但是此时还不能立即返回,因为我们上面有规定,是要查找到第一个出现目标值的位置,所以此情况下,最终的ans依然有可能在index左边。也就是说此情况与问题2的情况相同对待。
代码如下
有了上面三种情况的分析之后,我们代码如下:
function bs(data, target, begin, end) {
let left = begin;
let right = end;
let mid = 0;
while(left < right){
mid = (left + right)/2;
if (nums[mid]<target)
{
left = mid + 1;
}else{
right = mid;
}
}
return left;
}
其中begin和end是ans可能取值的最小和最大值,在一般情况下,begin 传入0,end传入数组的长度即可。
注意,end 不是 数组的长度 - 1, 而就是数组的长度,因为ans在极端情况下,就是数组的长度。
mid的计算方式
看这一句:
mid = (left + right)/2。
这种计算方式,mid是靠左的,也就是说,极端情况下,mid == left。
会发生无限循环吗,不会。
因为下面的两个语句分支,都直接会导致left > right。所以不会发生无限循环。
改动查找目标为从左往右最后一个相等的元素
, 来搜索3,按照上面的代码,会得到下标5。
现在想要得到最后一个3,也就是要得到7, 代码如何实现?
我们在上面的代码里,是这样来执行逻辑的:
- 如果处于mid下标的元素 = target,我们就把最终范围的右边界right缩到了mid处。
这很明显不行,因为mid的右边很有可能依然有等于target的元素存在。
所以我们遇到这种情况,需要把左边界left移动到mid+1。
此时也会出现一个问题,那就是,left代表了最终ans的最小值。如果mid+1处的元素已经大于target,很明显返回的left是一个错误的ans。
不要紧,如果真出现了 , 此时做一次额外的判断就行了。
代码如下:
function bs(data, target, begin, end) {
let left = begin;
let right = end;
let mid = 0;
while(left < right){
mid = (left + right)/2;
if (nums[mid]<=target) // 注意这里变成<=
{
left = mid + 1;
}else{
right = mid;
}
}
// 如果left已经越界或者left处的元素大于target
if (left == end || data[left]>target ) {
if (data[left - 1] == target) {
return left - 1;
}
}
return left;
}