测试列表是否共享python中的任何项目


131

我想检查一个列表中的任何项目是否存在于另一个列表中。我可以使用下面的代码简单地做到这一点,但是我怀疑可能有一个库函数可以做到这一点。如果没有,是否有更多的pythonic方法可以达到相同的结果。

In [78]: a = [1, 2, 3, 4, 5]

In [79]: b = [8, 7, 6]

In [80]: c = [8, 7, 6, 5]

In [81]: def lists_overlap(a, b):
   ....:     for i in a:
   ....:         if i in b:
   ....:             return True
   ....:     return False
   ....: 

In [82]: lists_overlap(a, b)
Out[82]: False

In [83]: lists_overlap(a, c)
Out[83]: True

In [84]: def lists_overlap2(a, b):
   ....:     return len(set(a).intersection(set(b))) > 0
   ....: 

我能想到的唯一优化是删除,len(...) > 0因为bool(set([]))产生False。当然,如果您将列表保留为集合,那么可以节省集合创建的开销。
msw


1
请注意,你不能明显True1False0not set([1]).isdisjoint([True])gets True,与其他解决方案相同。
Dimali

Answers:


313

简短答案:使用not set(a).isdisjoint(b),通常是最快的。

有测试四种常见的方式,如果两个列表ab共享任何项目。第一种选择是将两个都转换为集合并检查它们的交集,如下所示:

bool(set(a) & set(b))

由于集合是使用Python中的哈希表存储的,因此可以搜索它们O(1)(有关Python中运算符复杂性的更多信息,请参见此处)。从理论上讲,这是O(n+m)对平均nm在列表中的对象ab。但是1)它必须首先从列表中创建集合,这可能花费不可忽略的时间量; 2)它假定哈希冲突在您的数据中很少。

第二种方法是使用生成器表达式对列表执行迭代,例如:

any(i in a for i in b)

这允许就地搜索,因此不会为中间变量分配新的内存。它也可以在第一个发现上解决。但是in操作员始终O(n)在列表中(请参阅此处)。

另一个建议的选项是混合访问列表中的一个,转换另一个集合,然后测试该集合的成员资格,如下所示:

a = set(a); any(i in a for i in b)

第四种方法是利用isdisjoint()(冻结)集合的方法(请参阅此处),例如:

not set(a).isdisjoint(b)

如果您搜索的元素在数组的开头附近(例如已排序),则倾向于使用生成器表达式,因为集合交集方法必须为中间变量分配新的内存:

from timeit import timeit
>>> timeit('bool(set(a) & set(b))', setup="a=list(range(1000));b=list(range(1000))", number=100000)
26.077727576019242
>>> timeit('any(i in a for i in b)', setup="a=list(range(1000));b=list(range(1000))", number=100000)
0.16220548999262974

这是此示例的执行时间与列表大小的关系图:

在开始时共享的元素共享测试执行时间

请注意,两个轴都是对数的。这代表了生成器表达式的最佳情况。可以看出,该isdisjoint()方法对于非常小的列表大小更好,而生成器表达式对于更大的列表大小更好。

另一方面,由于搜索是从混合表达式和生成器表达式的开头开始的,因此,如果共享元素系统地位于数组的末尾(或者两个列表都不共享任何值),则不相交和集合交集方法比生成器表达式和混合方法快得多。

>>> timeit('any(i in a for i in b)', setup="a=list(range(1000));b=[x+998 for x in range(999,0,-1)]", number=1000))
13.739536046981812
>>> timeit('bool(set(a) & set(b))', setup="a=list(range(1000));b=[x+998 for x in range(999,0,-1)]", number=1000))
0.08102107048034668

最后共享时元素共享测试执行时间

有趣的是,对于较大的列表大小,生成器表达式要慢得多。这仅适用于1000次重复,而不是前一个数字的100000次。当没有共享任何元素时,此设置也很合适,并且是不相交和设置相交方法的最佳情况。

这是两个使用随机数的分析(而不是操纵设置以偏爱一种或多种技术):

随机共享数据的元素共享测试执行时间,共享可能性很高 随机共享数据的元素共享测试执行时间,共享可能性很高

分享的可能性很高:元素是从中随机抽取的[1, 2*len(a)]。分享机会低:元素是从中随机抽取的[1, 1000*len(a)]

