【问题标题】:Perform a binary search for a string prefix in Python在 Python 中对字符串前缀执行二进制搜索
【发布时间】:2011-11-14 21:20:16
【问题描述】:

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

这是一个查找所有精确匹配项的示例:

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']。如果我可以修改二分搜索的比较方法,那么我可以使用name.startswith('bob')来做到这一点。

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

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

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

如何在 Python 中做到这一点?

【问题讨论】:

  • 根据您的需要,您也许可以使用 trie 数据结构

标签: python arrays search


【解决方案1】:

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']
>>> 

卫生部。右边的一分为二有效,但左边的显然没有。 “亚当”不以“鲍勃”为前缀!。要修复它,您也必须调整顺序。

>>> 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']
>>> 

【讨论】:

  • 这不适用于所有应用程序。它适用于 bisect_right(names, key),因为 bisect_right 代码测试 key &lt; names[mid]。但是,自定义比较不会用于 bisect_left(names, key),因为 bisect_left 代码测试 names[mid] &lt; key。您会得到正确的结果,因为 bisect_left 的默认行为恰好返回了所需的结果。
  • 如果您实现__eq__() 方法并添加@total_ordering 包装器,那么所有其他比较(以相同顺序)也将起作用。见Total Ordering
  • @total_ordering 并非在所有版本的 python 中都可用,对于本示例也不是必需的。相关链接是ActiveState recipe for the same thing
【解决方案2】:

很遗憾,bisect 不允许您指定 key 函数。不过,您可以在字符串中添加'\xff\xff\xff\xff',然后再使用它来查找最高索引,然后获取这些元素。

【讨论】:

  • 非常聪明的解决方案。我会等着看是否有人在接受之前发布了更强大的内容。
【解决方案3】:

作为 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']

【讨论】:

    【解决方案4】:

    来自函数式编程背景,我很惊讶没有可以提供自定义比较函数的常见二进制搜索抽象。

    为了防止自己一遍又一遍地复制该内容或使用粗俗且难以理解的 OOP hack,我只是编写了与您提到的 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
    

    这是您提供的适用于此功能的代码 sn-p:

    >>> 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

    【讨论】:

      【解决方案5】:

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

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

      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']
      

      你怎么看,这是要走的路吗?

      【讨论】:

      • 如果 bisect 函数简单地接受自定义比较器作为函数会更通用
      猜你喜欢
      • 1970-01-01
      • 2012-03-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多