实现思想:
二分查找是分治思想应用的一个典型例子,对于递归实现,可以画一个二分查找树来辅助理解,这棵动态生成树并不像归并排序那样需要全部遍历(前序),而是根据条件判定生成结点且仅遍历其中一个结点或者中止遍历。
【版本一】
直观递归(成功则返回查找元素key的下标,不成功则返回-1或者返回小于key的最大元素下标)
递归出口有两个,一个是查找失败则一直回溯,返回的-1是栈顶函数的-1; 一个是查找成功也是一直回溯,返回的mid也是栈顶函数的mid
return -1;前面的else可以略去,比如几次查找后成功从第一个if里面的某条分支中返回一个确定值,分析栈底函数,直接返回这个值后会结束函数而不会继续执行return -1,当然加上else更加清晰一些
这个版本递归思想很直白,我比较喜欢
int
bSearch(int a[], int l, int r, int key)
{
int mid;
if (l <= r) {
mid = l + (r-l)/2; //use "mid=(l+r)/2;" may overflow
if (key == a[mid])
return mid;
else if (key < a[mid])
return bSearch(l, mid, key);
else
return bSearch(mid+1, r, key);
}
else
return -1;
}
也是递归(成功则返回key下标,不成功返回-1)
原理就是把mid初始为-1,如果查找成功则返回的是第一个if语句里面的mid,即mid被修改为非-1;如果查找失败返回没有被修改过的mid或第一个if里面被修改过但最终修改为-1的mid。
语句return mid;前面不可以有else,假如有else,经过几次查找成功后,抵达栈顶函数时mid可能由第一个if里面第二条分支语句所赋值,如果有else则丢弃了mid值函数直接退出,因为此时else后的语句不会执行。
这个版本比【版本一】难理解
int
bSearch(int a[], int l, int r, int key)
{
int mid = -1;
if (l <= r) {
mid = l + (l-r)/2;
if (key == a[mid])
return mid;
else if (key < a[mid])
mid = bSearch(l, mid, key);
else
mid = bSearch(mid+1, r, key);
}
return mid;
}
【版本三】
迭代,这个最容易理解,功能也与【版本一】一样
int
bSearch(int a[], int l, int r, int key)
{
int mid;
while (l <= r) {
mid = l + (r-l)/2;
if (key == a[mid])
return mid;
else if (key < a[mid])
r = mid;
else
l = mid+1;
}
return -1;
}
几个解释:
1、为什么查找失败时不返回大于key的最小元素的下标呢?l不是刚好指到那里吗?
因为如果key > 有序表的最大元素,这时l只能指向小于key的最大元素,而其他时候l指向大于key的最小元素。所以返回小于key的最大元素下标,则任意情况下失败时的返回值,其意义都是统一的。
【更正】:
当采用[l, mid]和[mid+1, r]划分,且a[l] <= key <= a[r]时,如果查找不成功,那么l等于大于key的最小元素下标,l-1也就等于小于key的最大元素下标。
尤其注意前提条件有两个:一是采用[l, mid]和[mid+1, r]划分;二是满足a[l] <= key <= a[r]。
对于key<a[l]和key>a[r]的情况最好另外处理。
2、为什么当前发生a[mid] != key 时, 不查找[l, mid-1]或[mid+1, r], 而是查找[l, mid]或[mid+1, r]?
对于第一种划分,若仅实现查找成功返回key的下标失败返回-1则是正确的,但是不能返回小于key的最大元素下标,比如查找1 3 5 9 11,查找6失败指向9,查找10失败也指向9,即失败时(r == l)时l指向的元素与key的大小关系不是确定的。因此用后者可以同时实现二分查找失败时返回小于key的最大元素下标或大于key的最小元素下标。注意key的范围。
3、为什么不使用mid = (l+r)/2?
防止(l+r)时溢出,改成l/2+r/2没问题,但不如l+(r-h)/2效率好。当然这里已经假定函数接受的参数是合法的,所有没有作参数检查。但是上述情况是合法的参数调进却可能发生溢出,这就是算法实现有bug了。
补充 :今天做题时才发现自己沙茶了,既然key可能大于a[l],也可能小于a[r]
现在的问题是, 给定整数S、E,满足S < E,如何在有序表中找出下标距离最大的两个元素X,Y,满足S<=X<Y<=E?
可以分5类情况:
设X=a[left],Y=a[right],则有——
若E<a[l] || S>a[r], 则找不到;
若S<=a[l] && a[r]<=E,则left = l; right = r;
若a[l]<=S && E<=a[r],则left = i, 第一个a[i]>=S; right = i-1, 第一个a[i]>E;
若S<=a[l] && a[l]<E<a[r],则left = l; right = i-1, 第一个a[i]>E;
若E>=a[r] && a[l]<S<a[r],则left = i, 第一个a[i]>S; right = r;
下面假定只考虑情况3——
1、如果用直接查找,对于左边,则查找到>=S时,存当前下标,对于右边,则查找到>E时,存前一个元素的下标
for (i = 0; a[i] < S, i++) NULL;
left = i;
for (i = 0; a[i] <= E, i++) NULL;
right = i-1;
2、用二分查找呢?
对情况3,有a[l] <= key <= a[r]
int
bSearch(int mark, int a[], int l, int r, int key)
{
int mid;
while (l <= r) {
mid = l+(r-l)/2;
if (key == a[mid])
return mid;
else if (key < a[mid])
r = mid;
else
l = mid+1;
}
if (mark == 0)
return l;//返回大于key的最小元素下标
else
return l-1;//返回小于key的最大元素下标
}
查找左边则调用
left = bSearch(0, a, l, r, S);
查找右边则调用
right = bSearch(1, a, l, r, E);