Python中的二分查找(又称折半查找)

214

是否有一个库函数可以在列表/元组上执行二分搜索,并返回找到项的位置,如果未找到则返回“False”(-1、None等)?

我在bisect module中找到了bisect_left/right函数,但是即使在列表中没有该项,它们仍然会返回一个位置。这对于它们预期的使用完全没问题,但我只想知道一个项目是否在列表中(不想插入任何东西)。

我考虑使用bisect_left,然后检查该位置上的项目是否等于我正在搜索的项目,但是这似乎很麻烦(如果数字可能大于我的清单中最大的数字,我还需要进行边界检查)。如果有更好的方法,我想知道它。

编辑为了澄清我需要这个的原因:我知道字典非常适合这个任务,但我要尽可能地减少内存消耗。我的目的是创建一种双向查找表。我在表中有一列值,我需要能够根据它们的索引访问这些值。我还希望能够找到特定值的索引或None(如果该值不在列表中)。

使用字典将是最快的方法,但将(大约)加倍内存要求。

我问这个问题是因为我认为可能已经在Python库中忽略了一些东西。正如Moe建议的那样,看来我将不得不编写自己的代码。


1
你想要实现什么目标?如果值是唯一的,考虑使用集合和“if value in set: something”。 - Kirk Strauser
就这个问题而言,“-1”被认为是真的;“0”则为假。 - Glyph
3
我提到了-1,因为返回数组中搜索项的索引的函数可能已经返回0,所以如果未找到该项,则返回-1(类似于子字符串搜索)。 - rslite
3
如果您使用NumPy,np.searchsorted是一个有用的函数。http://docs.scipy.org/doc/numpy/reference/generated/numpy.searchsorted.html - Roman Shapovalov
在问题中,你说:“我想使用bisect_left,然后检查该位置的项是否等于我要搜索的内容,但这似乎很麻烦”。那么为什么你选择了做完全相同的事情(并进行边界检查)的答案呢? - Raymond Hettinger
22个回答

276

bisect_left函数用于在给定的有序范围内查找第一个可以插入元素并保持排序顺序的位置,如果x存在于该范围中,则其位置将是x的位置。如果p是“超出末尾”位置,则表示未找到x。否则,我们可以测试以查看是否存在x,以确定是否已找到x

from bisect import bisect_left

def binary_search(a, x, lo=0, hi=None):
    if hi is None: hi = len(a)
    pos = bisect_left(a, x, lo, hi)                  # find insertion position
    return pos if pos != hi and a[pos] == x else -1  # don't walk off the end

11
泛指二分查找算法也是如此。 - cubuspl42
1
降序呢? - Parikshit Chalke
请注意:您可以使用pos < hi来支持a =(),x = -1,lo = 1,hi = 0的情况(空范围的结果应为-1)。同样,hi = min(hi,len(a))以支持a =(0,),x = 0,lo = 0,hi = 2(结果应为0,而不是IndexError),并且类似于lo。鉴于二分搜索在边缘处很棘手,最好是明确的,例如,对不支持的lo,hi值引发ValueError。 - jfs

61

为什么不看一下bisect_left/right的代码,并进行适当修改以适应您的目的。

像这样:

def binary_search(a, x, lo=0, hi=None):
    if hi is None:
        hi = len(a)
    while lo < hi:
        mid = (lo+hi)//2
        midval = a[mid]
        if midval < x:
            lo = mid+1
        elif midval > x: 
            hi = mid
        else:
            return mid
    return -1

35
我最初点赞了这个,但现在得出结论这不是一件好事。如果按照这个答案操作,会造成很多重复代码,并且众所周知,二分查找很容易出错。 - abyx
10
它们是两个等效的变体,取决于上限是否包含。您可以将 hi = mid 更改为 hi = mid-1hi = len(a) 更改为 hi = len(a)-1,将 while lo < hi: 更改为 while lo <= hi,这样也是等效正确的。 - user102008
2
为什么不做像这样的事情:def binary_search(a, x, lo = 0, hi = None): i = bisect(a, x, lo, hi) return i if a[i] == x else -1对于格式不当我很抱歉-不确定如何在评论区正确显示。 - Vitali
1
你应该使用 bisect.bisect_left() 而不是这个。 - al45tair
1
当你搜索一个不在列表中的数字时,这将会无限循环而不是返回-1。现有的if语句之前需要增加一个if midval != x and lo + 1 == hi: return -1 - A Student at a University
显示剩余6条评论

41

这有点离题(因为Moe的答案看起来已经完整回答了提问者的问题),但是考虑从头到尾查看您整个过程的复杂性可能是值得的。如果您正在使用排序列表(其中二分搜索会有所帮助)存储物品,然后仅检查存在性,则会产生以下开销(除非指定最坏情况):

排序列表

  • O(nlogn) 初始创建列表(如果是未排序数据则为 O(n),如果已排序,则为 O(n))
  • O(logn) 查找(这是二分搜索部分)
  • O(n) 插入/删除(平均情况下可能是 O(1) 或 O(logn),具体取决于您的模式)

而使用 set() 则会产生:

  • O(n) 创建
  • O(1) 查找
  • O(1) 插入/删除

