【问题标题】:Given Two Lists of Integers, Find Each Pair Within a Distance of Each Other < O(N^2)给定两个整数列表,找到彼此距离 < O(N^2) 内的每一对
【发布时间】:2012-12-13 13:40:26
【问题描述】:

我有两个排序的整数列表。我想分别从第一个和第二个列表中找到所有在一定距离内的整数对。

天真的方法是检查每一对,导致 O(N^2) 时间。我确信有一种方法可以在 O(N*logN) 或更短的时间内完成。

在python中,朴素的O(N^2)方法如下:

def find_items_within(list1, list2, within):
    for l1 in list1:
        for l2 in list2:
            if abs(l1 - l2) <= within:
                yield (l1, l2)

pythonic 答案加分。

应用说明

我只是想指出这个小谜题的目的。我正在搜索一个文档,并希望在另一个术语的一定距离内找到一个术语的所有出现。首先你找到这两个词的词向量,然后你可以使用下面描述的算法来确定它们是否在给定的距离内。

【问题讨论】:

    标签: python algorithm


    【解决方案1】:

    没有比O(n^2) 更好的方法了,因为有O(n^2) 对,而对于within = infinity,你需要让它们全部产生。


    要找到这些对的数量是另一回事,可以通过找到每个元素e 的第一个索引来完成within-e &lt; arr[idx]。例如,可以使用二分搜索有效地找到索引 idx - 这将为您提供 O(nlogn) 解决方案来找到这些对的number

    也可以在线性时间 (O(n)) 内完成,因为您实际上并不需要对所有元素进行二分搜索,在找到第一个 [a,b] 范围后,请注意对于其他范围 @ 987654330@ - 如果a&gt;a' 然后b&gt;=b' - 所以你实际上需要用两个指针迭代列表并且“永不回头”以获得线性时间复杂度。

    伪代码:(用于线性时间解)

    numPairs <- 0
    i <- 0
    a <- 0
    b <- 0
    while (i < list1.length):
      while (a < i && list1[i] - list2[a] > within):
          a <- a+1
      while (b < list2.length && list2[b] - list1[i] < within):
          b <- b+1
      if (b > a):
          numPairs <- numPairs + (b-a)
      i <- i+1
    return numPairs
    

    (我对最初的伪代码进行了一些修复——因为第一个伪代码的目标是在单个列表中查找范围内的对数——而不是两个列表之间的匹配,抱歉)

    【讨论】:

    • (对于二分搜索 - OP 应该查看 bisect 模块)
    • 我认为配对的数量可以在 O(n) 内完成 - 只需找到第一个数字的窗口,然后滑动它以获得后续数字。
    • amit -- 你不需要全部测试。由于数据已排序,一旦超出您关心的距离,您就可以安全地打破该循环。
    • @mgilson:数字不需要等距。窗口可以扩大和缩小。
    • @mgilson:是的,但是 2 个指针将精确滑动 n 次。我们只是从上一个位置开始。
    【解决方案2】:

    此代码为 O(n*log(n)+m),其中 m 是答案的大小。

    def find_items_within(l1, l2, dist):
        l1.sort()
        l2.sort()
        b = 0
        e = 0
        ans = []
        for a in l1:
            while b < len(l2) and a - l2[b] > dist:
                b += 1
            while e < len(l2) and l2[e] - a <= dist:
                e += 1
            ans.extend([(a,x) for x in l2[b:e]])
        return ans
    

    在最坏的情况下,可能是m = n*n,但如果答案只是所有可能对的一小部分,这会快很多。

    【讨论】:

    • 这里有很多好的建议,但我认为这个是最容易理解的,不依赖任何太花哨的东西,并且算法时间与其他的一样好或更好。虽然@J.F.塞巴斯蒂安的回答声称更快,我认为当你考虑到它的 O(log(n)) 查找时它是一样的。
    • @speedplane: set() 在 Python 中具有 O(1) 摊销查找,因为我的代码中的 cmets 明确表示(想想 C++ 中的 unordered_set&lt;&gt;,而不是 set&lt;&gt;O(log(n)))。顺便说一句,如果在上面删除.sort(),时间复杂度并不好(我的回答假设输入按您问题的第一句中所述进行排序,并且代码提示中的assert issorted() 语句也是如此) .对于我在我的机器上尝试过的输入,这个答案要快 2-3 倍。
    • @speedplane:顺便说一句,如果将ans.extend 替换为yield i, (b,e) 其中l1[i] == a,Thomash 的答案可以是线性时间。您仍然需要O(n*n) 来显式枚举所有对,但您可以在O(n) 中找到对(如:知道它们的索引范围)以进行排序输入。
    • @J.F.Sebastian:不可能有线性时间算法,因为输出不是线性的。您能做的最好的事情是 O(m),其中 m 是输出的大小,如果您考虑对输入进行排序,这就是我的算法的复杂度。
    • @Thomash:你读过“你仍然需要O(n*n)O(m) 使用你的术语)来明确列举所有对”我评论的一部分吗?单个 yield i, (b, e) 立即为您提供 e-b 对。
    【解决方案3】:

    这里有与你给出的相同界面的东西:

    def find_items_within(list1, list2, within):
        i2_idx = 0
        shared = []
        for i1 in list1:
            # pop values to small
            while shared and abs(shared[0] - i1) > within: 
                shared.pop(0)
            # insert new values 
            while i2_idx < len(list2) and abs(list2[i2_idx] - i1) <= within:
                shared.append(list2[i2_idx])
                i2_idx += 1
            # return result
            for result in zip([i1] * len(shared), shared):
                yield result
    
    for item in find_items_within([1,2,3,4,5,6], [3,4,5,6,7], 2):
        print item
    

    不是很漂亮,但它应该在O(N*M) 中发挥作用,其中N 是list1 的长度,M 是每个项目的共享对列表(假设删除并附加到shared 的元素是平均恒定)。

    【讨论】:

      【解决方案4】:

      根据列表中值的分布,您可以通过使用 binning 来加快速度:取所有值所在的范围 (min(A+B), max(A+B)),然后除以该范围与您正在考虑的距离D 的大小相同。然后,要查找所有对,您只需要比较一个 bin 内或相邻 bin 内的值。如果您的值在多个 bin 之间拆分,这是避免进行 M*N 比较的简单方法。

      另一种在实践中可能同样简单的技术:进行有界线性扫描。从头开始维护列表 A 和列表 B 的索引。在每次迭代中,将索引推进到列表 A(从第一个元素开始),将此元素称为 A0。然后,将索引推进到列表 B。记住 B 的最后一个小于 A0-D 的值(这是我们要开始下一次迭代的地方)。但是,当您在 A0-D 和 A0+D 之间找到值时,请继续前进——这些是您正在寻找的对。一旦 B 中的值变得大于 A0+D,停止此迭代并开始下一个迭代 — 将一个元素进一步推进 A,并从 B 的最后一个位置开始扫描 B

      如果平均而言,每个元素附近有恒定数量的对,我认为这应该是 O(M+N)?

      【讨论】:

        【解决方案5】:

        此方法使用一个字典,其键是list2 的可能值,其值是list1 值的列表,这些值在list2 的距离内。

        def find_items_within(list1, list2, within):
            a = {}
            for l1 in list1:
                for i in range(l1-within, l1+within+1):
                    if i not in a:
                        a[i] = []
                    a[i].append(l1)
            for l2 in list2:
                if l2 in a:
                    for l1 in a[l2]:
                        yield(l1, l2)
        

        这个复杂度有点傻。对于大小为 M 的列表 1 和大小为 N 的列表 2 和大小为 W 的范围内,它是 O(log(M*W) * (M*W + N))。在实践中,我认为它对小 W 效果很好。

        奖励:这也适用于未排序的列表。

        【讨论】:

        • 使用字典的好方法。然而,缺点是它还分配了 M*W 大小的结构。
        【解决方案6】:

        这似乎有效:

        from itertools import takewhile
        def myslice(lst,start,stop,stride=1):
            stop = len(lst) if stop is None else stop
            for i in xrange(start,stop,stride):
                yield lst[i]
        
        def find_items_within(lst1,lst2,within):
            l2_start = 0
            for l1 in lst1:
                try:
                    l2_start,l2 = next( (i,x) for i,x in enumerate(myslice(lst2,l2_start,None),l2_start) if abs(l1-x) <= within )
                    yield l1,l2
                    for l2 in takewhile(lambda x:(abs(l1-x) <= within), myslice(lst2,l2_start+1,None)):
                        yield l1,l2
                except StopIteration:
                    pass
        
        
        x = range(10)
        y = range(10)
        print list(find_items_within(x,y,2.5))
        

        【讨论】:

          【解决方案7】:

          您可以使用“扫描线”技术在 线性时间 (O(n)) 中找到所有 xx 中的 [x - within, x + within] 区间内的整数(请参阅How to Find All Overlapping IntervalsSub O(n^2) algorithm for counting nested intervals?)。

          要从list1 枚举相应的区间,您需要O(m) 时间,其中m 是区间数,即整体算法为O(n*m)

          from collections import namedtuple
          from heapq import merge
          
          def find_items_within(list1, list2, within):
              issorted = lambda L: all(x <= y for x, y in zip(L, L[1:]))
              assert issorted(list1) and issorted(list2) and within >= 0
          
              # get sorted endpoints - O(n) (due to list1, list2 are sorted)
              Event = namedtuple('Event', "endpoint x type")
              def get_events(lst, delta, type):
                  return (Event(x + delta, x, type) for x in lst)
              START, POINT, END = 0, 1, 2 
              events = merge(get_events(list1, delta=-within, type=START),
                             get_events(list1, delta=within, type=END),
                             get_events(list2, delta=0, type=POINT))
          
              # O(n * m), m - number of points in `list1` that are 
              #               within distance from given point in `list2`
              started = set() # started intervals
              for e in events:  # O(n)
                  if e.type is START: # started interval
                      started.add(e.x) # O(m) is worst case (O(1) amortized)
                  elif e.type is END: # ended interval
                      started.remove(e.x)  # O(m) is worst case (O(1) amortized)
                  else:  # found point
                      assert e.type is POINT
                      for x in started:  # O(m)
                          yield x, e.x
          

          允许list1 中的重复值;您可以在Event 中为每个x 添加索引,并使用字典index -&gt; x 而不是started 集合。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2018-03-07
            • 1970-01-01
            • 2019-11-19
            • 1970-01-01
            • 2017-07-06
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多