在Python中对字符串前缀执行二进制搜索

时间:2011-09-11 19:17:52

标签: python arrays search

我想搜索以给定子字符串开头的所有元素的有序字符串列表。

以下是查找所有完全匹配的示例:

import bisect
names = ['adam', 'bob', 'bob', 'bob', 'bobby', 'bobert', 'chris']
names.sort()
leftIndex = bisect.bisect_left(names, 'bob')
rightIndex = bisect.bisect_right(names, 'bob')
print(names[leftIndex:rightIndex])

打印['bob', 'bob', 'bob']

相反,我想用'bob'搜索开始的所有名称。我想要的输出是['bob', 'bob', 'bob', 'bobby', 'bobert']。如果我可以修改bisect搜索的比较方法,那么我可以使用name.startswith('bob')来执行此操作。

例如,在Java中它很容易。我会用:

Arrays.binarySearch(names, "bob", myCustomComparator);

其中'myCustomComparator'是一个利用了startswith方法的比较器(以及一些额外的逻辑)。

我如何在Python中执行此操作?

5 个答案:

答案 0 :(得分:6)

bisect可以通过使用使用选择的自定义比较器的实例来使用自定义比较来愚弄:

>>> class PrefixCompares(object):
...     def __init__(self, value):
...         self.value = value
...     def __lt__(self, other):
...         return self.value < other[0:len(self.value)]
... 
>>> import bisect
>>> names = ['adam', 'bob', 'bob', 'bob', 'bobby', 'bobert', 'chris']
>>> names.sort()
>>> key = PrefixCompares('bob')
>>> leftIndex = bisect.bisect_left(names, key)
>>> rightIndex = bisect.bisect_right(names, key)
>>> print(names[leftIndex:rightIndex])
['adam', 'bob', 'bob', 'bob', 'bobby', 'bobert']
>>> 

DOH。正确的平分工作,但左边显然没有。 “adam”没有以“bob”为前缀!为了解决它,你也必须调整顺序。

>>> class HasPrefix(object):
...     def __init__(self, value):
...         self.value = value
...     def __lt__(self, other):
...         return self.value[0:len(other.value)] < other.value
... 
>>> class Prefix(object):
...     def __init__(self, value):
...         self.value = value
...     def __lt__(self, other):
...         return self.value < other.value[0:len(self.value)]
... 
>>> class AdaptPrefix(object):
...     def __init__(self, seq):
...         self.seq = seq
...     def __getitem__(self, key):
...         return HasPrefix(self.seq[key])
...     def __len__(self):
...         return len(self.seq)
... 
>>> import bisect
>>> names = ['adam', 'bob', 'bob', 'bob', 'bobby', 'bobert', 'chris']
>>> names.sort()
>>> needle = Prefix('bob')
>>> haystack = AdaptPrefix(names)
>>> leftIndex = bisect.bisect_left(haystack, needle)
>>> rightIndex = bisect.bisect_right(haystack, needle)
>>> print(names[leftIndex:rightIndex])
['bob', 'bob', 'bob', 'bobby', 'bobert']
>>> 

答案 1 :(得分:4)

不幸的是,bisect不允许您指定key功能。你可以做的是在使用它来查找最高索引之前将'\xff\xff\xff\xff'添加到字符串中,然后获取这些元素。

答案 2 :(得分:2)

作为IfLoop的答案的替代方案 - 为什么不使用内置的__gt__

>>> class PrefixCompares(object):
...     def __init__(self, value):
...         self.value = value
...     def __lt__(self, other):
...         return self.value < other[0:len(self.value)]
...     def __gt__(self, other):
...         return self.value[0:len(self.value)] > other
>>> import bisect
>>> names = ['adam', 'bob', 'bob', 'bob', 'bobby', 'bobert', 'chris']
>>> names.sort()
>>> key = PrefixCompares('bob')
>>> leftIndex = bisect.bisect_left(names, key)
>>> rightIndex = bisect.bisect_right(names, key)
>>> print(names[leftIndex:rightIndex])
['bob', 'bob', 'bob', 'bobby', 'bobert']

