当前位置:嗨网首页>书籍在线阅读

05-二分搜索

  
选择背景色: 黄橙 洋红 淡粉 水蓝 草绿 白色 选择字体: 宋体 黑体 微软雅黑 楷体 选择字体大小: 恢复默认

2.1.3 二分搜索

有一种搜索方法比查看每个元素速度快,但需要提前了解数据结构的顺序。如果我们知道某数据结构已经排过序,并且可以通过数据项的索引直接访问其中的每一个数据项,就可以执行二分搜索(binary search)。根据这一标准,已排序的Python List 是二分搜索的理想对象。

二分搜索的原理如下:查看一定范围内有序元素的中间位置的元素,将其与所查找的元素进行比较,根据比较结果将搜索范围缩小一半,然后重复上述过程。下面看一个具体的例子。

假定有一个按字母顺序排列的单词 List ,类似于 ["cat", "dog", "kangaroo", "llama", "rabbit", "rat", "zebra"] ,要查找的单词是“rat”。

(1)可以确定在这7个单词的列表中,中间位置的元素为“llama”。

(2)可以确定按字母顺序“rat”将排在“llama”之后,因此它一定位于“llama”之后的一半(近似)列表中。(如果在本步中已经找到“rat”,就应该返回它的位置;如果发现所查找的单词排在当前的中间单词之前,就可以确信它位于“llama”之前的一半列表中。)

(3)可以对“rat”有可能存在其中的半个列表再次执行第1步和第2步。这半个列表事实上就成了新的目标列表。这些步骤会持续执行下去,直至找到“rat”或者当前搜索范围中不再包含待查找的元素(意味着该单词列表中不存在“rat”)。

图2-3展示了二分搜索的过程。请注意,与线性搜索不同,它不需要搜索每个元素。

15.png

图2-3 最坏情况下,二分搜索仅会遍历lg(n)个列表元素

二分搜索将搜索空间不停地减半,因此它的最坏情况运行时间为O(lg n)。但是这里还有个排序问题。与线性搜索不同,二分搜索需要对有序的数据结构才能进行搜索,而排序是需要时间的。实际上,最好的排序算法也需要O(n lg n)的时间才能完成。如果我们只打算运行一次搜索,并且原数据结构未经排序,那么可能进行线性搜索就比较合理。但如果要进行多次搜索,那么用于排序所付出的时间成本就是值得的,获得的收益来自每次搜索大幅减少的时间成本。

为基因和密码子编写二分搜索函数,与为其他类型的数据编写搜索函数没什么区别,因为同为 Codon 类型的数据可以相互比较,而 Gene 类型只不过是一个 List 。具体代码如代码清单2-7所示。

代码清单2-7 dna_search.py(续)

def binary_contains(gene: Gene, key_codon: Codon) -> bool:
    low: int = 0
    high: int = len(gene) - 1
    while low <= high:  # while there is still a search space
        mid: int = (low + high) // 2
        if gene[mid] < key_codon:
            low = mid + 1
        elif gene[mid] > key_codon:
            high = mid - 1
        else:
            return True
    return False

下面就逐行过一遍这个函数。

low: int = 0
high: int = len(gene) - 1

首先看一下包含整个列表( gene )的范围。

while low <= high:

只要还有可供搜索的列表范围,搜索就会持续下去。当 low 大于 high 时,意味着列表中不再包含需要查看的槽位(slot)了。

mid: int = (low + high) // 2

我们用整除法和在小学就学过的简单均值公式计算中间位置 mid

if gene[mid] < key_codon:
    low = mid + 1

如果要查找的元素大于当前搜索范围的中间位置元素,我们就修改下一次循环迭代时要搜索的范围,方法是将 low 移到当前中间位置元素后面的位置。下面是把下一次迭代的搜索范围减半的代码。

elif gene[mid] > key_codon:
    high = mid - 1

类似地,如果要查找的元素小于中间位置元素之前,就将当前搜索范围反向减半。

else:
    return True

如果当前查找的元素既不小于也不大于中间位置元素,就表示它就是要找的元素!当然,如果循环迭代完毕,就会返回 False ,以表明没有找到,这里就不重现代码了。

下面可以尝试用同一份基因数据和密码子运行函数了,但必须记得先进行排序。具体代码如代码清单2-8所示。

代码清单2-8 dna_search.py(续)

my_sorted_gene: Gene = sorted(my_gene)
print(binary_contains(my_sorted_gene, acg))  # True
print(binary_contains(my_sorted_gene, gat))  # False

提示  可以用Python标准库中的 bisect 模块构建高性能的二分搜索方案。