二分查找的细节

导读:本篇文章讲解 二分查找的细节,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

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时,能够得到一个唯一位置
  • 分析:
    1. 利用二分查找法找到target之后,不能马上返回当前middle下标,因为找到的这个target可能是第一个,也可能不是第一个,我们需要继续寻找。
    2. 如果当前target(nums[middle])不是第一个,那第一个target下标必然在middle左边,扫描区间右边界需要左移到middle处。
      right = middle;
    3. 如果当前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,数组中相邻元素不能相等,返回任意一个局部最小值下标

  • 局部最小值的定义:
    1. 如果nums[0]<nums[1],则nums[0]是一个局部最小值
    2. 如果nums[n-1]<nums[n-2],则nums[n-1]是一个局部最小值
    3. 如果nums[i]<nums[i-1] && nums[i]<nums[i+1],则nums[i]是一个局部最小值
  • 如果数组边界部分不满足局部最小值定义,说明数组头部呈下降趋势,数组尾部呈上升趋势,则数组中间一定存在转折的局部最小值点。
  • 使用二分查找,左边界left,右边界right,找到位于中点下标的数nums[i]:
    1. 如果nums[i]<nums[i-1] && nums[i]<nums[i+1],则nums[i]是一个局部最小值,直接返回i
    2. 如果nums[i-1]<nums[i],说明i-1 → i 呈上升趋势,left~(i-1)之间存在局部最小值,搜索范围缩小
    3. 如果nums[i+1]<nums[i],说明i → i+1 呈下降趋势,(i+1)~right之间存在局部最小值,搜索范围缩小
    4. (如果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
《算法笔记》
左程云系列视频
二叉查找从入门到入睡

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/75098.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!