【问题标题】:Is it faster to union sets or check the whole list for a duplicate?联合集合或检查整个列表是否有重复是否更快?
【发布时间】:2011-01-12 21:22:57
【问题描述】:

很抱歉标题措辞不佳,但我之前问了一个问题,关于从两个列表中获取唯一的项目列表。人们告诉我要列出 -> 集合然后合并。

所以现在我想知道这样做是否更快:

  1. 一个项添加到列表时,扫描整个列表以查找重复项。
  2. 将该项目设为集合,然后合并集合。

事后看来,我可能应该只是阅读片场......

顺便说一下,在 Python 中 - 抱歉没有澄清。

【问题讨论】:

  • 用什么语言?用什么图书馆?摘要中没有答案。
  • 您熟悉timeit 模块吗?您可以收集一些数据并将timeit 结果作为您问题的一部分。
  • 集合是一个容器,和列表一样。您不会将一个项目组合成一组,而是将整个列表组合成一组。您不会一次将元素放入集合中。您只需从列表中创建集合。
  • @Karl Knechtel:但是为什么从列表而不是集合开始呢?如果不需要更改数据类型,那就更好了。我的意思是从不转换比转换一次要快。
  • @Karl Knetchel:嗯,可能,仍然对实际问题感到好奇。当我了解有关逐个添加项目的问题时,我想知道以这种方式提出问题的原因并不是对逐个生成这些项目的过程的调用。

标签: python set


【解决方案1】:

正如您所见,将一个列表扩展到另一端,然后通过设置删除重复项 是最快的方式(至少在python中))

>>> def foo():
...     """
...     extending one list by another end then remove duplicates by making set
...     """
...     l1 = range(200)
...     l2 = range(150, 250)
...     l1.extend(l2)
...     set(l1)
... 
>>> def bar():
...     """
...     checking if element is on one list end adding it only if not
...     """
...     l1 = range(200)
...     l2 = range(150, 250)
...     for elem in l2:
...             if elem not in l1:
...                     l1.append(elem)
... 
>>> def baz():
...     """
...     making sets from both lists and then union from them
...     """
...     l1 = range(200)
...     l2 = range(150, 250)
...     set(l1) | set(l2)
... 
>>> from timeit import Timer
>>> Timer(foo).timeit(10000)
0.265153169631958
>>> Timer(bar).timeit(10000)
7.921358108520508
>>> Timer(baz).timeit(10000)
0.3845551013946533
>>> 

【讨论】:

  • set(itertools.chain(l1,l2)) 只是慢了一点点,不需要修改l1。如果 l1 和 l2 很大,内存效率也会更高
  • 执行x = set(l1); x.update(l2)foo 方法一样快(在我的计算机上可能稍微快一些)并且使用更少的内存。它也不会修改l1
  • 或者更快的set(l1).union(l2) 不会修改任何内容,而且内存效率也很高。
  • @kriss,你计时了吗?据我统计,这比使用更新方法慢了大约 25%。我很确定使用这样的联合会更慢,因为它首先创建set(l1),然后在执行联合时创建另一个集合(它返回)。
  • @Justin Peel:是的,我确实计时了,但是我的测试集中有错字,你的我更快(但在大多数情况下仍然慢 - 在这种情况下是三倍 - 比扩展初始列表)。
【解决方案2】:

我真的很喜欢 virhilo 所做的方法,但这是他正在测试的一组非常具体的数据。在所有这些中,不仅要测试功能,还要测试它们你将如何做。我整理了一个更详尽的测试集。它通过比较列表运行您指定的每个函数(仅使用一个小装饰器),并计算出每个函数需要多长时间,因此它慢了多少。结果是,在不了解数据的大小、重叠和类型的更多信息的情况下,您并不总是清楚应该执行哪个功能。

这是我的测试程序,下面是输出。

from timeit import Timer
from copy import copy
import random
import sys

funcs = []

class timeMe(object):
    def __init__(self, f):
        funcs.append(f)
        self.f = f

    def __call__(self, *args, **kwargs):
        return self.f(*args, **kwargs)

@timeMe
def extend_list_then_set(input1, input2):
    """
    extending one list by another end then remove duplicates by making set
    """
    l1 = copy(input1)
    l2 = copy(input2)
    l1.extend(l2)
    set(l1)

@timeMe
def per_element_append_to_list(input1, input2):
    """
    checking if element is on one list end adding it only if not
    """
    l1 = copy(input1)
    l2 = copy(input2)
    for elem in l2:
            if elem not in l1:
                    l1.append(elem)

@timeMe
def union_sets(input1, input2):
    """
    making sets from both lists and then union from them
    """
    l1 = copy(input1)
    l2 = copy(input2)
    set(l1) | set(l2)

