二分法查找是一种分治策略算法,也叫折半搜索,他将问题分解为规模更小的子问题,分而治之,逐一解决。采用二分法查找的前提条件问:采用顺序存储结构,数据元素排序。
二分法查找算法设计
- 初始化指针:设置两个指针,一个指针数组的起始位置(通常为left),另一个指向数组的结束位置(称为right)。
- 循环条件:只要left小于等于right,就继续循环
- 计算中间索引:计算中间索引mid。为了防止整数溢出,通常使用mid=left+(right-left)/2来计算。
- 比较中间元素:将数组中的中间元素arr[mid]与目标值target进行比较。
- 匹配条件:
如果arr[mid]等于target,则找到目标值,返回mid作为目标值的索引。
如果arr[mid]小于target,则将left更新为mid+1,表示目标值在数组的右半边。
如果arr[mid]大于target,则将right更新为mid-1,表示目标值在数组的左半边。
6.循环结束:如果循环结束时没有找到目标值,返回-1表示目标值不在数组中。
下面以arr[1,2,3,4,5,6,7]举例子:
1.当数组长度为奇数
middle=(0+6)/2=3
mid | |||||||
arr[] | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
2.当数组长度为偶数
middle=(0+7)=3.5,下向取整为3
mid | ||||||||
arr[] | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
当取中间元素,遇到两边数据个数不同时,并不影响我们查找元素,只需要规定是向上或向下取整。
所以数组长度是偶数还是奇数这个并不重要,也不影响怎么排除的问题,无非是多排除一个数字或者少排除一个数字。
3.实现过程
在 {1,2,3,4,5,6,7,8,9,10} 中查找元素9。
第一步要找到中间元素,设置两个变量left、high,分别指向数组第一个元素下标和最后一个元素下标,从而控制数组的范围,再根据low和high确定中间元素的下标mid
left | mid | right | ||||||||
arr[] | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
据mid锁定的元素,和查找的元素(9)比较,确定新的查找范围、left和right
比较5和9的大小,目标元素target>middle,那么left移动到middle+1=5的位置
然后重置middle
left | mid | right | ||||||||
arr[] | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
比较8和9的大小,目标元素target>middle,那么left移动到middle+1=8的位置
left mid | right | |||||||||
arr[] | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
此时,mid=8,arr[mid]=9,与要查找的元素相同,即已经找到了,并返回其下标
下面是代码实现:
public static int binarySearch(int[] arr ,int target){
int left=0;
int right=arr.length-1;
while (left<=right){
int mid =left+(right-left)/2;
if (arr[mid]==target){
return mid;
}else if(arr[mid]<target){
left=mid+1;
}else{
right=mid-1;
}
}
return -1;
}
1.为什么while循环的条件中是<=,而不是<?
因为初始化的right的赋值是nums.length-1,即最后一个元素的索引,而不是nums.length。
这两者可能出现在不同功能的二分查找中,区别是:前者相等于两端都闭区间[left,right],后者相当于左闭右开区间[left,right),因为索引大小nums.length是越界的。
2.此算法的缺陷是什么?
比如有序数组nums=[1,2,2,2,3],target=2,此算法返回到索引是2,是正确的,但是如果我们想返回第一个出现的2,和最后一个出现的2呢?那么这个算法是无法实现的。
所以我们后面引出来了另外两种算法
寻找左侧边界的二分搜索
代码实现:
public static int left_bound(int[] nums,int target){
if (nums.length==0){
return -1;
}
int left =0;
int right=nums.length;//注意这里
while(left<right){
int mid=(left+right)/2;
if (nums[mid]==target){
right=mid;
}else if (nums[mid]<target){
left=mid+1;
}else if (nums[mid]>target){
right=mid;
}
}
return left;
}
1.为什么while(left<right)而不是<=?
因为初始化right=nums.length而不是right=nums.length-1,因此每次循环的搜索区间[left,right)左闭右开。
2.为什么没有返回-1的操作,如果nums中不存在target这个值怎么办?
对于这个数组,算法会返回1,这个1的含义可以这样子解读:nums中小于2的元素有1个。
比如对于有序数组nums=[2,3,5,7],target=1,算法会返回0,含义是nums小于1的元素有0个。如果target=8,算法会返回4,含义是:nums中小于8的元素有4个。
3.为什么该算法能够搜索到左侧边界
关键在于这段代码:
if(nums[mid]==target)
right=mid;
可见,找到了target不要立即返回,而是缩小[搜索区域]的上界right,在区间[left,mid]中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
总结:
时间复杂度:
二分法的时间复杂度是 O(log n),其中 n 是数组的长度或搜索空间的大小。这是因为每次迭代都会将搜索区间减半,因此需要 log₂(n) 次迭代来将区间缩小到一个点。
空间复杂度:
二分法的空间复杂度通常是 O(1),因为它只需要常数级别的额外空间来存储索引或中间值。
注意事项:
- - 二分法要求搜索区间是有序的,因此它不适用于无序数据的搜索。
- - 对于非单调函数,二分法可能无法找到零点或最优解。
- - 在实际应用中,二分法通常需要结合其他算法或逻辑来处理边界条件和特殊情况。