持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第21天,点击查看活动详情
二进制搜索和利用假设
回到实现搜索(L,e)的问题,θ(1en(L))是我们能做的最好的吗?是的,如果我们对列表中元素的值的关系以及它们的存储顺序一无所知。在最坏的情况下,我们必须查看L中的每个元素以确定L是否包含e。
但是,假设我们知道一些关于元素存储顺序的信息,例如,假设我们知道我们有一个按升序存储的整数列表。我们可以更改实现,以便搜索在到达大于其正在搜索的数字时停止,如图 12-2 所示。
这将改善平均运行时间。但是,它不会改变算法的最坏情况的复杂性,因为在最坏的情况下,会检查L的每个元素。
然而,通过使用一种算法,二进制搜索,我们可以在最坏情况的复杂性方面得到相当大的改进,该算法类似于第3章中使用的对等搜索算法,以找到浮点数平方根的近似值。在那里,我们依赖于这样一个事实,即浮点数上有一个固有的总排序。在这里,我们依赖于列表已排序的假设。
这个想法很简单:
-
选择一个索引 i,将列表 L 大致分成两半。2.询问
-
如果没有,请询问L[i]是大于还是小于e。
-
根据答案,在 L 的左半部分或右半部分搜索 e。
考虑到该算法的结构,最直接的二进制搜索实现使用递归也就不足为奇了,如图 12-3 所示。
图 12-3 中的外部函数 search(L, e) 与图 12-2 中定义的函数具有相同的参数和规范。规范说,实现可以假设L按升序排序。确保满足此假设的责任在于搜索调用方。如果假设未得到满足,则实现没有义务表现良好。它可以工作,但它也可能崩溃或返回不正确的答案。是否应修改搜索以检查是否满足假设?这可能会消除错误的来源,但它会破坏使用二进制搜索的目的,因为检查假设本身需要o(1en(L))时间。
搜索等函数通常称为包装函数。该函数为客户端代码提供了一个很好的接口,但本质上是一个不做认真计算的传递。相反,它使用适当的参数调用帮助器函数 bsearch。这就提出了一个问题,为什么不取消搜索并让客户直接致电bin_search?原因是参数 1ow 和 high 与在列表中搜索元素的抽象无关。它们是实现细节,应该对那些编写调用搜索的程序的人隐藏。
现在让我们分析一下bin_search的复杂性。我们在上一节中展示了列表访问需要恒定的时间。因此,我们可以看到,排除递归调用,bsearch 的每个实例都是 θ(1)。因此,bin_search的复杂性仅取决于递归调用的数量。
如果这是一本关于算法的书,我们现在将使用一种称为递归关系的东西进行仔细的分析。但是,由于事实并非如此,我们将采取一种不那么正式的方法开始与问题“我们如何知道程序终止?回想一下,在第3章中,我们问了关于 while 循环的相同问题。我们通过为循环提供递减函数来回答这个问题。我们在这里做同样的事情。在此上下文中,递减函数具有以下属性:
它将形式参数绑定到的值映射到非负整数。
当其值为 o 时,递归终止。
对于每个递归调用,递减函数的值在进入进行调用的函数实例时小于递减函数的值。
bin_search的递减函数为高-低。搜索中的 if 语句确保此递减函数的值在第一次调用 bsearch 时至少为 o (递减函数属性 1)。
当输入bin_search时,如果 high-1ow 正好是 o,则该函数不进行递归调用 - 只需返回值 L[low]
==
e(满足递减函数属性 2)。
函数 bin_search 包含两个递归调用。一个调用使用覆盖中间左侧所有元素的参数,另一个调用使用覆盖中间右侧所有元素的参数。在任何一种情况下,高-低的值都会减半(满足递减函数属性 3)。
我们现在明白了递归终止的原因。接下来的问题是,在高低== 0之前,高低值可以减半多少次?回想一下,1ogy(x) 是 y 必须乘以自身才能达到 x 的次数。相反,如果 x 除以 y 个 logy(x) 次,则结果为 1。这意味着在达到o之前,使用最高-最低分割最多可以使用1og(高-低)时间将高低切成两半。
最后,我们可以回答这个问题,二进制搜索的算法复杂性是什么?由于当搜索调用 bsearch 时,高-低的值等于 len(L)-1,因此搜索的复杂度为 θ(1og(1en(L))).73