@timeMe
def set_from_one_add_from_two(input1, input2):
    """
    make set from list 1, then add elements for set 2
    """
    l1 = copy(input1)
    l2 = copy(input2)
    l1 = set(l1)
    for element in l2:
        l1.add(element)

@timeMe
def set_from_one_union_two(input1, input2):
    """
    make set from list 1, then union list 2
    """
    l1 = copy(input1)
    l2 = copy(input2)
    x = set(l1).union(l2)

@timeMe
def chain_then_set(input1, input2):
    """
    chain l1 & l2, then make a set out of that
    """
    l1 = copy(input1)
    l2 = copy(input2)
    set(itertools.chain(l1, l2))

def run_results(l1, l2, times):
    for f in funcs:
        t = Timer('%s(l1, l2)' % f.__name__,
            'from __main__ import %s; l1 = %s; l2 = %s' % (f.__name__, l1, l2))
        yield (f.__name__, t.timeit(times))    

test_datasets = [
    ('original, small, some overlap', range(200), range(150, 250), 10000),
    ('no overlap: l1 = [1], l2 = [2..100]', [1], range(2, 100), 10000),
    ('lots of overlap: l1 = [1], l2 = [1]*100', [1], [1]*100, 10000),
    ('50 random ints below 2000 in each', [random.randint(0, 2000) for x in range(50)], [random.randint(0, 2000) for x in range(50)], 10000),
    ('50 elements in each, no overlap', range(50), range(51, 100), 10000),
    ('50 elements in each, total overlap', range(50), range(50), 10000),
    ('500 random ints below 500 in each', [random.randint(0, 500) for x in range(500)], [random.randint(0, 500) for x in range(500)], 1000),
    ('500 random ints below 2000 in each', [random.randint(0, 2000) for x in range(500)], [random.randint(0, 2000) for x in range(500)], 1000),
    ('500 random ints below 200000 in each', [random.randint(0, 200000) for x in range(500)], [random.randint(0, 200000) for x in range(500)], 1000),
    ('500 elements in each, no overlap', range(500), range(501, 1000), 10000),
    ('500 elements in each, total overlap', range(500), range(500), 10000),
    ('10000 random ints below 200000 in each', [random.randint(0, 200000) for x in range(10000)], [random.randint(0, 200000) for x in range(10000)], 50),
    ('10000 elements in each, no overlap', range(10000), range(10001, 20000), 10),
    ('10000 elements in each, total overlap', range(10000), range(10000), 10),
    ('original lists 100 times', range(200)*100, range(150, 250)*100, 10),
]

fullresults = []
for description, l1, l2, times in test_datasets:
    print "Now running %s times: %s" % (times, description)
    results = list(run_results(l1, l2, times))
    speedresults = [x for x in sorted(results, key=lambda x: x[1])]
    for name, speed in results:
        finish = speedresults.index((name, speed)) + 1
        timesslower = speed / speedresults[0][1]
        fullresults.append((description, name, speed, finish, timesslower))
        print '\t', finish, ('%.2fx' % timesslower).ljust(10), name.ljust(40), speed

print
import csv
out = csv.writer(sys.stdout)
out.writerow(('Test', 'Function', 'Speed', 'Place', 'timesslower'))
out.writerows(fullresults)

结果

我的意思是鼓励您使用自己的数据进行测试,因此我不想在细节上喋喋不休。但是...第一个扩展方法是最快的平均方法,但是 set_from_one_union_two (x = set(l1).union(l2)) 赢了几次。如果您自己运行脚本,您可以获得更多详细信息。

我报告的数字是该函数比该测试中最胖的函数慢的次数。如果它是最快的,它将是 1。

                                            Functions                                                                                                                           
