二分搜索题总结
p划分问题
p划分问题转载自:这位大佬的 手把手教你写出正确的二分搜索。
这篇博客将二分搜索自成体系地总结了为了以下七个问题,以p划分作为问题共性,而且居然写出了证明,非常值得一看。
为了更好地理解p划分,我将这七个问题再以自己的方式总结一遍。
理解p划分
p划分指,此处有一个条件p,当条件p能将一个数组划分为两边,左半边和右半边刚好是相对条件p来说是对立的。这时,我们称这个数组是p划分的。
那么一般情况下,只有当数组有序时,才会出现p划分的情况。举例来说:在数组a=[1,2,3,4,5]中,如果以a[i]<3这个条件来划分的话,那么数组会被分成[1,2]和[3,4,5]。这样就完成了一个p划分。
那么p划分能解决什么样的问题呢?它能解决数组中的搜索问题,在数组中搜索时,如果能有一个条件迅速筛除数组的一部分,那么搜索的时间复杂度就会大大降低。
所以,其实p划分条件就是能满足二分搜索的划分条件,下面我们就来按照每个问题分析其在二分搜索中的条件p。注意,这七道问题,默认数组升序排列。
是否存在数字t
这是最常见最简单的二分搜索问题,只需根据数组中间的元素判断条件:a[mid]<t(其他条件如>,>=,<=也都可以,只要记住不同的条件对应不同的划分方式即可)
伪代码如下:
left=0; right=n-1;
// 预判一下t是否在a的数字范围中
if(t<a[left] || t>a[right]) return false;
while(left<right){
//这里写法是因为怕left+right溢出
int mid = left+((right-left)>>1);
if(a[mid]<t){
// 中间元素<t, 说明只有右半边数组可能存在t
left = mid+1;
}else{
//这种情况时 中间元素>=t, 说明左半边到mid可能存在t
right = mid;
}
return a[left]==target;
}
找到大于t的第一个数
找到大于t的第一个数,那么在划分时,也是要找到中间元素和t的关系。所以判断条件p:a[mid]>t
left=0; right=n-1;
// 如果right元素都<=t,那么数组中没有大于t的数
if(t>=a[right]) return -1;
while(left<right){
//这里写法是因为怕left+right溢出
int mid = left+((right-left)>>1);
if(a[mid]>t){
// 中间元素>t,
// 说明不含mid元素的右半边数组已经不可能有大于t的第一个数了,
// 所以只需找左半边到mid即可
right = mid;
}else{
//中间元素<=t, 说明只可能右半边才可能存在大于t的第一个数了
left = mid+1;
}
return a[left];
}
找到大于等于t的 第一个数
这个与上一道几乎一模一样,只要将判断条件p改为:a[mid]>=t,其余操作均相同。
left=0; right=n-1;
// 如果right元素都小于t,那么数组中的元素都小于t
if(t>a[right]) return -1;
while(left<right){
//这里写法是因为怕left+right溢出
int mid = left+((right-left)>>1);
if(a[mid]>t){
// 中间元素>t,
// 说明不含mid元素的右半边数组已经不可能有大于t的第一个数了,
// 所以只需找左半边到mid即可
right = mid;
}else{
//中间元素<=t, 说明只可能右半边才可能存在大于t的第一个数了
left = mid+1;
}
return a[left];
}
找到小于t的最后一个数
条件p:a[mid]<t
left=0; right=n-1;
// 如果left元素都大于等于t,那么数组中的元素都大于等于t,没有小于t的
if(a[left]>=t) return -1;
while(left<right){
//这里写法是因为怕left+right溢出
int mid = left+((right-left)>>1);
if(a[mid]<t){
// 中间元素<t,
// 说明左半边全部元素都小于t的,
// 那么只能在含mid的右半边找小于t的最后一个数
left = mid;
}else{
// 中间元素>=t,
// 说明右半边全部元素都>=t
// 所以只能在数组左半边找小于t的最后一个数了
right = mid-1;
}
return a[left]
}
找到小于等于t的最后一个数
这个与上一道几乎一模一样,只要将判断条件p改为:a[mid]<=t,其余操作均相同。
left=0; right=n-1;
// 如果left元素都大于t,那么数组中的元素都大于t,没有小于等于t的
if(a[left]>t) return -1;
while(left<right){
//这里写法是因为怕left+right溢出
int mid = left+((right-left)>>1);
if(a[mid]<t){
// 中间元素<t,
// 说明左半边全部元素都小于t的,
// 那么只能在含mid的右半边找小于t的最后一个数
left = mid;
}else{
// 中间元素>=t,
// 说明右半边全部元素都>=t
// 所以只能在数组左半边找小于t的最后一个数了
right = mid-1;
}
return a[left]
}
是否存在t,返回第一个t
这个问题可转化为找到大于等于t的第一个数。然后判断找到的那个数是否等于t即可。
是否存在t,返回最后一个t
这个问题可转化为找到小于等于t的最后一个数。然后判断找到的那个数是否等于t即可。
不知大家是否注意到,在这些问题中,只需判断a[mid]与t的关系即可划分数组,所以,不用死记硬背这些判断条件,只需用到的时候再思考即可。
扩展问题
在p划分问题中,只需一个判断条件和两种情况,即可将数组划分为两部分,从而进行二分搜索。那么在扩展问题中,是否还可以这样操作呢?
旋转数组问题
剑指offer 11. 旋转数组问题
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。
这个问题在初始条件上就打破了p划分的标准,数组不再有序,无法保证一个条件可以将数组拆成符合条件的一边和不符合条件的一边。所以在这样的数组上如何使用二分搜索呢?
这里我们考虑这样的几种情况,分析一下旋转数组的最小值有可能出现在数组的哪里。
- 分析最简单的情况:数组压根没有旋转。那么旋转数组的最小值只可能是第一个元素(或数组中后续的和第一个元素相等的元素)。
- 数组旋转了一部分。
那么第一种情况,最小值有可能在中间,例如:
该图画了旋转数组(3,4,5,1,2),显然肉眼可见,最小值为1.
这种情况是,左边部分较大,右边部分较小,最小值在中间。
那么我们比较中间值和右边值,判断下谁更小。
如果中间小,又已知中间到右边是逐渐递增的(旋转数组后依然分区有升序排列),那么我们选取左半边继续找最小值。
如果右边值小,那说明中间的那个最小值点一定在右半边,因为已知旋转数组的左边是要更大的。
我们再分析一种情况,对于旋转数组(3,4,5,1,2,2,2,2,2,2,2)
从图中可以发现,中间值和右边值是相等的,这种情况下怎么办呢?
比如,再举例极端一些的情况如(2,2,2,1,2)和(2,1,2,2,2,2,2)。 所以很遗憾,这种情况下就是无