答案 3 :(得分:1)

从功能编程背景来看,我很惊讶,你可以提供自定义比较功能,而不是常见的二进制搜索抽象。

为了防止我自己一遍又一遍地重复那个东西,或者使用严重且不可读的OOP黑客攻击,我只是编写了你提到的Arrays.binarySearch(names, "bob", myCustomComparator);函数的等价物:

class BisectRetVal():
    LOWER, HIGHER, STOP = range(3)

def generic_bisect(arr, comparator, lo=0, hi=None): 
    if lo < 0:
        raise ValueError('lo must be non-negative')
    if hi is None:
        hi = len(arr)
    while lo < hi:
        mid = (lo+hi)//2
        if comparator(arr, mid) == BisectRetVal.STOP: return mid
        elif comparator(arr, mid) == BisectRetVal.HIGHER: lo = mid+1
        else: hi = mid
    return lo

这是通用部分。以下是针对您案例的具体比较器:

def string_prefix_comparator_right(prefix):
    def parametrized_string_prefix_comparator_right(array, mid):
        if array[mid][0:len(prefix)] <= prefix:
            return BisectRetVal.HIGHER
        else:
            return BisectRetVal.LOWER
    return parametrized_string_prefix_comparator_right


def string_prefix_comparator_left(prefix):
    def parametrized_string_prefix_comparator_left(array, mid):
        if array[mid][0:len(prefix)] < prefix: # < is the only diff. from right
            return BisectRetVal.HIGHER
        else:
            return BisectRetVal.LOWER
    return parametrized_string_prefix_comparator_left

以下是您提供的适用于此功能的代码段:

>>> names = ['adam', 'bob', 'bob', 'bob', 'bobby', 'bobert', 'chris']
>>> names.sort()
>>> leftIndex = generic_bisect(names, string_prefix_comparator_left("bob"))
>>> rightIndex = generic_bisect(names, string_prefix_comparator_right("bob"))
>>> names[leftIndex:rightIndex]
['bob', 'bob', 'bob', 'bobby', 'bobert']

它在Python 2和Python 3中都没有改变。

有关其工作原理的更多信息以及更多有关此事的比较器,请查看以下要点:https://gist.github.com/Shnatsel/e23fcd2fe4fbbd869581

答案 4 :(得分:0)

这是一个尚未提供的解决方案:重新实现二进制搜索算法。

这通常应该避免,因为你重复代码(二进制搜索很容易搞砸),但似乎没有很好的解决方案。

bisect_left()已经提供了所需的结果,因此我们只需要更改bisect_right()。以下是参考的原始实现:

def bisect_right(a, x, lo=0, hi=None):
    if lo < 0:
        raise ValueError('lo must be non-negative')
    if hi is None:
        hi = len(a)
    while lo < hi:
        mid = (lo+hi)//2
        if x < a[mid]: hi = mid
        else: lo = mid+1
    return lo

这是新版本。唯一的变化是我添加and not a[mid].startswith(x),我称之为“bisect_right_prefix”:

def bisect_right_prefix(a, x, lo=0, hi=None):
    if lo < 0:
        raise ValueError('lo must be non-negative')
    if hi is None:
        hi = len(a)
    while lo < hi:
        mid = (lo+hi)//2
        if x < a[mid] and not a[mid].startswith(x): hi = mid
        else: lo = mid+1
    return lo

现在代码如下:

names = ['adam', 'bob', 'bob', 'bob', 'bobby', 'bobert', 'chris']
names.sort()
leftIndex = bisect.bisect_left(names, 'bob')
rightIndex = bisect_right_prefix(names, 'bob')
print(names[leftIndex:rightIndex])

产生预期结果:

['bob', 'bob', 'bob', 'bobby', 'bobert']

您怎么看?这是要走的路吗?