二分查找有N多种写法,有各种各样的形式,选一种合适自己,自己能够理解记住的才是最好的。
对于最最最原始的二分查找,即给出一个Key,查找出key值的索引号。
最经典的二分查找对返回来的索引号没有任何要求,只要其对应的值是Key就可以了!!!!
使用二分查找算法的序列必须是有序序列
核心代码:
int binarySearch(int a[],int key,int left,int right)
{
while(left+1 < right)
{
int mid=(left+right)/2;
if (a[mid]==key) {return mid;}
else if (a[mid]<key)
{
left=mid;
}
else {
right=mid;
}
}
if ( a[left]==key) return left;
if ( a[right]==key) return right;
return -1;
}
很多版本不同的地方其实就是在于while循环条件的判断和left(low、l)、right(high、hi)和mid的关系。
比较常见的有:
int binarySearch(int a[],int key,int left,int right)
{
while(left<=right)
{
int mid=(left+right)/2;
if (a[mid]==key) {return mid;}
else if (a[mid]<key)
{
left=mid+1;
}
else {
right=mid-1;
}
}
return -1;
}
其实!!!这里while循环里面是什么取决于left、right是怎么移动的。
如果是:
left=mid+1
right=mid-1
那么最后停止的条件就是应该是left>right。也就是说循环的条件应该是left<=right。
如果是
left=mid
right=mid
那么停止的条件就是left+1>right。因为在这种情况下,当循环到right-left=1时(两个指针差相邻),两者再计算mid时就永远保持一样的right和left了。
经典的二分查找了解了,下面就是二分查找的各种“变形”
1.如果序列存在重复的Key值,返回Key值的最小下标,不存在则返回-1。
其实,对于这个问题(包括下面的这些问题),处理点都只是在一个地方,把握好这个地方后所有问题都可以迎刃而解!!!
首先,这个问题不同于经典二分查找的地方在于,在经典二分查找中,我们一旦找到
if (a[mid]==key) {
return mid;
}
我们就return 了,也就是说程序就结束了!!!
但是现在的问题是key在序列中有重复的,也就是说我们需要继续找,不能停止!!!
我们再来对比经典二分查找还需要改变什么:所谓二分查找核心其实就是:
if (a[mid]<key)
{
left=mid;
}
else {
right=mid;
}
这个是核心,这个不能变。
上面提到既然找到一个key值后不能return ,
那 a[mid]==key 应该放在哪判断??
也就是说等号放哪。等号是跟着 a[mid]< key,还是a[mid]>key???
结论:
如果是a[mid]<=key里的时候是找最大的下标。
如果是a[mid]>=key里的时候是找最小的下标。
具体看代码:
下面这个是把=号(a[mid]==key)放在了a[mid]> key里。所以实现的是找最小的下标。
int binarySearch(int a[],int key,int left,int right)
{
while(left+1 < right)
{int mid=(left+right)/2;
if (a[mid]<key)
{
left=mid;
}
else {
right=mid;
}}
if (a[left]==key) return left;
if (a[right]==key) return right;
return -1;
}
这是为什么呢?其实理由很好理解,越大的下标值越靠后,而当我们碰到a[mid]==key时说明我们找到一个key值,但是不确定是不是最后一个,所以我们需要做的是在这个key之后的值中寻找,也就是把left指针移后来。
同理,当我们希望得到最小下标值时,就需要把right指针移到左边来。这样才能不断往小下标靠拢。
那没碰到a[mid]==key时怎么办??按照二分查找的思路继续走。
另外,我们需要有一个认识:二分查找最后的结果往往是把key值落在left和right指针所在的区间里,下面举个例子:
比如对于数组a[8]={1,2,5,7,9,12,14,15},查找8时,用上面的移动方法(left=mid,right=mid,而不是left=mid-1,right=mid-1)。我们可以得到在结束while循环时,left一定是指在7处,right一定是指在9处。
再比如a[8]={1,2,5,7,9,12,14,15},查找2时,left和right指在哪?可以肯定的是只有两种情况:left->1 right->2 或者left->2 right->5。
到底是哪一种,取决于a[mid]==key放在哪处。
根据前面的,当是:
if (a[mid]<=key)
{
left=mid;
}
else {
right=mid;
}
一定是:left->2 right->5
当是:
if (a[mid]<key)
{
left=mid;
}
else {
right=mid;
}
一定是 left->1 right->2。
对于查找2时有两种情况,而8只有一种情况正是由于a[mid]==key引起的不同。
当然,如果查找一个比数组最小值还小的值,比如对于 a[8]={1,2,5,7,9,12,14,15}找-1,最后left,right分别指在1,2。如果是找100,left->14 right->15。
2.如果序列存在重复的Key值,返回Key值的最大下标,不存在返回-1。
int binarySearch(int a[],int key,int left,int right)
{
while(left+1 < right)
{
int mid=(left+right)/2;
if (a[mid]<=key)
{
left=mid;
}
else {
right=mid;
}
}
if (a[right]==key) return right;
if (a[left]==key) return left;
return -1;
}
特别需要注意的是,在得到left和right后在单独处理。注意right和left返回的次序。
3.返回大于Key值的最小下标。
这句话刚开始读了几次也没读明白,其实就是比如有
a[8]={1,2,5,7,9,12,14,15}。希望返回大于7的最小下标,也就说说返回9的下标4。
其实这个问题很好解决,我们只要返回7的最大下标然后+1就可以。
具体来说用上面2的算法。
这里需要注意的是!!!! 我们用的是:返回7最大下标的算法去获得大于7最小的下标。
int binarySearch(int a[],int key,int left,int right)
{
while(left+1 < right)
{
int mid=(left+right)/2;
if (a[mid]<=key)
{
left=mid;
}
else {
right=mid;
}
}
if (a[left]>key) return left;//考虑到比数组里最小的还小
if (a[right]>key) return right;
return -1;
}
4.返回小于Key值的最大下标。
同样沿用3的思路。
5.返回大于等于Key值的最小下标。
这里单独把“等于”从4里面拿出来是因为循环结束需要单独处理的部分不太一样。
int binarySearch(int a[],int key,int left,int right)
{
while(left+1 < right)
{
int mid=(left+right)/2;
if (a[mid]<=key)
{
left=mid;
}
else {
right=mid;
}
}
if (a[left]>=key) return left;
if (a[right]>=key) return right;
return -1;
}
6.返回小于等于Key值的最大下标。
与5类似。
总结-1
其实不管是哪一种情况,也不管是什么样新的需求。把经典的二分查找弄明白后,其他的地方都是很好理解。特别就是注意到上面已经提到过的一个东西:
二分查找(left=mid,right=mid,这种更新方式)完之后left和right都是指在包含key的区间处的(当然这里不考虑比数组最小的还小和比数组最大的还大的两种情况。其余情况key总是落在left和right所指的值之间)。特别需要单独考虑的就是key值在序列里存在时有两种可能的情况,这个需要根据需求进一步来确定是哪一种。
除了经典的情况,数组{1,2,5,7,9,12,14,15}:
得到大于8的最小下标值,最终:left->7,right->9
得到小于8的最大下标值,最终: left->7,right->9
得到大于5的最小下标值,最终:left->5,right->7
得到小于5的最大下标值,最终: left->2,right->5
数组{1,2,5,7,9,9,9,9,9,9,9,12,14,15}:
得到等于9的最小下标值,最终:left->7,right->9
得到等于9的最大下标值,最终: left->9,right->12
二分查找复杂度
时间复杂度是O(logn)。
证明很容易,每次折半,假设有n个元素n,n/2,n/4…..n/2^k。假设最坏的时候折半了k次时当只剩一个元素,则有n/2^k=1 ===>k=log(n)
空间复杂度:
递归空间复杂度=递归的深度×辅助空间的大小
二分查找递归时辅助空间为常数,递归深度了log(n),所以空间复杂度log(n)
非递归的情况下:
二分查找的空间复杂度为常数O(1)