二分查找的大前提:数组有序
一、无重复元素的二分查找
左闭右闭的二分查找算法(推荐)
public void binarySearch(int[] nums,int target){
int left = 0;
int right = a.length - 1;
while(left <= right){
int mid = (left + right) >>> 1;
if(target < nums[mid]){
right = mid - 1;
}else if(nums[mid] < target){
left = mid + 1;
}else{
return mid;
}
}
retun -1;
}
问题?
- 为什么是
left<=right
,而不是left<right
?
当 target 是数据中的第一个元素的时候,使用 < 会将这个元素跳过去,导致没有查找到目标元素。 - 为什么 mid 计算的时候,不使用
(left+right)/2
?
在Java中 int 类型是有范围限制的,如果超过这个范围,他就会将二进制的最高位当作是符号位(1是负数,0是正数),那么他最终得到的中间索引 mid 就可能是一个负数。导致最终出错。
例子:
结果:
所以这里推荐(left + right) >>> 1
或者left + (right - left)/ 2
;
左闭右开的二分算法
public void binarySearch(int[] nums,int target){
int left = 0;
int right = a.length;
while(left < right){
int mid = (left + right) >>> 1;
if(target < nums[mid]){
right = mid;
}else if(nums[mid] < target){
left = mid + 1;
}else{
return mid;
}
}
retun -1;
}
相较于第一种,这种二分查找算法的右边界是不参比较的。因此,这里的right=mid
。也就是说如果使用[left,mid-1)
,他就会导致mid-1
为目标元素的时候,会导致错过这个元素,找不到目标元素。
如何理解这个,错过mid-1
这个元素呢?
当right = mid - 1;
的时候,如下:
也就说:左闭右开的这中二分查找算法,右边界的这个元素是不参与比较的,如果我们的右边界移动范围为right = mid - 1
,这就会导致mid-1
处的这个值错过比较,而我们使用right = mid;
就不会有这种情况。
平衡版的二分查找
平衡版是无论最好最坏的情况,它的时间复杂度都为 O(log(n))。因为他总是在循环完之后才进行结果的比较。
可以这样理解:按照前面的二分查找算法
- 当目标元素刚好在第一次 mid 处时,这就是他最好的时间复杂度 O(1)。
- 当目标元素小于 nums[mid] 的时候,他就直接判断
target < nums[mid]
(因为我们这个条件在第一个),这次计算次数我们记为L。 - 当目标元素大于 nums[mid] 的时候,那么就他得先去判断
target < nums[mid]
,由2知这个判断次数为L。之后才去执行我们的nums[mid] < target
,那么他总共就得执行2L。
对于这种因为目标元素在 mid 的左边还是右边,影响我们的执行次数的情况就是不平衡
。
public int binarySearch(int[] nums,int target){
int left = 0;
int right = nums.length;
while(1 < (right-left)){
int mid = (left + right) >>> 1;
if(target < nums[mid]){
right = mid;
}else{
left = mid;
}
}
return nums[left] == target ? left : -1;
}
这里我个人推荐使用第一种左闭右闭的二分查找算法,他理解起来简答,而且它循环里边的判断有规律,不必去考虑边界元素的问题。
还有一点就是,我下边的二分查找算法都是在它基础上改的。(当然也可以在其他的二分查找算法基础上改,这里只是说我写的这个。)
二、有重复元素的二分查找
返回最左边的索引位置的二分查找
public int binarySearch(int[] nums,int target){
int left = 0;
int right = nums.length - 1;
int condition = -1;
while(left <= right){
int mid = (left + right) >>> 1;
if(nums[mid] < target){
left = mid + 1;
}else if(target < nums[mid]){
right = mid - 1;
}else{
condition = mid;
// 如果找到了目标元素,继续向左查找
right = mid - 1;
}
}
return condition;
}
返回最右边的索引位置的二分查找
public int binarySearch(int[] nums,int target){
int left = 0;
int right = nums.length - 1;
int condition = -1;
while(left <= right){
int mid = (left + right) >>> 1;
if(nums[mid] < target){
left = mid + 1;
}else if(target < nums[mid]){
right = mid - 1;
}else{
condition = mid;
// 如果找到了目标元素,继续向右查找
left= mid + 1;
}
}
return condition;
}
二分查找重复元素,返回元素的最左边还是最右边的元素,只是取决于他找到目标元素之后,他的边界条件的变动。如果是变动左边界就是返回最右边的索引,反之则是最左边的元素。
含重复元素的,思路都是一样的。所以这里根据自己的需求来就行。
三、返回值有效的二分查找
何为返回值有效的二分查找?在前边的二分查找算法中,都是如果目标元素在数组中那么就返回元素在数组中的索引位置,反之就返回-1。这个-1就是一个无效的值。
何为返回值有效?主要针对目标元素不在数组中,返回值是-1的情况。我们这里返回的是不存在的这个元素应该在数组中那个位置插入
。
二分查找(返回值>=target的最左索引)(推荐)
public int binarySearch(int[] nums,int target){
int left = 0;
int right = nums.length - 1;
while(left <= right){
int mid = (left + right) >>> 1;
if(target <= nums[mid]){
right = mid - 1;
}else{
left = mid + 1;
}
}
return left;
}
这种返回值是:
- 如果目标元素存在,返回的是最左的索引(包含数组中有重复元素的情况)。
- 如果目标元素不存在,返回的是它插入的位置。
二分查找(返回值<=target的最右索引)
public int binarySearch(int[] nums,int target){
int left = 0;
int right = nums.length - 1;
while(left <= right){
int mid = (left + right) >>> 1;
if(target < nums[mid]){
right = mid - 1;
}else{
left = mid + 1;
}
}
return left - 1;
}
这种返回值是:
- 如果目标元素存在,返回的是最右的索引(包含数组中有重复元素的情况)。
- 如果目标元素不存在,返回的是距离它左边最近的那个元素的位置。
我个人这里推荐使用第一种这个有效返回值的二分查找算法,他理解起来也很容易。
综上:我们在需要返回值有效的情况下,记住二分查找(返回值>=target的最左索引)
这个就可以了。如果找不到返回值就得是-1,那么记住二
中的就行。
当然:我们也可以将二分查找(返回值>=target的最左索引)
的返回条件改成:
return nums[left] == target ? left : -1;
这既满足含重复元素数组的情况,又在目标元素不存在的时候,返回-1。
关键是理解这其中的各种情况下的边界条件。在理解之后,可以按照自己的需求进行修改。
如果文章中有描述不准确或者错误的地方,还望指正。您可以留言📫或者私信我。🙏
最后希望大家多多 关注+点赞+收藏^_^,你们的鼓励是我不断前进的动力!!!
感谢感谢~~~🙏🙏🙏