1. 适用范围
当题干中出现有序 、时间复杂度为O(logN) 等字眼,可能会使用到二分查找算法
2. 二分查找的两种写法
在分析两种写法之前,先提出一点优化
int middle = (left+right)/2;
当left和right都很大时,两者相加结果可能造成整型数据溢出,最好改成下面的写法
int middle = left+(right-left)/2;
//还可以用位运算的方式(位运算速度会快于算术运算)
int middle = left+((right-left)>>1);
2.1 左闭右闭
在数组nums[left,right]区间内寻找target的下标,此时left~right都可以是有效下标
-
终止条件:left > right(left = right+1)
终止时的区间[right+1,right](不存在任何可取的下标) -
target < nums[middle] :目标只能出现在偏小区间,扫描区间右边界向左移
right = middle-1;(target下标一定不是middle,而right有可能是target下标) -
target > nums[middle] :目标只能出现在偏大区间,扫描区间左边界向右移
left = middle+1; -
如果target在该数组不存在,返回-1
整体代码如下:
public int getIndex(int[] nums,int target){
int left = 0;
int rihgt = nums.length-1;
int middle = 0;
while(left<=right){
middle = left+(right-left)/2;
if(target == nums[middle]) return middle;
else if(target < nums[middle]) right = middle-1;
else left = middle+1;
}
return -1;//target下标不存在
}
2.2 左闭右开
在数组nums[left,right)区间内寻找target的下标,此时left~right-1都可以是有效下标,right无效
- 终止条件:left == right
终止时的区间[right,right)(不存在任何可取的下标) - target < nums[middle] :目标只能出现在偏小区间,扫描区间右边界向左移
right = middle;(target下标一定不是middle,right不可能是target下标) - target > nums[middle] :目标只能出现在偏大区间,扫描区间左边界向右移
left = middle+1; - 如果target在该数组不存在,返回-1
整体代码如下:
public int getIndex(int[] nums,int target){
int left = 0;
int rihgt = nums.length;
int middle = 0;
while(left<right){
middle = left+(right-left)/2;
if(target == nums[middle]) return middle;
else if(target < nums[middle]) right = middle;
else left = middle+1;
}
return -1;//target下标不存在
}
3. 二分查找的更多应用
3.1 寻找target的左边界
当一个有序数组中可能有多个相同的target,如何找到第一个出现的target?
如在[1,2,2,2,3]数组中,返回第一个2出现的下标
- 初始区间:0~nums.length;当数组中所有元素都小于target时,需要返回nums.length
- 终止条件:left == right时终止;即使数组中不存在target,也要假设其存在,并返回理论上的下标。只有当left==right时,能够得到一个唯一位置
- 分析:
- 利用二分查找法找到target之后,不能马上返回当前middle下标,因为找到的这个target可能是第一个,也可能不是第一个,我们需要继续寻找。
- 如果当前target(nums[middle])不是第一个,那第一个target下标必然在middle左边,扫描区间右边界需要左移到middle处。
right = middle; - 如果当前target(nums[middle])是第一个,即使扫描区间左移到middle也无影响,因为边界处可以取
整体代码如下:
public int getIndex(int[] nums,int target){
int left = 0;
int rihgt = nums.length;
int middle = 0;
while(left<right){
middle = left+(right-left)/2;
if(target == nums[middle]) right = middle;
else if(target < nums[middle]) right = middle; //可以与上面的条件合并
else left = middle+1;
}
return right;
}
左边界的特殊含义:
在[0,n)区间内寻找target左边界下标
- 如果target的左边界下标为x,说明数组中有x个数小于target
- 若x == 0,说明数组中不存在target,所有数都大于target
- 若x == n,说明数组中不存在target,所有数都小于target
改进代码:
public int getIndex(int[] nums,int target){
int left = 0;
int rihgt = nums.length;
int middle = 0;
while(left<right){
middle = left+(right-left)/2;
if(nums[middle]>=target) right = middle;
else left = middle+1;
}
//有可能所有数都小于target,避免出现数组越界问题
if(right == nums.length) return -1;
//有可能所有数都大于target
return nums[right] == target ? right : -1;
}
解决“寻找有序序列第一个满足某条件的元素的位置”问题的固定模板
//二分区间左闭右闭
public int solve(int left,int right){
int mid;
while(left<right){
mid = left+(right-left)/2;
if(满足条件){
right = mid;
}else{
left = mid+1;
}
}
return left;
}
3.2 局部最小值
给出一个长度为n随机无序数组nums,数组中相邻元素不能相等,返回任意一个局部最小值下标
- 局部最小值的定义:
- 如果nums[0]<nums[1],则nums[0]是一个局部最小值
- 如果nums[n-1]<nums[n-2],则nums[n-1]是一个局部最小值
- 如果nums[i]<nums[i-1] && nums[i]<nums[i+1],则nums[i]是一个局部最小值
- 如果数组边界部分不满足局部最小值定义,说明数组头部呈下降趋势,数组尾部呈上升趋势,则数组中间一定存在转折的局部最小值点。
- 使用二分查找,左边界left,右边界right,找到位于中点下标的数nums[i]:
- 如果nums[i]<nums[i-1] && nums[i]<nums[i+1],则nums[i]是一个局部最小值,直接返回i
- 如果nums[i-1]<nums[i],说明i-1 → i 呈上升趋势,left~(i-1)之间存在局部最小值,搜索范围缩小
- 如果nums[i+1]<nums[i],说明i → i+1 呈下降趋势,(i+1)~right之间存在局部最小值,搜索范围缩小
- (如果nums[i-1],nums[i+1]都小于nums[i],则任选一边缩小即可)
- 终止条件:由于存在越界问题,二分查找应该在搜索范围只剩两个元素时停止 [left,right] → [right-1,right]
代码实现
public int minIndex(int[] nums){
if(nums == null || nums.length == 0) return -1;
int n = nums.length;
if(n == 1) return 0;
int left = 0;
int right = n-1;
//当left == right-1停止,搜索范围内还有两个元素。如果数组一开始只有两个元素也不会进入循环
while(left<right-1){
int mid = left+(right-left)/2;
if(nums[mid]<nums[mid-1] && nums[mid]<nums[mid+1]) return mid; //情况1
else if(nums[mid-1]<nums[mid]) right = mid-1; //情况2、4
else left = mid+1; //情况3
}
return nums[left]<nums[right] ? left : right;
}
参考
代码随想录
labuladong
《算法笔记》
左程云系列视频
二叉查找从入门到入睡