繁体   English   中英

检查列表中是否正好有n个项目与python中的条件匹配的最快方法

[英]Fastest way to check if exactly n items in a list match a condition in python

如果我在列表中有m个项目,最快的方法是检查列表中的n个是否满足特定条件? 例如:

l = [1,2,3,4,5]

如何检查列表中是否有任何两项符合条件x%2 == 0

天真的方法是使用嵌套的for循环:

for i in l:
    for j in l:
        if not i%2 and not j%2:
            return True

但这是一种极其低效的检查方法,如果我要检查2到1千万个项目中的任何50,000个项目,这将变得尤为难看。

[编辑以反映精确匹配,我们仍然可以通过短路来完成!]

我想您希望它短路(确定后停止,不仅结束时停止):

matched = 0
for i in l:
    if i%2 == 0:
        matched += 1
        if matched > 2: # we now have too many matches, stop checking
            break
if matched == 2:
    print("congratulations")

如果您想对相同的输入数据进行多次查询的速度更快,则应改用NumPy(不要出现短路):

l = np.array([1,2,3,4,5])

if np.count_nonzero(l%2 == 0) == 2:
    print "congratulations"

这不会短路,但是一旦构建了输入数组,它将是超快的,因此,如果您有一个很大的输入列表并且要执行许多查询,并且查询不能很早就短路,这可能会更快。 潜在地增加了一个数量级。

True sum相加的和解是正确的,可能比显式循环更有效,并且绝对是最简洁的:

if sum(i % 2 == 0 for i in lst) == n:

但是,它依赖于理解,在像加法这样的整数上下文中, True计为1False0 您可能不想依靠它。 在这种情况下,您可以重写它(squiguy的答案):

if sum(1 for i in lst if i % 2 == 0) == n:

但是您可能需要将此因素分解为一个函数:

def count_matches(predicate, iterable):
    return sum(predicate(i) for i in iterable)

在这一点上, filter列表并计算结果过滤后的可迭代长度可能会更具可读性:

def ilen(iterable):
    return sum(1 for _ in iterable)

def count_matches(predicate, iterable):
    return ilen(filter(predicate, iterable))

但是,所有这些变体的缺点(就像使用mapfilter是谓词必须是一个函数 ,而不仅仅是一个表达式。 当您只想检查some_function(x)返回True时很好,但是当您想检查x % 2 == 0 ,则必须执行将其包装在函数中的额外步骤,如下所示:

if count_matches(lambda x: x %2 == 0, lst) == n

…在这一点上,我认为您失去的可读性超过获得的可读性。


由于您要求最快的速度(即使这可能被误导了),因为我确信这些解决方案中的任何一种对于几乎所有应用程序来说都足够快,而且无论如何这都不是一个热点,因此这里有一些使用64位测试我的计算机上长度为250的CPython 3.3.2:

32.9 µs: sum(not x % 2 for x in lst)
33.1 µs: i=0\nfor x in lst: if not x % 2: i += 1\n
34.1 µs: sum(1 for x in lst if not x % 2)
34.7 µs: i=0\nfor x in lst: if x % 2 == 0: i += 1\n
35.3 µs: sum(x % 2 == 0 for x in lst)
37.3 µs: sum(1 for x in lst if x % 2 == 0)
52.5 µs: ilen(filter(lambda x: not x % 2, lst))
56.7 µs: ilen(filter(lambda x: x % 2 == 0, lst))

因此,事实证明,至少在64位CPython 3.3.2中,是使用显式循环,对False和True求和,还是对True求和,则取1。 在某些情况下,使用not而不是== 0会比其他情况产生更大的差异; 但即使是最坏的情况,也只比最坏的情况差12%。

因此,我将使用您认为可读性最高的一种。 而且,如果最慢的速度不够快,那么最快的速度可能也不足够,这意味着您可能需要重新排列应用程序以使用NumPy,在PyPy中运行应用程序而不是CPython,编写自定义的Cython或C代码,或者做一些比重新组织这个琐碎的算法更激烈的事情。

为了进行比较,下面是一些NumPy实现(假设lstnp.ndarray而不是list ):

 6.4 µs: len(lst) - np.count_nonzero(lst % 2)
 8.5 µs: np.count_nonzero(lst % 2 == 0)
17.5 µs: np.sum(lst % 2 == 0)

即使是最明显的NumPy转换速度也几乎快一倍。 只需一点工作,您就可以将其速度提高3倍。

这是在PyPy(3.2.3 / 2.1b1)中而不是CPython中运行完全相同的代码的结果:

14.6 µs: sum(not x % 2 for x in lst)

速度快一倍以上,而无需更改代码。

您可能要研究numpy

例如:

In [16]: import numpy as np 
In [17]: a = np.arange(5)

In [18]: a
Out[18]: array([0, 1, 2, 3, 4])

In [19]: np.sum(a % 2 == 0)
Out[19]: 3

时间:

In [14]: %timeit np.sum(np.arange(100000) % 2 == 0)
100 loops, best of 3: 3.03 ms per loop

In [15]: %timeit sum(ele % 2 == 0 for ele in range(100000))
10 loops, best of 3: 17.8 ms per loop

但是,如果您要考虑从listnumpy.array转换,则numpy不会更快:

In [20]: %timeit np.sum(np.array(range(100000)) % 2 == 0)
10 loops, best of 3: 23.5 ms per loop

编辑:

@abarnert的解决方案是最快的:

In [36]: %timeit(len(np.arange(100000)) - np.count_nonzero(a % 2))
10000 loops, best of 3: 80.4 us per loop

我会用while循环:

l=[1,2,3,4,5]

mods, tgt=0,2
while mods<tgt and l:
    if l.pop(0)%2==0:
        mods+=1

print(l,mods)  

如果您担心“最快”,请用双端队列替换此列表:

from collections import deque

l=[1,2,3,4,5]
d=deque(l)
mods, tgt=0,2
while mods<tgt and d:
    if d.popleft()%2==0: mods+=1

print(d,mods)     

无论哪种情况,它都易于阅读,并且在满足条件时会短路。

确实与短路写做精确匹配:

from collections import deque

l=[1,2,3,4,5,6,7,8,9]
d=deque(l)
mods, tgt=0,2
while mods<tgt and d:
    if d.popleft()%2==0: mods+=1

print(d,mods,mods==tgt)
# deque([5, 6, 7, 8, 9]) 2 True
# answer found after 4 loops


from collections import deque

l=[1,2,3,4,5,6,7,8,9]
d=deque(l)
mods, tgt=0,2
while mods<tgt and d:
    if d.popleft()%9==0: mods+=1

print(d,mods,mods==tgt)
# deque([]) 1 False
# deque exhausted and less than 2 matches found...

您还可以在列表上使用迭代器:

l=[1,2,3,4,5,6,7,8,9]
it=iter(l)
mods, tgt=0,2
while mods<tgt:
    try:
        if next(it)%2==0: mods+=1
    except StopIteration:
        break

print(mods==tgt)   
# True

您可以使用条件中内置的sum ,并检查它是否等于n值。

l = [1, 2, 3, 4, 5]
n = 2
if n == sum(1 for i in l if i % 2 == 0):
    print(True)

为什么不只使用filter()?

例如:检查清单中的偶数个整数:

>>> a_list = [1, 2, 3, 4, 5]
>>> matches = list(filter(lambda x: x%2 == 0, a_list))
>>> matches
[2, 4]

然后,如果您想要匹配的数量:

>>> len(matches)
2

最后是您的答案:

>>> if len(matches) == 2:
        do_something()

构建一个生成器,为符合条件的每个项返回1 ,并将该生成器限制为最多n + 1个项,并检查这些项的总和等于您要的数字,例如:

from itertools import islice

data = [1,2,3,4,5]
N = 2
items = islice((1 for el in data if el % 2 == 0), N + 1)
has_N = sum(items) == N

这有效:

>>> l = [1,2,3,4,5]
>>> n = 2
>>> a = 0  # Number of items that meet the condition
>>> for x in l:
...     if x % 2 == 0:
...         a += 1
...         if a > n:
...             break
...
>>> a == n
True
>>>

它具有只运行一次列表的优点。

Itertools是列表拖曳任务的有用快捷方式

import itertools

#where expr is a lambda, such as 'lambda a: a % 2 ==0'
def exact_match_count ( expr, limit,  *values):
    passes = itertools.ifilter(expr, values)
    counter = 0
    while counter <= limit + 1:
        try:
            passes.next()
            counter +=1
        except:
            break
    return counter == limit

如果您担心内存限制,请调整签名,以便* values是生成器而不是元组

任何寻求“最快解决方案”的人都需要对输入进行一次遍历,然后进行一次提前淘汰。

这是解决方案的一个很好的基准起点:

>>> s = [1, 2, 3, 4, 5]
>>> matched = 0
>>> for x in s:
        if x % 2 == 0:
            matched += 1
            if matched > 2:
                print 'More than two matched'
else:
    if matched == 2:
        print 'Exactly two matched'
    else:
        print 'Fewer than two matched'


Exactly two matched

以下是一些改进算法上正确的基准线解决方案的想法:

  1. 优化条件的计算。 例如,将x % 2 == 0替换为not x & 1 这称为强度降低

  2. 本地化变量。 由于全局查找和赋值比局部变量赋值更昂贵,因此如果在函数内部,则完全匹配测试将运行得更快。

    例如:

     def two_evens(iterable): 'Return true if exactly two values are even' matched = 0 for x in s: if x % 2 == 0: matched += 1 if matched > 2: return False return matched == 2 
  3. 通过使用itertools驱动循环逻辑,可以消除解释器的开销。

    例如, itertools.ifilter()可以以C速度隔离匹配:

     >>> list(ifilter(None, [False, True, True, False, True])) [True, True, True] 

    同样, itertools.islice()可以以C速度实现早期逻辑:

     >>> list(islice(range(10), 0, 3)) [0, 1, 2] 

    内置的sum()函数可以以C速度计算匹配项。

     >>> sum([True, True, True]) 3 

    将它们放在一起检查匹配的确切数目:

     >>> s = [False, True, False, True, False, False, False] >>> sum(islice(ifilter(None, s), 0, 3)) == 2 True 
  4. 仅当这是实际程序中的实际瓶颈时,这些优化才值得做。 通常只有在您要进行许多这种完全匹配计数测试时才会发生这种情况。 如果是这样,则可以通过在第一次通过时缓存一些中间结果,然后在后续测试中重复使用这些中间结果,从而进一步节省成本。

    例如,如果存在复杂条件,则可能会缓存和重用子条件结果。

    代替:

     check_exact(lambda x: x%2==0 and x<10 and f(x)==3, dataset, matches=2) check_exact(lambda x: x<10 and f(x)==3, dataset, matches=4) check_exact(lambda x: x%2==0 and f(x)==3, dataset, matches=6) 

    预计算所有条件(每个数据值一次):

     evens = map(lambda x: x%2==0, dataset) under_tens = map(lambda x: x<10, dataset) f_threes = map(lambda x: x%2==0 and f(x)==3, dataset) 

一种简单的方法:

def length_is(iter, size):
    for _ in xrange(size - 1):
        next(iter, None)

    try:
        next(iter)
    except StopIteration:
        return False  # too few

    try:
        next(iter)
        return False  # too many
    except StopIteration:
        return True
length_is((i for i in data if x % 2 == 0), 2)

这是一种稍微愚蠢的写法:

class count(object):
    def __init__(self, iter):
        self.iter = iter

    __eq__ = lambda self, n: length_is(self.iter, n)

给予:

count(i for i in data if x % 2 == 0) == 2

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM