二分查找:思路很简单,细节是魔鬼。
在不了解二分细节的情况下,写二分肯定就是玄学编程,能通过全靠菩萨保佑🙏。。
本文将代入东哥的思路,从三个二分最常用的场景入手,带你深入了解二分算法的细节。
1.寻找一个数
2.寻找左侧边界
3.寻找右侧边界
请着重思考:不等号什么情况下应该带等于
mid是否应该加一或者减一
..............
function binarySearch ( nums, target ) {
let left = 0 ,
right = ... ;
while ( ... ) {
let mid = Math, floor ( ( right - left) / 2 ) + left;
let midNum = nums[ mid]
}
}
东哥语录:分析二分查找的一个技巧是,不要用else,全部用else if,把所有情况都列出来,这样可以清楚地展现出来所有细节。
let mid = Math. floor ( ( right - left) / 2 ) + left;
let mid = Math. floor ( left + ( right - left) / 2 ) ;
let mid = Math. floor ( ( right - left) / 2 ) ;
function binarySearch ( nums, target ) {
let left = 0 ,
right = nums. length - 1 ;
while ( left <= right) {
let mid = Math. floor ( ( right - left) / 2 ) + left;
let midNum = nums[ mid] ;
if ( midNum === target) {
return mid
} else if ( midNum < target) {
left = mid + 1 ;
} else if ( midNum > target) {
right = mid - 1 ;
}
}
return - 1
}
问题一:为什么while循环的条件是 <=, 我明明看到有的时候是< 啊?
答:我们在初始化right的赋值是nums.length - 1,即最后一个元素的索引,而不是nums.length;这两个条件可能出现在不同功能的二分查找中,区别是:前者相当于两端都闭区间[left, right],后者相当于左闭右开的区间[left, right),因为索引大小为nums.length时是越界的。
我们这个算法使用的就是[left, right],是两端都闭合的区间,这个区间其实就是每次进行搜索的区间。
那什么时候要停止搜索呢?找到了目标值的时候就可以终止。
if ( midNum === target) {
return mid
}
但是如果没有找到的话,就需要while循环终止了,然后返回-1,当搜索区间为空的时候,while循环终止,
left<=right 的终止条件是left = right +1,写成区间表示就是[right + 1, right],这个时候搜索区间为空,跳出循环;
left < right 的终止条件是 left = right,写成区间的形式就是[right, right],这个时候搜索区间为空,跳出循环。
问题二:为什么 left = mid + 1,right = mid - 1?我看有的代码是 right = mid 或者 left = mid,没有这些加加减减,到底怎么回事,怎么判断?
答:联想刚刚的搜索区间概念就很容易得出,本算法的搜索区间是[left, right],那么当我们发现索引mid不是我们要找的target时,那我们就应该去找[left, mid - 1]或者[mid + 1, right]对不对,因为mid已经搜索过,应该从搜索区间去除。
问题三:此算法有什么缺陷?
答:至此,你应该已经掌握了该算法的所有细节,以及这样处理的原因。但是,这个算法存在局限性。
比如说给你有序数组 nums = [1,2,2,2,3],target 为 2,此算法返回的索引是 2,没错。但是如果我想得到 target 的左侧边界,即索引 1,或者我想得到 target 的右侧边界,即索引 3,这样的话此算法是无法处理的。
这样的需求很常见,你也许会说,找到一个 target,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的复杂度了。
function left_bound ( nums, target ) {
if ( nums. length === 0 ) retuen - 1
let left = 0 ,
right = nums. length;
while ( left < right) {
let mid = Math. floor ( ( right - left) / 2 ) + left;
let midNum = nums[ mid] ;
if ( midNum === target) {
right = mid
} else if ( midNum > target) {
right = mid
} else if ( midNum < target) {
left = mid + 1
}
}
return left
}
问题一:为什么while要小于而不是小于等于?
答:因为right 的取值是nums.length,而不是nums.length - 1,因此我们的搜索区间是左闭右开,用区间表示就是[left,right), while(left < right )的终止条件是left === right,用区间表示就是[right, right)为空,所以可以正确终止。
问题二:为什么没有return -1的操作?若果nums不存在target值的时候,怎么办?
答:对于数组[1,2,2,2,3],target=2算法会返回1,可以表示符合条件的2的下标是1,也可以表示nums中小于2的元素有1个。
对于数组[2,3,5,7],target=1,算法会返回0,表示,nums中比1小的元素有0个。
对于数组[2,3,5,7],target=8,算法会返回4,表示,nums中比1小的元素有4个。
综合可以看出,函数的返回值的取值区间是[0,nums.length),所以我们简单添加两行代码就能正确的时候 return -1;
while ( left < right) {
...
}
if ( left === nums. length) retuen - 1
return nums[ left] === target ? left : - 1
问题三:为什么 left = mid + 1,right = mid ?和之前的算法不一样?
答:还是因为搜索区间,我们的搜索区间是[left,right)左闭右开,所以当nums[mid]被检测之后,下一步的搜索区间应该去掉mid分割成两个部分,即[left, mid)或者[mid + 1, right).
问题四:为什么该算法能够搜索左侧边界?
答:关键在于 if(midNum === target) { right = mid } 当我们找到target的时候不要立即返回,而是缩小搜索区间的上界,在[left.mid)中继续搜索,即不断的向左收缩,达到锁定左侧边界的目的。
问题五:为什么返回的是left而不是right?
答:我们whie的终止区间是left===rigth,所以返回left和right其实是一样的。
function right_bound ( nums. target ) {
if ( nums. length === 0 ) return - 1
let left = 0 ,
right = nums. length;
while ( left < right) {
let mid = Math. floor ( ( right - left) / 2 ) + left;
let midNum = nums[ mid] ;
if ( midNum === target) {
left = mid + 1
} else if ( midNum > target) {
right = mid
} else if ( midNum < target) {
left = mid + 1
}
}
return left - 1
}
问题一:为什么这个算法能够找到右侧边界?
答:if (nums[mid] == target) { left = mid + 1 }
当nums[mid] === target的时候,不要直接返回mid,而是增大搜索区间[mid + 1, right)使得区间不断向右收缩,达到锁定右侧边界的目的。
问题二:为什么最后返回 left - 1 而不像左侧边界的函数,返回 left?而且我觉得这里既然是搜索右侧边界,应该返回 right 才对。
答:首先,while循环的跳出条件是left === right,所以返回left和right其实是一样的。至于为什么要减一,这是搜索右侧边界的一个特殊点,关键在于这个条件的判断:
if (nums[mid] == target) { left = mid + 1; } 因为我们对left的更新必须是 left = mid + 1,就是说当while循环结束的时候,nums[left]一定不等于target,而nums[left - 1]肯定是target。
问题三:为什么没有返回 -1 的操作?如果 nums 中不存在 target 这个值,怎么办?
答:类似之前的左侧边界搜索,因为 while 的终止条件是 left == right,就是说 left 的取值范围是 [0, nums.length),所以可以添加两行代码,正确地返回 -1:
while ( left < right) {
}
if ( left == 0 ) return - 1 ;
return nums[ left- 1 ] == target ? ( left - 1 ) : - 1 ;
逻辑统一:
首先我们初始化right的时候是nums. length - 1 ;
所以我们的搜索区间是 [ left, right]
所以我们的while 条件是 left <= right
同时也决定了 left = mid + 1 或者 right = mid - 1
因为我们只要找到一个target的索引就可以了,
所以当nums[ mid] === target的时候就可以返回了
我们初始化right的时候是nums. length
所以我们的搜索区间是 [ left, right) 左闭右开
所以我们while 的条件是 left < right
同时也决定了 left = mid + 1 或者 right = mid
因为我们要找符合条件的最左侧索引,所以当target === nums[ mid] 的时候不要立即返回结果,而是收紧右侧边界以锁定左侧边界。
我们初始化right的时候是nums. length
所以我们的搜索区间是 [ left, right)
所以我们 while 的条件是 left < right
同时也决定了 left = mid + 1 或者 right = mid
因为我们要寻找右侧边界,所以当nums[ mid] === target的时候不要立即返回,而要收缩左侧的边界以锁定右侧的边界。
又因为收紧左侧边界的时候必须left = mid + 1 ,所以最后的返回应该是 left - 1