实现二进制搜索有哪些陷阱?

时间:2009-02-02 18:35:44

标签: algorithm binary-search

二进制搜索比它看起来更难实现。 “尽管二元搜索的基本思想相对简单,但细节却令人惊讶地难以理解......” - 唐纳德克努特。

最有可能将哪些错误引入新的二进制搜索实现?

7 个答案:

答案 0 :(得分:58)

答案 1 :(得分:29)

以下是我能想到的一些内容:

    确定下一个间隔的边界时,
  • 一个错误
  • 重复项目的处理,如果您想要返回数组中的第一个相等项目,而是返回后续相等的项目
  • 计算索引时,
  • 数字下溢/溢出,包含巨大的数组
  • 递归 vs 非递归实施,您应该考虑的设计选择

这些是你的想法吗?

答案 2 :(得分:14)

Read this。在任何人发现它之前,Java的二进制搜索实现隐藏了一个错误近十年。

错误是整数溢出。它没有引起人们的问题,因为几乎没有人在搜索足够大的数据结构。

答案 3 :(得分:7)

如果你附近有编程珍珠书,你应该查看第4章。

edit2:正如评论中所指出的,您可以下载我提到作者网站的章节草稿:http://www.cs.bell-labs.com/cm/cs/pearls/sketch04.html

答案 4 :(得分:2)

人们无法正确实现二进制搜索的一个重要原因是我们没有很好地表征二进制搜索,这是一个明确定义的问题但通常没有定义它好。

一个普遍的规则是从失败中学习。在这里,思考'无效'案件有助于澄清问题。如果输入为空怎么办?如果输入包含重复项怎么办?我应该通过一次条件测试或每次迭代的两次测试(以及提前终止的额外测试)来实现它吗?和其他技术问题,如计算指数中的数字溢出/下溢,以及其他技巧。

正如@Zach Scrivena指出的那样,通过对问题进行特征描述可以避免的错误是一个一个错误,以及处理重复的项目。

许多人将二进制搜索视为在给定排序数组的情况下查找目标值。 这是二进制如何使用,而不是二进制搜索本身。

我会尝试给出一个相对严格的二进制搜索定义/表达方式,并通过符合一个特定的方法显示一种方法来解决一个错误和重复问题方法,当然不是新的。

# (my) definition of binary search:
input: 
    L: a 'partially sorted' array, 
    key: a function, take item in L as argument
prerequisite: 
    by 'partially sorted' I mean, if apply key function to all item of L, we get a 
    new array of bool, L_1, such that it can't be partitioned to two left, right blocks, 
    with all item in left being false, all item in right being true. 
    (left or/and right could be empty)
output: 
    the index of first item in right block of L_1 (as defined in prerequisite). 
    or equivalently, the index of first item in L such that key(item) == True

这个定义自然地解决了重复的问题。

通常有两种表示数组的方法,[]和[),我更喜欢后者,[)方法的等价方法是使用(start,count)对。

# Algorithm: binary search
# input: L: a 'partially sorted' array, key: a function, take item in L as argument
    while L is not empty:
        mid = left + (right - left)/2  # or mid = left + count/2
        if key(mid item) is True:
            recede right # if True, recede right
        else:
            forward left # if False, forward left
    return left

因此,如果您将"如果为True,则退回(结束)" "如果为False,则转发(开始)" 部分正确,你差不多完成了。我称之为" FFTR规则"二元搜索。如果要找到第一个项目,如上面的定义,左边将是开始,但是如果你要找到最后一个项目,则右边将开始。 我遵循[)方式,然后可能的工具是,

while left<right:
    mid = left + (right - left)/2
    if key(L(mid)) == True:
        right = mid
    else:
        left = mid+1
    return left

让我们进一步验证,首先显示收敛,然后显示正确性。

<强>收敛:

whenever left == right, we exit loop (also true if being empty at the first)

so, in the loop, if denote, 

    diff = (right - left)/2, 

    lfstep = 1 + diff/2, 'lfstep' for 'left index forward step size'

    rbstep = diff - diff/2, 'rbstep' for 'right index back (recede) step size'

it can be show that lfstep and rbstep are alway positive, so left and right 
will be equal at last. 

both lfstep and rbstep are asymptotically half of current subarray size, so it's 
of logarithm time complexity.

<强>正确性:

if the input array is empty:
    return the left index;
else:
    if key(mid item) is true:
        "recede right"
        so mid and all item after mid are all true, we can reduce the search range 
        to [left, mid), to validate it, there are two possible cases,
        case 1:
            mid is the first item such that key(item) is True, so there are no true items 
            in new search range [left, mid), so the test will always be false, and we 
            forward left at each iteration until search range is empty, that is  
            [finalleft,mid), since we return finalleft, and finalleft == mid, correctly done!
        case 2:
            there are item before mid such that key(item) is True,
            in this case we just reduce to a new problem of smaller size
    else:
        "forward left"
        mid and all item before mid is false, since we are to find the first true one, 
        we can safely reduce to new search range [mid+1, right) without change the result.

等效(开始,计数)版本,

while count>0:
    mid = start + count/2
    if key(L[mid]) == True:
        right = mid
    else:
        left = mid+1
return left

总结,如果符合[)惯例,

1. define your key function of your problem, 
2. implement your binary search with "FFTR rule" -- 
    "recede (end) if True ( key(item) == True) else forward (start)" 
    examples:
        if to find a value target, return index or -1 if not found,
        key = lambda x: x>=target, 
        if L[found_index] == target: return found_index
        else: return -1

至于通过额外测试提前终止,我认为不值得付出,但你可以试试。

答案 5 :(得分:1)

在计算两个索引之间的中点时,没有考虑到将高值和低值相加可能会导致整数溢出。

Reference

答案 6 :(得分:-1)

现代处理器的流水线架构更适合进行线性搜索,而不是进行具有大量决策和分支的二进制搜索。

因此,当一个简单的线性搜索时,常见的性能错误过早优化(你知道,这些是所有邪恶的根源)正在使用二进制搜索更快,当然更简单。

根据读取的数量,即使对数据进行排序也会使事情变慢。对于简单的键(例如32位整数),线性和二进制之间的收支平衡点很容易达到1000个元素。