6种二分查找及其变式总结及牢记方法
假设数组为:int arr[] = { 1,2,3,3,4,5,5,8,10,12 };(数组从小到大排序)
int n=sizeof(arr)/sizeof(int)-1;
先从最开始的两个二分查找入手,通过细节变形而总结出6种二分查找:
- 查找第一个等于key的元素(或 是第一个大于等于key的元素 或 最后一个小于key的元素)
我们首先想一下,当 arr[mid]==key 的时候,我们有两个策略:
1、left=mid+1
2、right=mid-1
我们注意到:此时left~mid这个左半边都是<=key的,如果我们取left=mid+1则可能会丢失第一个等于key的元素,故我们需要选择第二个策略,这样可以保证在压缩区间的情况下,保留第一个等于key的元素位置。
所以有:
if(arr[mid]==key)
right=mid-1;
我们可以与当arr[mid]>key时合并为while循环框架:
if (arr[mid] >= key)
right = mid - 1;
else
left = mid + 1;
区间表示完后,接下来分析返回值的问题,同样有两个策略:
1、第一个等于key的下标为left
2、第一个等于key的下标为right
我们想一下,之前的区间压缩相当于是这样的: 一开始left是在left=mid+1压缩,当left一直压缩到arr[left]==key时,那么此时left的右边全是>=key了,说明这时候的left所指向的是第一个>=key的元素(left从左往右直到第一个等于key,此时left右边全是>=key可能值了,那left不就是指向第一个<=key的元素么),这就导致之后的mid只会满足arr[mid]>=key,从而只执行right=mid-1。
这就可以理解为: 当left一直向右靠拢直到left为第一个等于key的下标时,left将不会再向中间靠拢,此时相当于left固定,right继续向中间靠拢,就会出现下面的例子:
这时候左边界arr[left]已经是等于key值了,这时left所指向的是第一个>=key的元素,不需要再往中间压缩,之后中间只是有arr[mid]>=key的情况发生
上图的mid=(2+3)/2,int类型取整,所以mid=2 (注意:mid是下标位置,例子从0开始)
这里是常说的bug,也就是为什么while循环控制的是(left<=right)而不是(left<right)。
当left等于right时,left或right还必须往“中间”移动否则会出现死循环或导致结果不正确
left>right,while循环退出。我们现在可以清晰地看到:right所指向的是最后一个小于key的元素,而left就是刚刚所说的,指向的是第一个大于等于key的元素。
所以 通过这个循环框架(即上面第二个代码框) 得到:
1、left最后所指向的是第一个大于等于key的元素
2、right最后指向的是最后一个小于key的元素
这样我们可以清晰地解决3个二分查找了,循环框架相同,返回的不同而已。
1. 查找第一个等于key的元素
int a(int key)//查找第一个等于key的元素
{
int left = 0, right = n, mid;
while (left <= right)
{
mid = (left + right) >> 1;
if (arr[mid] >= key)
right = mid - 1;
else
left = mid + 1;
}
if (left <= n && arr[left] == key)//这里&&的左半边是为了防止找不到key而超边界
//右半边是由于:left指向的是第一个大于等于key的元素,而我们这里只需要第一个等于key的元素。当然如果取到的是大于,我们就需要返回-1(未找到)了
return left;
else
return -1;
}
2. 查找第一个大于等于key的元素
int e(int key)//查找第一个等于或大于key的元素
{
int left = 0, right = n, mid;
while (left <= right)
{
mid = (left + right) >> 1;
if (arr[mid] >= key)
right = mid - 1;
else
left = mid + 1;
}
return left;
}
3、查找最后一个小于key的元素
int d(int key)//查找最后一个小于key的元素
{
int left = 0, right = n, mid;
while (left <= right)
{
mid = (left + right) >> 1;
if (arr[mid] >= key)
right = mid - 1;
else
left = mid + 1;
}
return right;//与上面的返回值不同而已
}
我是分界线分界线分界线分界线分界线分界线分界线分界线分界线分界线分界线分界线分界线
我是分界线分界线分界线分界线分界线分界线分界线分界线分界线分界线分界线分界线分界线
-查找最后一个等于key的元素(或 最后一个小于等于key的元素 或 第一个大于key的元素)
重回例子:int arr[] = { 1,2,3,3,4,5,5,8,10,12 };
同样,在arr[mid]==key时考虑两个策略:
1、left=mid+1
2、right=mid-1
我们注意到:此时mid~right这个右半边都是>=key的,如果我们取right=mid-1则可能丢失最后一个等于key的元素,故我们选择第一个策略,这样可以保证在逐渐压缩区间的情况下,保留最后一个等于key的元素位置。
所以有:
if(arr[mid]==key)
left=mid+1;
我们可以与arr[mid]<key时合并为while框架:
if (arr[mid] <= key)
left = mid + 1;
else
right = mid - 1;
区间表示完后,接下来分析返回值的问题,同样有两个策略:
1、最后一个等于key的下标为left
2、最后一个等于key的下标为right
我们想一下,我们的区间压缩大概是这样的:right逐渐在right=mid-1压缩,直到出现arr[right]==key时,此时right左边全都是<=key的了,说明right所指向的是第一个<=key的元素 (应该容易理解吧,right从右边往左直到第一个等于key就停止,左边都是小于<=key,那不就是第一个<=key的元素啦) 这样之后的mid只会满足arr[mid]<=key,从而只执行left=mid+1。
相当于right固定,left从左往右向中间靠拢,例子如下~~
同样,我们能看到:
1、left最后指向的是第一个大于key的元素
2、right最后指向的是最后一个小于等于key的元素
代码如下~~
4、查找最后一个与key相等的元素
int b(int key)//查找最后一个与key相等的元素
{
int left = 0, right = n, mid;
while (left <= right)
{
mid = (left + right) >> 1;
if (arr[mid] <= key)
left = mid + 1;
else
right = mid - 1;
}
if (right <= n && arr[right] == key)//同样,防溢出 和 排除arr[right]<key的可能
return right;
else
return -1;
}
5、查找最后一个等于或小于key的元素
int c(int key)//查找最后一个等于或小于key的元素
{
int left = 0, right = n, mid;
while (left <= right)
{
mid = (left + right) >> 1;
if (arr[mid] <= key)
left = mid + 1;
else
right = mid - 1;
}
return right;
}
6、查找第一个大于key的元素
int f(int key)//查找第一个大于key的元素
{
int left = 0, right = n, mid;
while (left <= right)
{
mid = (left + right) >> 1;
if (arr[mid] <= key)
left = mid + 1;
else
right = mid - 1;
}
return left;
}
总结:
4种变形其实都是由两种基本形式所变形而来的,只是说法或返回的left与right不同而已。望能牢记~~~
为了牢记而写,有错望指出~