排序列表真正能给你的是“下一个”、“上一个”和“范围”(包括插入或删除范围),在给定起始索引的情况下为 O(1) 或 O(|range|)。如果您不经常使用这些操作,则将其存储为set,然后进行排序以供显示可能会更划算。在Python中,使用set()几乎不会带来任何额外的开销。


7
排好序的列表还有一个好处,就是可以进行O(n)的有序遍历。而使用set进行遍历的时间复杂度是O(n log n),并且你最终需要将数据的引用复制到一个列表中。 - Omnifarious
1
够真实!感谢您对我所说的范围搜索进行了扩展。顺便说一下,完整遍历与在min、max之间进行范围查询是相同的,其时间复杂度为O(k),其中k = n :) - Gregg Lind
列表中有重复项怎么办? - illuminato

19

上面链接中的 index(a, x) 解决了(二分)搜索任务。+1 - Ambareesh

11

使用bisect最简单,检查前面一个位置以查看该项是否存在:

def binary_search(a,x,lo=0,hi=-1):
    i = bisect(a,x,lo,hi)
    if i == 0:
        return -1
    elif a[i-1] == x:
        return i-1
    else:
        return -1

2
不错,但如果您没有传递“hi”值,代码会出错。我会这样写:“def binary_search(a,x,lo=0,hi=None): from bisect import bisect i = bisect(a,x,lo,hi or len(a)) return (i-1 if a[i-1] == x else -1) " 并像这样测试它:" for i in range(1, 20): a = list(range(i)) for aa in a: j = binary_search(a, aa) if j != aa: print i, aa, j" - hughdbrown

9
这是手册上的正文: http://docs.python.org/2/library/bisect.html 8.5.1. 查找已排序的列表
上述 bisect() 函数非常适用于查找插入点,但对于常见的搜索任务来说可能会变得棘手或笨拙。下面的五个函数展示了如何将它们转换为已排序列表的标准查找:
def index(a, x):
    'Locate the leftmost value exactly equal to x'
    i = bisect_left(a, x)
    if i != len(a) and a[i] == x:
        return i
    raise ValueError

所以,稍作修改后,您的代码应该是这样的:
def index(a, x):
    'Locate the leftmost value exactly equal to x'
    i = bisect_left(a, x)
    if i != len(a) and a[i] == x:
        return i
    return -1

7

这个基于数学断言,即 (low + high)/ 2的底部 总是小于高限制,其中 low 是下限,high 是上限。


def binsearch(t, key, low = 0, high = len(t) - 1):
    # bisecting the range
    while low < high:
        mid = (low + high)//2
        if t[mid] < key:
            low = mid + 1
        else:
            high = mid
    # at this point 'low' should point at the place
    # where the value of 'key' is possibly stored.
    return low if t[low] == key else -1

你如何在 high=len(t)-1 中使用变量 t? - Rishabh Gupta

6

我认为@DaveAbrahams的答案使用二分模块是正确的方法。但他在回答中没有提到一个重要的细节。

文档bisect.bisect_left(a, x, lo=0, hi=len(a))

二分模块不需要预先计算搜索数组。你可以将端点直接提供给bisect.bisect_left而不是使用默认值0len(a)

对于我的需求更加重要,即寻找使给定函数误差最小的值X。为了做到这一点,我需要让bisect_left调用我的计算方式。这非常简单。

只需提供一个对象,该对象定义__getitem__a

例如,我们可以使用二分算法找到任意精度的平方根!

import bisect

class sqrt_array(object):
    def __init__(self, digits):
        self.precision = float(10**(digits))
    def __getitem__(self, key):
        return (key/self.precision)**2.0

sa = sqrt_array(4)

# "search" in the range of 0 to 10 with a "precision" of 0.0001
index = bisect.bisect_left(sa, 7, 0, 10*10**4)
print 7**0.5
print index/(10**4.0)

这不够简洁。使用 scipy.optimize 来完成此任务。 - Neil G

4
如果您只是想查看其是否存在,可以尝试将列表转换为字典:
# Generate a list
l = [n*n for n in range(1000)]

# Convert to dict - doesn't matter what you map values to
d = dict((x, 1) for x in l)

count = 0
for n in range(1000000):
    # Compare with "if n in l"
    if n in d:
        count += 1

在我的电脑上,“if n in l” 花费了37秒,而“if n in d”只花费了0.4秒。该内容涉及IT技术。

2
这并不总是一个好的选择,原因有几个:1)字典/集合占用更多内存。2)如果列表中没有太多元素,二分查找可能会更快。3)将列表转换为字典是O(n)操作,而二分查找是O(log n)操作。 - Jason Baker
3
FYI,与Python列表相比,Python中“set”的开销非常低,并且它们对于查找非常快。二分查找真正擅长的是查找范围。 - Gregg Lind
将列表转换可能是O(n)的,但对列表中的数据进行排序(在二分搜索之前必须这样做)会更糟糕。数据从哪里来,你可以在进行过程中将其插入到字典中。我同意内存可能是个问题。 - Mark Baker

4
戴夫·阿布拉罕的解决方案很好。尽管我会做得更简约:

def binary_search(L, x):
    i = bisect.bisect_left(L, x)
    if i == len(L) or L[i] != x:
        return -1
    return i

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接