1. 二分查找的思想
二分查找针对的是一个有序的数据集合,查找的思想类似于分治思想,每次都与区间中间的元素对比,将区间的大小缩小为原来的一半,直至找到所找的元素或者区间的大小变为0
- 二分查找的时间复杂度:o(logn),非常高效
- 二分查找应用的场景比较局限:(1)在顺序表(数组)上进行使用(需要数据结构支持随机访问的特点才能体现其查找优势)(2)针对插入和删除特别频繁的动态数组不太适合使用二分查找,因为维护序列的有序性所需要的成本太高(3)数据量太小(无法体现其优势)和数据量太大(使用的是数组,需要连续的内存)都不适合使用二分查找。
2.二分查找的基本实现
其基本实现注意三个点:
- 终止条件是low=high,因为此时区间大小为0
- mid的计算方式最好为low + ((high-low)>>1)(注意多个括号,运算符的优先级)
- low和high的值的更新问题,避免陷入死循环
非递归实现:
class Solution{
public int bSearch(int[] a, int n, int value){
if(n<=0) return;
int low = 0, high = n-1;
while(low<=high){
int mid = low + ((high-low)>>1); //防止low+high的值过大溢出,另外位运算比除法运算要快
if(a[mid] == value){
return mid;
}else if(a[mid]>value){
high = mid - 1;
}else{
low = mid + 1;
}
}
return -1;
};
}
递归实现:
class Solution{
public int bSearch(int a[], int n, int value){
if(n<=0) return;
return bSearchRecur(a, 0, n-1, value);
};
public int bSearchRecur(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;
if(a[mid] > value){ //value位于a[mid]左侧
return bSearchRecur(a, low, mid-1, value);
}else{ //value位于a[mid]右侧
return bSearchRecur(a, mid+1, high, value);
}
};
}
3. 二分查找的变形问题
(1) 查找第一个值等于给定值的元素
class Solution{
public int bSearchFisrt(int[]a, int n, int value){
if(n<=0) return;
return bSFirst(a, 0, n-1, value);
};
pulic int bSFisrt(int[]a, int low, int high, int value){
if(low > high) return -1;
int mid = low + ((high-low)>>1);
if(a[mid] > value)
return bFsearch(a, low, mid-1, value);
else if(a[mid]<value)
return bFsearch(a, mid+1, high, value);
else{
if(mid==0 || a[mid-1]!=value) //关键步骤,要么mid已经是数组的第一个元素,要么比mid小的下标对应的数组值不等于value,否则继续向前找
return mid;
return bFsearch(a, low, mid-1, value);
}
};
}
(2) 查找最后一个值等于给定值的元素
class Solution{
public int bSearchFisrt(int[]a, int n, int value){
if(n<=0) return;
return bSFirst(a, 0, n-1, value);
};
pulic int bSFisrt(int[]a, int low, int high, int value){
if(low > high) return -1;
int mid = low + ((high-low)>>1);
if(a[mid] > value)
return bFsearch(a, low, mid-1, value);
else if(a[mid]<value)
return bFsearch(a, mid+1, high, value);
else{
if(mid==a.lengh-1 || a[mid+1]!=value) //关键步骤,要么mid已经是数组的最后个元素,要么比mid大的下标对应的数组值不等于value,否则继续向后找
return mid;
return bFsearch(a, mid+1, high, value);
}
};
}
(3)查找第一个大于等于给定值的元素
class Solution{
public int bSearchFisrt(int[]a, int n, int value){
if(n<=0) return;
return bSFirst(a, 0, n-1, value);
};
pulic int bSFisrt(int[]a, int low, int high, int value){
if(low > high) return -1;
int mid = low + ((high-low)>>1);
if(a[mid] >= value){
if(mid == 0|| a[mid-1] < value) //关键步骤,当mid下标对应元素为数组第一个元素或者mid-1下标对应元素的值比value值要小时返回,否则继续向前找
return mid;
return bFsearch(a, low, mid-1, value);
}
else
return bFsearch(a, mid+1, high, value);
};
}
(4)查找最后一个小于等于给定值的元素
class Solution{
public int bSearchFisrt(int[]a, int n, int value){
if(n<=0) return;
return bSFirst(a, 0, n-1, value);
};
pulic int bSFisrt(int[]a, int low, int high, int value){
if(low > high) return -1;
int mid = low + ((high-low)>>1);
if(a[mid] > value)
return bFsearch(a, low, mid-1, value);
else{
if(mid == a.length-1 || a[mid+1] > value) //关键步骤,当mid下标对应元素为数组最后一个元素或者mid+1下标对应元素的值比value值要大时返回,否则继续向后找
return mid;
return bFsearch(a, mid+1, high, value);
}
};
}
4. 二分查找问题实战
(1)求一个数的平方根?要求误差小于某个具体值range
class Solution{
public double getSqrt(int num, double range){
if(num<0) return;
return mySqrt(num, 0.0, (double)num, range);
};
public double mySqrt(int num, double low, double high, double range){
double mid = (low + high)/2f;
if(num < mid*mid){ //当num < mid*mid时,mid的值偏大,故需要在小一点值的区间查找
return mySqrt(num, low, mid, range);
}else{
if(num - mid*mid > range){ //当num > mid*mid,且num-mid*mid>range的值时,mid的值偏小,需要在大一点值的区间查找
return mySqrt(num, mid, high, range);
}
return mid;
}
};
}
(2)搜索旋转排序数组(leetcode33)
整数数组 nums 按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
思路:观察循环有序数组我们可以发现若将其二分,二分后的两个数组,要么左侧数组为有序数组、右侧数组为循环数组;要么左侧数组为循环数组、右侧数组为有序数组,按照这个特性,可以完成查找工作:
- 如果首元素nums[l]小于 nums[mid],说明前半部分是有序的,后半部分是循环有序数组;
- 如果首元素nums[l]大于 nums[mid],说明后半部分是有序的,前半部分是循环有序的数组;
- 如果目标元素在有序数组范围中,使用二分查找;
- 如果目标元素在循环有序数组中,设定数组边界后,使用以上方法继续查找。
class Solution {
public int search(int[] nums, int target) {
int len = nums.length;
if(len<=0)
return -1;
if(len == 1){
return nums[0] == target ? 0:-1;
}
int low = 0, high = len-1;
while(low <= high){
int mid = low + ((high - low)>>1);
if(nums[mid] == target) return mid;
if(nums[low]<=nums[mid]){ //表示左边有序
if(nums[low]<=target && target < nums[mid]) //target位于左侧有序数组,二分查找target
high = mid-1;
else //target位于右侧循环数组,继续分组
low = mid+1;
}else{
if(nums[mid]<target && target<=nums[high])
low = mid + 1; //target位于右侧有序数组,二分查找target
else
high = mid-1; //target位于左侧循环数组,继续分组
}
}
return -1;
}
}