到目前为止,该分析假设两个列表的大小相同。如果有两个不同大小的列表,例如a小得多,isdisjoint()总是更快:

在开始时共享时,元素在两个大小不同的列表上共享测试执行时间 最后共享时,元素在两个大小不同的列表上共享测试执行时间

确保a列表较小,否则性能会降低。在此实验中,a列表大小设置为常量5

综上所述:

  • 如果列表很小(<10个元素),not set(a).isdisjoint(b)则总是最快的。
  • 如果列表中的元素已排序或具有可以利用的规则结构,则生成器表达式any(i in a for i in b)在大列表大小时最快。
  • not set(a).isdisjoint(b)用来测试设置的交集,它总是比快bool(set(a) & set(b))
  • 混合“遍历列表,按条件测试” a = set(a); any(i in a for i in b)通常比其他方法慢。
  • 当涉及到不共享元素的列表时,生成器表达式和混合函数比其他两种方法要慢得多。

在大多数情况下,使用该isdisjoint()方法是最好的方法,因为生成器表达式的执行时间会更长,因为在没有共享任何元素时效率非常低。


8
那是一些有用的数据,表明big-O分析并不是运行时间的全部和全部推论。
史蒂夫·艾里森

最坏的情况呢?any以第一个非False值退出。使用唯一匹配值位于末尾的列表,我们得到: timeit('any(i in a for i in b)', setup="a=list(range(1000));b=[x+998 for x in range(999,-0,-1)]", number=1000) 13.739536046981812 timeit('bool(set(a) & set(b))', setup="a=list(range(1000));b=[x+998 for x in range(999,-0,-1)]", number=1000) 0.08102107048034668 ...并且仅具有1000次迭代。
RobM

2
感谢@RobM提供的信息。我已经更新了答案,以反映这一点,并考虑到该线程中提出的其他技术。
Soravux

应该not set(a).isdisjoint(b)测试两个列表是否共享一个成员。set(a).isdisjoint(b)返回True如果两个列表都不能共享的成员。答案应该被编辑吗?
Guillochon'2

1
感谢大家的注意,@ Guillochon,问题已解决。
Soravux

25
def lists_overlap3(a, b):
    return bool(set(a) & set(b))

注意:以上假设您想要布尔值作为答案。如果您只需要在if语句中使用表达式,则只需使用if set(a) & set(b):


5
这是最坏的情况O(n + m)。但是,不利的一面是它创建了一个新集合,并且在早期发现一个公共元素时不会退出。
马修·弗拉申

1
我很好奇为什么会这样O(n + m)。我的猜测是使用哈希表实现集合,因此in运算符可以及时工作O(1)(在退化的情况下除外)。这样对吗?如果是这样的话,假设哈希表的最坏情况下的查找性能为O(n),这是否意味着在不同的情况下它会具有O(n * m)更好的性能?
fmark

1
@fmark:从理论上讲,您是对的。实际上,没有人在乎。阅读CPython源代码中的Objects / dictobject.c中的注释(集合只是仅包含键,没有值的dict),并查看是否可以生成将导致O(n)查找性能的键列表。
约翰·马钦

好的,感谢您的澄清,我想知道是否发生了一些魔术:)。虽然我同意实际上我不需要关心,但是生成将导致O(n)查找性能的键列表很简单;),请参见pastebin.com/Kn3kAW7u仅用于lafs。
fmark

2
是的,我知道。另外,我只是阅读了您指向的参考资料,其中记载了非随机哈希函数(例如内置哈希函数)的更多魔力。我认为它需要像Java一样的随机性,这会导致类似stackoverflow.com/questions/2634690/…的麻烦。我需要不断提醒自己,Python不是Java(感谢神!)。
fmark

10
def lists_overlap(a, b):
  sb = set(b)
  return any(el in sb for el in a)

这是渐近最优的(最坏情况O(n + m)),并且由于any的短路,可能比交叉点方法更好。

例如:

lists_overlap([3,4,5], [1,2,3])

到达后将立即返回True 3 in sb

编辑:另一种变化(感谢Dave Kirby):

def lists_overlap(a, b):
  sb = set(b)
  return any(itertools.imap(sb.__contains__, a))

这依赖于imap用C实现的迭代器,而不是生成器理解。它还sb.__contains__用作映射功能。我不知道这会带来多少性能差异。它仍然会短路。


