二分区间可以是左闭右开,或者都闭也行。
如果是都闭,判断条件是while(L<R)。如果是L+1,那么就需要mid=L+(R-L)/2,靠近L一点。如果是R-1,就注意要mid=L+(R-L+1)/2,靠近R一点。这样做是为了避免出现区间[a,a+1]出现死循环问题。
总之二分的条件很灵活,判断条件也可以是L<R-1,L和R也可以都等于mid,还是要看情况,只要避免死循环就行了。
考虑一个问题:给定正序排列的数组,A,有 n 个元素,找出不大于 M 的元素中最大的元素的下标。
这个问题要考虑以下几个边界条件:
1.不大于 M 的元素中最大的元素可能就是 M
2.不大于 M 的元素中最大的元素不是M
另外,还有隐形的边界问题!!!
1.查找何时结束。在二分查找中,一般是以等于查找元素或者待查找的部分为空作为结束条件。在这个问题中,实际上也是以为这两个条件的或作为结束条件。
2.查找的过程中,设当前查找部分为 i ~ j,我们用 A[(i + j)/2]去定位下次应该在左边部分,还是右边部分查找。
但是这个思想忽略了一种情况,我们设想一下,
如果现在等查找的部分是下标为13 ~ 17 的元素,
值是 11,23,44,100,200, 令 M = 70;
此时 A[mid] = A[(13 + 17)/2] = A[15] = 44,
按上面的思想,下一次查找的范围是 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 ,那么,它就是一个潜在的答案,我们只需要保存下来,并且去更新就行了,最终一定可以得到答案。
最后,还有一个问题。
查找结束的条件,真的是“等于查找元素或者待查找的部分为空”吗?
while(i != j) 是正确的处理吗?答案是,不正确!
按代码的逻辑,可能出现在一堆大于 M 的元素中寻找结果。
假设现在还剩下两个元素 i ~ j ,值是 100,200。
A[mid] 必将大于 M,因为这一堆元素都比 M 大。
mid = (i + j)/2 = i,下一次查找的范围是 i ~ mid - 1,即 i ~ i-1,此时,应该退出查找,因为查找的部分为负范围,是无效的范围。
因此,正确的代码是将上面那句改成: while(i <= j)。
毫无疑问,上面的第 2 种处理效率更高,因为它能避开很多无谓的查找,因此,最终给出一个效率稍高的实现。