二分查找
二分查找作为数组的基操,肯定是要牢牢掌握的,其思想十分简单,但是对于细节的处理,就可以看出你对二分算法的理解深刻与否。
如果没有真正的理解细节处的内涵,二分查找算法的编写就是玄学编写,这里加不加等号呢?这里是+1还是不变呢?最后该返回啥呢?
接下来我们就来看看这些细节该如何处理
下面使用三个二分查找使用的场景做出分析:寻找一个数、寻找左侧边界、寻找右侧边界
二分查找的骨架
1 | public int binarySearch(int[] nums, int target) { |
或许你经常看见一些代码不会将等于的情况单独列出来,但是我觉得对于二分查找的分析,将等于的情况单独分离出来,会更有利于理解
等到彻底理解后,随便怎么写都不会少搜索区间的时候,那就随便合并语句了
上述骨架中..
出现的地方就是我们需要去琢磨的细节之处了,乍一看,全是细节,所以叫魔鬼细节探究
先解决最好解决的一个细节
很多人都在LeetCode官网中看到有时候官网对于中间值的取值
有时候会使用(right - left) / 2 + left
有时候会使用(left + right) >> 1
有时候看见别人写的代码中又是(right + left) / 2
,肯定会很迷糊,这三者的计算到底有什么不同之处呢?
(right - left) / 2 + left
这种写法是为了防止(left+right)
因为数据过大导致溢出,导致mid计算错误,
(left + right) >> 1
这是位运算,处理效率会高一点
(right + left) / 2
这就是正常写法,一般也不会出错
接下来按照二分查找的常见使用场景来逐个做出分析
查找一个数
场景1:查找一个数,存在返回索引,不存在返回-1
1 | int binarySearch(int[] nums, int target) { |
- 循环条件是
left <= right
而不是left < right
?
这里我们要说一下什么时候应该退出循环,就是当我所有的元素都搜索完成之后(即搜索区间里面没有元素的时候),或者是找到了目标值的时候可以直接终止,返回结果。
当right赋值为nums.length - 1
,则搜索区间为[left,right]
,所有元素搜索完成的条件应该是left==right+1
,即搜索区间为[right+1,right]
,符合要求。如果循环退出条件改为 left<right
,则退出循环的时候,搜索区间为[left,right]
,会缺少搜索第left个元素,不符合要求
如果**right赋值为nums.length
**,则搜索区间为[left,right)
,所有元素搜索完成的条件应该是left==right
,即搜索区间为[left,right)
- 当
nums[mid] > target
的时候,为什么是right = mid - 1;
而不是right = mid;
?
还是回到搜索区间的问题上,当我发现当前这个元素与target不等而是大于target,那么我只需要搜索左边区间,即将[left,right]
分割为左边区间[left,mid-1]
和右边区间[mid+1,right]
,所以下一步应该将mid-1
赋值给right
- 此算法的用途
这种方法只可以在找到目标值的情况下返回索引,但是当nums为[1,2,2,2,4]
的时候,返回的值是2,没错,但是这样可能就无法满足我们的需求了,只能在数组是严格递增的情况下使用。
如果场景变为:查找一个数,如果找到则返回索引位置,如果找不到,则返回目标值插入数组的位置
这个时候其实只要最后返回left即可
返回左边界
1 | int binarySearch(int[] nums, int target) { |
- 为什么最后返回的时候要做出判断?
当目标元素比所有的值都小的时候,在最后一次循环的时候,right = mid - 1
,left此时为0,但是无法确定该元素是否为目标元素,所以需要做出判断,
当目标元素比所有的值都大的时候,在最后一次循环的时候,left= mid + 1
,left此时为数组长度,这时候则说明,该元素在此数组中不存在,也要做出判断
- 为什么找到的一定是左边界?
因为这一段代码
1 | if(nums[mid] == target){ |
当数组元素等于目标元素的时候,不着急返回,而是缩小查找范围。因为需要查找左边界,所以需要向左边收缩区间
- 这里面while的循环条件可以改为
left < right
吗?
当然可以,只要理解了搜索区间,只要能够保证所有的元素都被查找了,怎么写都可以。
1 | int binarySearch(int[] nums, int target) { |
当目标元素比所有的值都大的时候,在最后一次循环的时候,left= mid + 1
,left此时为数组长度,这时候则说明,该元素在此数组中不存在,需要单独做出判断
当目标元素比所有的值都小的时候,在最后一次循环的时候,right = mid
,left此时为0,但是无法确定该元素是否为目标元素,所以需要做出判断,
返回右边界
1 | int binarySearch(int[] nums, int target) { |
- 为什么最后返回的时候要做出判断?
当目标元素比所有的值都大的时候,在最后一次循环的时候,left= mid + 1
,left此时为数组长度,这时候则说明,该元素在此数组中不存在,也要做出判断
当目标元素比所有的值都小的时候,在最后一次循环的时候,right = mid - 1
,left此时为0,但是无法确定该元素是否为目标元素,所以需要做出判断,
- 为什么找到的一定是右边界?
因为这一段代码
1 | if(nums[mid] == target){ |
当数组元素等于目标元素的时候,不着急返回,而是缩小查找范围。因为需要查找右边界,所以需要向右边收缩区间
- 如何修改循环条件为
left < right
?
1 | int binarySearch(int[] nums, int target) { |