您的位置:首页 > 其它

二分查找的边界问题

2014-07-25 12:48 190 查看
考虑一个问题:给定正序排列的数组,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 ,那么,它就是一个潜在的答案,我们只需要保存下来,并且去更新就行了,最终一定可以得到答案。给出代码。

最后,还有一个问题。

查找结束的条件,真的是“等于查找元素或者待查找的部分为空”吗?也就是上面代码中的,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 种处理效率更高,因为它能避开很多无畏的查找,因此,最终给出一个效率稍高的实现。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: