二分查找是一种针对有序集合,每次将要查找的区间缩小一半,直到找到查找元素,或区间被缩小为0。
实现有3个注意点:
非递归实现
// 终止条件low>=high 当low=high时,mid=low+(high-low)/2=low=high,即没有等号将少比较一个点
// 中点 mid = low + (high-low)/2 即以low为起点,计算low和high之间的中点
// midvalue,找小区间,high=mid-1
public int binSearch(int[] arr,int n,int value){
int low=0;
int high=n-1;
while(low<=high){
int mid=low+((high-low)>>1);//int mid=low+(high-low)/2;
if(arr[mid]value){
high=mid-1;
} else{
return mid;
}
}
return -1;
}
递归实现
// 二分查找的递归实现
public int bsearch(int[] a, int n, int val) {
return bsearchInternally(a, 0, n - 1, val);
}
private int bsearchInternally(int[] a, int low, int high, int value) {
if (low > high) return -1;
int mid = low + ((high - low) >> 1);
if (a[mid] == value) {
return mid;
} else if (a[mid] < value) {
return bsearchInternally(a, mid+1, high, value);
} else {
return bsearchInternally(a, low, mid-1, value);
}
}
1. 二分查找依赖的是顺序表结构,也就是数组
2. 二分查找要求数据有序
3. 数据量太小不适合二分查找,除非每个数据的操作非常耗时,比如,数组中存储的都是长度超过 300 的字符串,如此长的两个字符串之间比对大小,就会非常耗时。
4. 数据量太大也不适合二分查找,因为数组需要占用大量连续的空间
5. 动态数据不适合二分查找,频繁的插入和删除在数组结构下效率很低
n/2 + n/4 + n/8 + …
和约等于n,因此算法复杂度为O(n),单考虑到其他二分查找的额外操作,会比直接遍历循环查找慢一些。
对于有序数组中有重复元素而言二分查找通常有以下4个变体问题。
查找第一个值等于给定值的元素
查找最后一个值等于给定值的元素
查找第一个大于等于给定值的元素
查找最后一个小于等于给定值的元素
这些都是查找等于给定值的变体问题,想一想,你有什么思路可以实现呢?
arr[mid]和value的值的比较有三种情况:大于,小于,等于
那对于问题1和2而言我们要特殊处理的等于的情况,
问题3要特殊处理大于等于的情况,
问题4要特殊处理小于等于的情况。
问题1的特殊处理逻辑:
等于的情况下,我们mid的下标有可能是0,即数组的第一个元素,那么我们直接返回mid就好了,
那其余情况就是mid>0,那么我们就看mid的前一个元素是否等于value,如果不等于,说明mid是第一个等于value的元素,也直接返回,
如果等于mid,那么我们就需要在前面的区间去查找了,即 high = mid - 1;
那问题2,3,4和1的处理思路类似,可以试着自己实现。
1
public int binSearchFirst(int[] arr,int n,int value){
int low=0;
int high=n-1;
while (low<=high) {
int mid = low + (high - low) / 2;
if(arr[mid]value){
high=mid-1;
} else{
// 需要特殊处理的是等于的情况
// 1.看mid是否为第一个元素,是返回mid
// 2.看mid的前一个元素是否等于value,不等于返回mid
// 3.mid的前一个元素等于value,那么就应该去前面区间找,即 high = mid - 1;
if(mid==0||arr[mid-1]!=value) {
return mid;
}
high=mid-1;
}
}
return -1;
}
2
public int binSearchFirst(int[] arr,int n,int value){
int low=0;
int high=n-1;
while (low<=high) {
int mid = low + (high - low) / 2;
if(arr[mid]value){
high=mid-1;
} else{
// 需要特殊处理的是等于的情况
// 1.看是mid否为最后一个元素,是返回mid
// 2.看mid的后一个元素是否等于value,不等于返回mid
// 3.mid的后一个元素等于value,那么就应该去后面区间找,即 low = mid + 1;
if(mid==n-1||arr[mid+1]!=value) {
return mid;
}
low=mid+1;
}
}
return -1;
}
3
public int binSearchFirst(int[] arr,int n,int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (arr[mid] >= value) {
// 主要处理大于等于的情况
// 1.看mid是否为第一个元素,是返回mid
// 2.看mid的前一个元素是否小于value,是返回mid
// 3.前一个元素大于等于value,应该去前面区间找,即high=mid-1
if(mid==0||arr[mid-1]
4
public int binSearchFirst(int[] arr,int n,int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (arr[mid] <= value) {
// 主要处理大于等于的情况
// 1.看mid是否为最后一个元素,是返回mid
// 2.看mid的后一个元素是否大于value,是返回mid
// 3.后一个元素小于等于value,应该去后面区间找,即low=mid+1
if(mid==n-1||arr[mid+1]>value){
return mid;
}
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1;
}
绝大部分情况能用二分查找解决的问题我们更倾向使用散列表或二叉查找树,
那二分查找其实更适用于近似的查找(范围查找)问题,因为这类问题用上述数据结构都不容易实现。
我们今天讲的都是非常规的二分查找问题,今天的思考题也是一个非常规的二分查找问题。如果有序数组是一个循环有序数组,比如 4,5,6,1,2,3。针对这种情况,如何实现一个求“值等于给定值”的二分查找算法呢?
我们发现循环数组存在一个性质:以数组中间点为分区,会将数组分成一个有序数组和一个循环有序数组。
如果首元素小于 mid,说明前半部分是有序的,后半部分是循环有序数组;
如果首元素大于 mid,说明后半部分是有序的,前半部分是循环有序的数组;
如果目标元素在有序数组范围中,使用二分查找;
如果目标元素在循环有序数组中,设定数组边界后,使用以上方法继续查找。
时间复杂度为 O(logN)。