1
交叉方法中的循环全部使用C代码。您的方法中有一个循环,其中包含Python代码。一个大的未知数是一个空的交叉点是可能还是不可能。
John Machin

2
您还可以使用any(itertools.imap(sb.__contains__, a))应该更快的方法,因为它避免使用lambda函数。
戴夫·柯比

谢谢,@戴夫。:)我同意删除lambda是一个胜利。
马修·弗拉申

4

您还可以将其any与列表理解一起使用:

any([item in a for item in b])

6
您可以,但是时间为O(n * m),而设置相交方法的时间为O(n + m)。您也可以在没有列表理解的情况下进行操作(放开[]),它会运行得更快并且使用更少的内存,但是时间仍然是O(n * m)。
约翰·马钦

1
尽管您的大O分析是正确的,但我怀疑对于n和m较小的值,构建基础哈希表所花费的时间将会发挥作用。Big O忽略了计算哈希所花费的时间。
安东尼·科尼尔斯

2
建立“哈希表”摊销O(n)。
John Machin

1
我明白了,但是您丢掉的常数很大。对于大的n值无关紧要,但是对于小的n值却无关紧要。
Anthony Conyers,2010年

3

在python 2.6或更高版本中,您可以执行以下操作:

return not frozenset(a).isdisjoint(frozenset(b))

1
似乎不必提供集合或Frozenset作为第一个参数。我尝试了一个字符串,它起作用了(即:任何可迭代的方法都可以)。
2015年

2

您可以使用任何内置函数/ wa generator表达式:

def list_overlap(a,b): 
     return any(i for i in a if i in b)

正如John和Lie所指出的那样,当对于两个列表共享的每个i bool(i)== False时,这都会给出错误的结果。它应该是:

return any(i in b for i in a)

1
扩大Lie Ryan的评论:对于交叉点bool(x)为False的任何项x,都会给出错误的结果。在烈瑞安的例子,x为0。唯一的解决方法是any(True for i in a if i in b)哪个更好写成已经看到any(i in b for i in a)
John Machin 2010年

1
更正:将给出错误的结果时,所有的项目x在路口是这样的,bool(x)False
约翰·马钦

1

这个问题已经很老了,但是我注意到当人们在争论集合与列表时,没有人想到将它们一起使用。按照Soravux的示例,

清单的最坏情况:

>>> timeit('bool(set(a) & set(b))',  setup="a=list(range(10000)); b=[x+9999 for x in range(10000)]", number=100000)
100.91506409645081
>>> timeit('any(i in a for i in b)', setup="a=list(range(10000)); b=[x+9999 for x in range(10000)]", number=100000)
19.746716022491455
>>> timeit('any(i in a for i in b)', setup="a= set(range(10000)); b=[x+9999 for x in range(10000)]", number=100000)
0.092626094818115234

列表的最佳情况是:

>>> timeit('bool(set(a) & set(b))',  setup="a=list(range(10000)); b=list(range(10000))", number=100000)
154.69790101051331
>>> timeit('any(i in a for i in b)', setup="a=list(range(10000)); b=list(range(10000))", number=100000)
0.082653045654296875
>>> timeit('any(i in a for i in b)', setup="a= set(range(10000)); b=list(range(10000))", number=100000)
0.08434605598449707

因此,遍历一个列表以查看它是否在集合中比遍历两个列表更快,这是有意义的,因为检查数字是否在集合中需要固定的时间,而通过遍历列表进行检查所花费的时间与长度成正比。名单。

因此,我的结论是遍历一个列表,并检查它是否在set中


1
isdisjoint()如@Toughy所示,在(冻结)集合上使用该方法甚至更好:timeit('any(i in a for i in b)', setup="a= set(range(10000)); b=[x+9999 for x in range(10000)]", number=100000)=> 0.00913715362548828
Aktau,

1

如果您不在乎重叠的元素是什么,则只需检查len合并列表与合并为一组的列表的即可。如果有重叠的元素,则集合将更短:

len(set(a+b+c))==len(a+b+c) 如果没有重叠,则返回True。


如果第一个值重叠,无论大小如何,它仍会将整个列表转换为一个集合。
彼得·伍德

1

我将以一种功能性的编程风格来介绍另一个:

any(map(lambda x: x in a, b))

说明:

map(lambda x: x in a, b)

返回在其中的元素布尔值的列表b中找到a。然后any将该列表传递给,该列表仅返回True是否有任何元素True

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.