Tests                                       extend_list_then_set     per_element_append_to_list    set_from_one_add_from_two  set_from_one_union_two     union_sets      chain_then_set
original, small, some overlap               1                          25.04                        1.53                        1.18                       1.39           1.08
no overlap: l1 = [1], l2 = [2..100]         1.08                       13.31                        2.10                        1                          1.27           1.07
lots of overlap: l1 = [1], l2 = [1]*100     1.10                        1.30                        2.43                        1                          1.25           1.05
50 random ints below 2000 in each           1                           7.76                        1.35                        1.20                       1.31           1   
50 elements in each, no overlap             1                           9.00                        1.48                        1.13                       1.18           1.10
50 elements in each, total overlap          1.08                        4.07                        1.64                        1.04                       1.41           1   
500 random ints below 500 in each           1.16                       68.24                        1.75                        1                          1.28           1.03
500 random ints below 2000 in each          1                         102.42                        1.64                        1.43                       1.81           1.20
500 random ints below 200000 in each        1.14                      118.96                        1.99                        1.52                       1.98           1   
500 elements in each, no overlap            1.01                      145.84                        1.86                        1.25                       1.53           1   
500 elements in each, total overlap         1                          53.10                        1.95                        1.16                       1.57           1.05          
10000 random ints below 200000 in each      1                        2588.99                        1.73                        1.35                       1.88           1.12
10000 elements in each, no overlap          1                        3164.01                        1.91                        1.26                       1.65           1.02
10000 elements in each, total overlap       1                        1068.67                        1.89                        1.26                       1.70           1.05
original lists 100 times                    1.11                     2068.06                        2.03                        1                          1.04           1.17

                                 Average    1.04                      629.25                       1.82                         1.19                       1.48           1.06
                      Standard Deviation    0.05                     1040.76                       0.26                         0.15                       0.26           0.05
                                     Max    1.16                     3164.01                       2.43                         1.52                       1.98           1.20

【讨论】:

  • +1 表示不错的测试集。仍然有一些实现要添加到测试中,比如来自@gnibbler 的 ichain。同样值得怀疑的是,在实际使用中应该计算列表构建时间(l1.copy())......在某些情况下,例如集合复制 l1 的并集只是浪费时间,并且在所有情况下复制 l2 完全没用(l2 一开始没有修改),这可能会改变“赢家”......总体而言,了解更多关于 OP 的实际用例将有助于解释结果。
  • 我同意复制操作很奇怪,但他们都这样做了,所以它应该对所有人都有同样的影响。我想以一致的方式平衡谁修改原始 l1 和谁不尽可能多地修改原始 l1 的竞争环境。我现在将添加@gnibbler 的链。
【解决方案3】:

一切都取决于你有什么作为输入和想要作为输出。

如果您在开头有一个列表li,并希望在最后得到一个修改后的列表,那么更快的方法是if not elt in li: li.append(elt),问题是将初始列表转换为集合,然后再转换回列表,这是一种方式太慢了。

但如果你可以一直使用 s 集合(你不关心列表的顺序,接收它的方法只需要一些可迭代的),那么只使用 s.add(elt) 会更快。

如果一开始您必须列出并最终想要一个列表,即使最终从列表集转换为列表,使用集合管理项目的唯一性也更快,但您可以轻松查看提供的示例@virhilo 在它的回答中,比使用扩展连接两个列表,然后将结果转换为集合比将两个列表转换为集合并执行联合更快。

我不确切知道您的程序的限制是什么,但是如果唯一性和看起来一样重要,并且如果不需要保持插入顺序,那么建议您始终使用集合,永远不要更改他们列出来。无论如何,大多数算法都适用于这两种方法,这要归功于 Duck Typing,因为它们都是不同类型的可迭代对象。

【讨论】:

    【解决方案4】:

    您可以做的最快的事情是从列表中构建两个集合并取它们的并集。 list 和 set union 的集合构造都是在运行时实现的,在非常优化的 C 中,所以它非常快。

    在代码中,如果列表是l1l2,你可以这样做

    unique_elems = set(l1) | set(l2)
    

    编辑:正如@kriss 所说,用l2 扩展l1 更快。但是,此代码不会更改 l1,并且如果 l1l2 是通用可迭代对象,也可以使用。

    【讨论】:

    • 正如您在查看@virhilo 答案时所看到的,您的答案不正确。扩展初始列表然后转换为集合更快。调用非常优化的 C 也是有道理的,并且扩展检查几乎没有,而且比构建一个集合要简单得多。但是,我不会投反对票,因为我怀疑您的提案对于某些测试集可能仍然更快(例如在开始时包含许多重复项的列表)。
    • 我使用@virhilo 代码检查了l1 = range(200) * 100; l2 = range(150, 250) * 100,确实将两个列表转换为集合比首先扩展初始列表要快一些......但我相信这个测试集是一个极端情况。
    • @kriss:如果有很多重复,它会更快。无论如何,我仍然更喜欢这段代码,因为它对第一个列表没有副作用(而 @virhilo 的代码有),并且如果 l1l2 不是 lists 的可迭代对象,它也可以工作跨度>
    • 您也可以执行newlist = list(l1); newlist.extend(l2); return newlist;,它也适用于任何可迭代且不修改初始列表,或者(甚至更快)return set(l1).union(l2) 最后一个避免一个操作(构造第二组)比较到你的版本。
    猜你喜欢
    • 1970-01-01
    • 2022-01-04
    • 1970-01-01
    • 2012-03-10
    • 1970-01-01
    • 2014-05-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多