比较有序列表和计算常用元素*的最快方法,包括*重复项

时间:2013-11-28 14:17:14

标签: python

我需要比较两个数字列表,并计算第二个列表中第一个列表的元素数量。例如,

a =  [2, 3, 3, 4, 4, 5]
b1 = [0, 2, 2, 3, 3, 4, 6, 8]

这里我应该得到4的结果:我应该计算'2'1次(因为它在第一个列表中只发生一次),'3' - 2次,'4' - 1次(因为它只发生一次)第二个清单)。我使用以下代码:

def scoreIn(list1, list2):
   score=0
   list2c=list(list2)
   for i in list1:
      if i in list2c:
         score+=1
         list2c.remove(i)
   return score

它工作正常,但对我的情况来说太慢了(我称之为15000次)。我读了一个关于通过排序列表“行走”的提示,这些列表应该更快,所以我试着这样做:

def scoreWalk(list1, list2):
   score=0
   i=0
   j=0
   len1=len(list1) # we assume that list2 is never shorter than list1
   while i<len1:
      if list1[i]==list2[j]:
         score+=1
         i+=1
         j+=1
      elif list1[i]>list2[j]:
         j+=1
      else:
         i+=1
   return score

不幸的是,这段代码甚至更慢。有没有办法让它更有效率?在我的例子中,两个列表都是排序的,只包含整数,而list1永远不会比list2长。

2 个答案:

答案 0 :(得分:8)

您可以使用collections.Counter的交集功能以简单易读的方式解决问题:

>>> from collections import Counter
>>> intersection = Counter( [2,3,3,4,4,5] ) & Counter( [0, 2, 2, 3, 3, 4, 6, 8] )
>>> intersection
Counter({3: 2, 2: 1, 4: 1})

正如@Bakuriu在评论中所说,要获得交集中元素的数字(包括重复项),例如scoreIn函数,您可以使用sum( intersection.values() )

然而,以这种方式这样做你实际上并没有利用你的数据是预先排序的事实,也没有实际上(在评论中提到)你正在一遍又一遍地使用相同的事实列表。

这是一个更专门针对您的问题量身定制的更精细的解决方案。它使用Counter作为静态列表,并直接使用已排序的动态列表。在我的机器上,它以随机生成的测试数据的初始Counter方法运行时间的43%运行。

def common_elements( static_counter, dynamic_sorted_list ):
    last = None # previous element in the dynamic list
    count = 0 # count seen so far for this element in the dynamic list

    total_count = 0 # total common elements seen, eventually the return value

    for x in dynamic_sorted_list:
        # since the list is sorted, if there's more than one element they
        # will be consecutive.
        if x == last:
            # one more of the same as the previous  element

            # all we need to do is increase the count
            count += 1
        else:
            # this is a new element that we haven't seen before.

            # first "flush out" the current count we've been keeping.
            #   - count is the number of times it occurred in the dynamic list
            #   - static_counter[ last ] is the number of times it occurred in
            #       the static list (the Counter class counted this for us)
            # thus the number of occurrences the two have in common is the
            # smaller of these numbers. (Note that unlike a normal dictionary,
            # which would raise KeyError, a Counter will return zero if we try
            # to look up a key that isn't there at all.)
            total_count += min( static_counter[ last ], count )

            # now set count and last to the new element, starting a new run
            count = 1
            last = x

    if count > 0:
        # since we only "flushed" above once we'd iterated _past_ an element,
        # the last unique value hasn't been counted. count it now.
        total_count += min( static_counter[ last ], count )

    return total_count

这样做的想法是,在创建Counter对象时,您可以预先完成一些工作。完成该工作后,您可以使用Counter对象快速查找计数,就像在字典中查找值一样:static_counter[ x ]返回x发生的次数在静态列表中。

由于静态列表每次都相同,因此您可以执行此操作一次,并使用生成的快速查找结构15 000次。

另一方面,为动态列表设置Counter对象可能无法在性能方面获得回报。创建Counter对象涉及一些开销,我们一次只使用每个动态列表Counter。如果我们可以完全避免构造对象,那么这样做是有意义的。正如我们在上面看到的那样,您实际上可以通过迭代动态列表并在另一个计数器中查找计数来实现您所需的。

帖子中的scoreWalk功能无法处理最大项目仅在静态列表中的情况,例如scoreWalk( [1,1,3], [1,1,2] )。但是,与您报告的结果相反,更正了它实际上比任何Counter方法更好地执行 。您的数据分布与统一分布的测试数据可能存在显着差异,但请仔细检查scoreWalk的基准测试,以确定。

最后,请考虑您可能正在使用错误的工具来完成工作。你不是追求简短,优雅和可读 - 你试图从一个相当简单的任务中挤出最后一点性能。 CPython允许你write modules in C。其中一个主要用例是实现高度优化的代码。它可能非常适合您的任务。

答案 1 :(得分:0)

您可以使用dict理解来执行此操作:

>>> a =  [2, 3, 3, 4, 4, 5]
>>> b1 = [0, 2, 2, 3, 3, 4, 6, 8]
>>> {k: min(b1.count(k), a.count(k)) for k in set(a)}
{2: 1, 3: 2, 4: 1, 5: 0}

如果set(a)很小,这会快得多。如果set(a)超过40个项目,则基于Counter的解决方案会更快。