考虑一个问题:给定正序排列的数组,A,有 n 个元素,找出不大于 M 的元素中最大的元素的下标。
这个问题要考虑以下几个边界条件:
1.不大于 M 的元素中最大的元素可能就是 M
2.不大于 M 的元素中最大的元素不是M
另外,还有隐形的边界问题
1.查找何时结束。在二分查找中,一般是以等于查找元素或者待查找的部分为空作为结束条件。在这个问题中,实际上也是以为这两个条件的或作为结束条件。
2.查找的过程中,设当前查找部分为 i ~ j,我们用 A[(i + j)/2]去定位下次应该在左边部分,还是右边部分查找。因此会有下面的代码:
while(...)
{
mid = (i + j) >> 1;
if (M == A[mid])
{
pos = mid; //记录找到的位置
break;
}
else if(M < A[mid])
{
j = mid - 1; //下次去左边部分查找
}
else
{
i = mid + 1; //下次去右边部分查找
}
}
范围是 16 ~ 17 元素。但我们看上面的值,它们都大于 M,因此,不可能在剩下的元素中找到不大于 M 的元素中最大的元素。很明显,说明上一次划分下一次查找部分的地方出错了。
我们先这样考虑,当 A[mid] < M时,将 mid 元素也纳入下一次查找的范围,因为 A[mid] 是小于 M的,这样就保证了划分到的范围必然能够找到解。
但如果我们考虑查找的部分是下标为 4 ~ 5 的元素,值是 4,7,令 M = 70,A[mid] = A[(4 + 5)/2] = A[4] < M,如果按上面的思路,我们将 A[mid] 纳入到下一次查找的范围,因此,下次查找的范围是 4 ~ 5,又是 4 ~5 !于是,按这个处理会陷入死循环!
所以,上面的思路是行不通的!
所以,当 A[mid] < M时,不能将 mid 元素纳入下一次查找的范围。(结论一)
我们再考虑 A[mid] > M 时,显然没有必要将 mid 元素纳入下一次查找的范围,因为我们查找的元素是小于 M的,没有必要将一个不可能成为结果的元素纳入查找。实际上,如果纳入查找,也可能形成死循环。读者可以自行举例。
所以,当A[mid] > M时,不能将 mid 元素纳入下一次查找的范围。(结论二)
因此,我们循环里面的执行流程结构是不会改变的,还是上面给出代码的结构。
由结论一,不将 mid 加入下次查找的范围,那么,下一次查找的范围的元素都是大于 M 的,此时,我们有两种处理手段:
1.让其继续寻找,依据结论一和结论二,我们避开了死循环的机会,因此,继续寻找最终必定会处理完成。
2.我们每次检查下次查找的范围的首个元素 A[mid + 1] (注意判断 mid + 1 是不是有效的下标),若 A[mid + 1] > M,则 mid 元素就是小于 M 的最大元素。若 A[mid + 1] = M ,直接返回结果。剩下的 A[mid + 1] < M 继续查找。
但是上面还没有回答一个问题,结果如何给出?
实际上,我们的处理过程,每次都可以找出 A[mid] < M ,那么,它就是一个潜在的答案,我们只需要保存下来,并且去更新就行了,最终一定可以得到答案。给出代码。
int maxLessThanEmePos(int M)
{
int pos = -1;
int mid;
int i = 0,j = ARRAYSIZE-1;
bool continue_ = true;
while(continue_)
{
mid = (i + j) >> 1;
continue_ = (!(i == j)); //一旦寻找的部分只有一个元素,只需要往后再寻找一次了
if (M == A[mid])
{
pos = mid;
break;
}
else if(M < A[mid])
{
j = mid - 1;
}
else
{
i = mid + 1;
pos = mid; //每个 A[mid] < M 都是一个满足条件的答案,且慢慢地变大
}
}
return pos;
}
最后,还有一个问题。
查找结束的条件,真的是“等于查找元素或者待查找的部分为空”吗?也就是上面代码中的,continue_ = (!(i == j)) 是正确的处理吗?答案是,不正确!
按代码的逻辑,可能出现在一堆大于 M 的元素中寻找结果。假设现在还剩下两个元素 i ~ j ,值是 100,200。A[mid] 必将大于 M,因为这一堆元素都比 M 大。mid = (i + j)/2 = i,下一次查找的范围是 i ~ mid - 1,即 i ~ i-1,此时,应该退出查找,因为查找的部分为负范围,是无效的范围。因此,正确的代码是将上面那句改成: continue_ = (!(i <= j))。
毫无疑问,上面的第 2 种处理效率更高,因为它能避开很多无畏的查找,因此,最终给出一个效率稍高的实现。
int A[ARRAYSIZE] = {1,2,3,4,5,7,24,44,100,200};
int maxLessThanEmePos(int M)
{
if(A[0] > M)
{
return -1;
}
else if(A[0] == M)
{
return 0;
}
else
{
//往后继续处理
}
int pos = -1;
int mid;
int i = 0,j = ARRAYSIZE-1;
bool continue_ = true;
while(continue_)
{
mid = (i + j) >> 1;
continue_ = (!(i >= j)); //一旦寻找的部分只有一个元素,只需要往后再寻找一次了
if (M == A[mid])
{
pos = mid;
break;
}
else if(M < A[mid])
{
j = mid - 1;
}
else
{
i = mid + 1;
if(A[i] > M)
{
pos = mid;
break;
}
else if(A[i] == M)
{
pos = i;
break;
}
else
{
pos = mid; //每个 A[mid] < M 都是一个满足条件的答案,且慢慢地变大
}
}
}
return pos;
}