二分查找法

311 阅读5分钟

基本理解

二分法的思想很简单,因为整个数组是有序的,数组默认是递增的。

首先选择数组中间的数字和需要查找的目标值比较
如果相等最好,就可以直接返回答案了
如果不相等:
如果中间的数字大于目标值,则中间数字向右的所有数字都大于目标值,全部排除
如果中间的数字小于目标值,则中间数字向左的所有数字都小于目标值,全部排除

二分法的实现要注意两边的范围,在数组中可以是[)也可以是[],左闭右开说明右边的取值是nums.length,左闭右闭说明右侧的取值为nums.length-1。

基本实现

以左闭右闭的情况书写:

public int search(int [] nums,int target){
//去除不在范围的情况
  if(nums[0] > target || nums[nums.length-1] < target){
    return -1;
  }
  //左右两点位置获取
  int left =0;
  int right = numslength-1;
 //由于是左闭右闭,所以当leftright相等的时候有意义,可以进行比较。
 //而如果是左闭右开,实际比的是leftright-1的内容,left = right没有意义。
  while(left <= right){
   int mid = left + (right - left)/2;
   //如果中间值小于target,说明中间节点及左侧都小于,左位置修改
   if(nums[mid] < target){
    left = mid+1;
  }
  //同理,排除右侧
  else if(nums[mid) > target){
  right = mid-1;
  }
  else if(nums[mid] == target){
  return mid;
  }
  }
}

变种

1、取到大于target的最小值

这种类型就是对该有序数组中查找到比目标值target大的值中的最小值。
在做这种题的时候我们要注意好我们的目标和循环不变量。
在该问题中如果nums[mid] < target,说明中间节点左侧都小于target,都排除
如果nums[mid] = target,说明中间节点等于target,由于我们要找的是大于target的最小值,所以排除。
如果nums[mid] > target, 说明中间节点的值大于target,所以mid右侧的值都大于target,我们要找最小的,所以mid后面的都不要。

综上,我们可以知道我们的判断代码部分如下:

if(nums[mid] <= target){
left = mid+1;}
if(nums[mid] > target){
right =mid;}

从上面的解析我们知道当muns[mid]的值小于等于target的时候不在我们查找的范围,也就是说不是解,所以直接全部去除。
而当nums[mid]的值大于target的时候,我们要保留mid的值,因为这种情况说明mid右侧的值都不合适了,而mid左侧的情况未知,可能mid就是大于target的最小值,所以要保留。

这种变种的取值范围是[0,nums.length],保留最后无值的标是因为最后可能没有大于目标值的数字,所以要返回一个值,即数组中最大的值都比target小,

2、取到小于target的最大值

和上面一样的分析方法,

if(nums[mid] < target){
left = mid;
}
if(nums[mid] >= target){
right = mid-1;
}

取值范围是[-1,nums.length-1].原因和上面一样,防止数组值都比目标值大的情况发生。 但这样子的写法有一个问题,不在于算法本身,而是计算机计算机计算取值出现的问题。 当l和r分别为0和1的时候,计算mid的公式:l + (r - l)/2。就是0 + (1-0)/2 =0。
因为计算机向下取整造成的这样的结果。 如果前2个元素为1,1。我们要找的值为2,那么l的值会从0变成0。这样就会导致死循环。这种情况发生的前提就是l和r相邻。我们可以单独判断这种情况进行特殊处理。也可以使用这种计算方法:mid = l+(r-l +1)/2;

为什么第一种变种没有影响?因为第一种方法右边界可能不变,而左边界一定会变,所以就没有这种问题,如果计算机是向上取整,那么情况就会反过来。变种2没问题,而变种1就有问题了。

模板

关键是理解我们要找的元素要满足的条件是什么。仔细分析范围的初始值,在循环过程中如何调整左右边界。

image.png

在b站评论区中看到的话: 我是在看了这篇文章,https://blog.[csdn]()**.net/groovy2007/article/details/78309120,里那句“关键不在于区间里的元素具有什么性质,而是区间外面的元素具有什么性质。”之后醍醐灌顶,建立了我自己的二分查找心智模型,和up主的有些类似。

也就是看最终左右指针会停在哪里。
如果我们要找第一个大于等于x的位置,那么我就假设L最终会停在第一个大于等于x的位置,R停在L的左边。
这样按照上面那句话,可以把循环不变式描述为“L的左边恒小于x,R的右边恒大于等于x”,这样一来,其他的各种条件就不言自明了。
比如循环条件肯定是L小于R,因为我假设R停在L的左边。
而L和R转移的时候,根据循环不变式,如果mid小于x,肯定要令L等于mid+1,如果大于等于x,就令R等于mid-1。
至于初始的时候L和R怎么选,也是看循环不变式,只需要保证初始L和R的选择满足“L的左边恒小于x,R的右边恒大于等于x”,并且不会出现越界的情况即可,L必为0,因为0左边可以看作负无穷,恒小于x,R取第一个一定满足条件的(防止mid取到非法值),例如n-1(n开始可以看作正无穷,恒大于等于x,如果保证x在数组里可以选择n-2,其实大于等于n也满足不变式,但是mid可能会取非法值),而且这样一来即使是搜索数组的某一段,也可以很方便根据这个条件地找到初始位置。

如果假设L最终会停在第一个大于等于x的位置,R停在L的位置,那么循环不变式就是“L的左边恒小于x,R以及R的右边恒大于等于x”,这样的话,循环条件就是L等于R的时候退出;转移的时候R=mid;初始时,一般取R=n(如果保证x在数组里,也可以取n-1)。

其他的情况也类似,比较直观的推导方法就是在要找的位置的分界处(比如在第一个大于等于x的位置后面)画一条线,然后假定L和R最终会停在这条线的左边还是右边,接着倒推各种条件即可。

二分查找【基础算法精讲 04】_哔哩哔哩_bilibili