有效地检查一个元素是不是在列表中至少出现 n 次
Posted
技术标签:
【中文标题】有效地检查一个元素是不是在列表中至少出现 n 次【英文标题】:Efficiently check if an element occurs at least n times in a list有效地检查一个元素是否在列表中至少出现 n 次 【发布时间】:2017-03-14 01:40:25 【问题描述】:如何最好地编写 Python 函数 (check_list
) 以有效地测试元素 (x
) 在列表 (l
) 中是否至少出现 n
次?
我的第一个想法是:
def check_list(l, x, n):
return l.count(x) >= n
但是一旦x
被找到n
次并且总是 O(n),这不会短路。
一种简单的短路方法是:
def check_list(l, x, n):
count = 0
for item in l:
if item == x:
count += 1
if count == n:
return True
return False
我还有一个更紧凑的发电机短路解决方案:
def check_list(l, x, n):
gen = (1 for item in l if item == x)
return all(next(gen,0) for i in range(n))
还有其他好的解决方案吗?什么是最有效的方法?
谢谢
【问题讨论】:
您无法想出比 O(n) 更好的技术,因为您需要扫描整个列表进行检查。没有它,你无法得出结论。如果你有一个排序列表,那么情况就不同了。list.count
在大多数情况下会更快。它不会短路,但是(在 CPython 中)它是在 C 中实现的,可以很容易地弥补这一点。主要替代方法是在循环中调用index
,这会短路但需要多次调用。
@Veedrac 关于 C 实现的好点,但我想这取决于列表有多大以及它可以多早短路。 index
方法是什么意思?
@Chris_Rands 我指的是 trincot 的方法。
您的赏金理由表明它没有受到太多关注,到目前为止我看到了 2 个很好的答案。您不认为在这里详细说明“注意”的含义是个好主意吗?
【参考方案1】:
c=0
for i in l:
if i==k:
c+=1
if c>=n:
print("true")
else:
print("false")
【讨论】:
这比 OP 的原始代码更糟糕,因为它没有短路(注意标题,“Efficiently”)。如果l
有 1000 个项目并且有 [4,5,4,.... (997 others not 4]
并且您正在检查是否有 2 个 4
,那么您的代码将遍历 all 1000 个项目,即使它已经找到 2 个 @987654325 @的。【参考方案2】:
如果您预计大量案例会导致提前终止,那么最终短路是可行的方法。让我们探索一下可能性:
以list.index
方法与list.count
方法为例(根据我的测试,这两个是最快的,虽然 ymmv)
对于list.index
,如果列表包含 n 个或更多 x 并且该方法被调用 n 次。在 list.index 方法中,执行速度非常快,允许比自定义生成器更快的迭代。如果 x 的出现相距足够远,则从index
的较低级别执行将看到很大的加速。如果 x 的实例靠得很近(更短的列表/更常见的 x),则将花费更多的时间来执行调解函数其余部分的较慢的 python 代码(循环n
并递增i
)
list.count
的好处是它可以完成慢速 python 执行之外的所有繁重工作。这是一个更容易分析的函数,因为它只是 O(n) 时间复杂度的一个例子。通过几乎不花时间在 python 解释器上,但是几乎可以保证短列表的速度更快。
选择标准总结:
较短的列表有利于list.count
任何长度的列表都不太可能短路支持list.count
列表很长,可能会短路青睐list.index
【讨论】:
您在基准测试中包含了生成器解决方案?你能在这里包括代码/时间吗? @Chris_Rands 生成器只是原始问题中您的。我编写的唯一代码是计时和测试。然后我改变l
(整数的随机列表)的长度和范围,并改变n
以获得不同的短路概率,以及x
实例之间的长度
@Chris_Rands 搜索 x
的下一个实例将始终为 O(n),除非列表已排序(O(log n!) 无论如何都慢)。因此,如果统计数据告诉你这是值得的,那么短路,并尽可能少花时间在 python 解释器中
最好包含 Moses 的生成器解决方案(除非您是这个意思);他已经包含了一个基准 + donkopotamus 的评论
@Chris_Rands 我无法重现两者之间的任何有意义的差异。我也不能让它们比list.index
方法更快。如果列表长度很长并且提前终止的可能性很高,我可以让它们比list.count
方法更快,但是这种情况也有利于list.index
。【参考方案3】:
您可以使用index
的第二个参数来查找出现的后续索引:
def check_list(l, x, n):
i = 0
try:
for _ in range(n):
i = l.index(x, i)+1
return True
except ValueError:
return False
print( check_list([1,3,2,3,4,0,8,3,7,3,1,1,0], 3, 4) )
关于index
参数
官方文档在其Python Tutuorial, section 5中没有提到该方法的第二个或第三个参数,但是你可以在更全面的Python Standard Library, section 4.6中找到它:
s.index(x[, i[, j]])
s 中第一次出现 x 的索引(在索引 i 处或之后和索引之前j) (8)(8)
index
在 s 中找不到 x 时引发ValueError
。支持时,索引方法的附加参数允许有效搜索序列的子部分。传递额外的参数大致相当于使用s[i:j].index(x)
,只是不复制任何数据并且返回的索引是相对于序列的开头而不是切片的开头。
性能比较
在比较list.index
方法和islice(gen)
方法时,最重要的因素是要找到的事件之间的距离。一旦该距离平均为 13 或更大,list.index
具有更好的性能。对于较短的距离,最快的方法还取决于要查找的出现次数。要找到的出现次数越多,islice(gen)
方法在平均距离方面的表现就越快:当出现次数变得非常大时,这种增益就会消失。
下图绘制了(近似的)边界线,在该边界线上两种方法的表现都一样好(X 轴是对数):
【讨论】:
谢谢 我不知道list.index
有第二个参数,它没有在文档中列出docs.python.org/3.6/tutorial/datastructures.html
更全面的文档在库参考中。我在答案中添加了一个关于此的部分。
我实际上将其提交为文档错误bugs.python.org/issue28587
你不应该;本教程的设计并不全面。引自the Tutorial:“本教程并未试图全面涵盖每一个功能......”.
错误问题已关闭,list.index
上的文档现已更新(适用于 Python 3.6)docs.python.org/3.6/tutorial/datastructures.html【参考方案4】:
这显示了另一种方法。
-
对列表进行排序。
查找第一次出现的项目的索引。
将索引增加一比项目必须出现的次数少一。 (n - 1)
查找该索引处的元素是否与您要查找的项目相同。
def check_list(l, x, n):
_l = sorted(l)
try:
index_1 = _l.index(x)
return _l[index_1 + n - 1] == x
except IndexError:
return False
【讨论】:
排序列表很可能是O(n log n),那么list.index
是O(n),所以到2结束。这显然不是一个好办法
您的语言有点难以理解。注意澄清@Chris_Rands【参考方案5】:
我建议使用collections
模块中的Counter
。
from collections import Counter
%%time
[k for k,v in Counter(np.random.randint(0,10000,10000000)).items() if v>1100]
#Output:
Wall time: 2.83 s
[1848, 1996, 2461, 4481, 4522, 5844, 7362, 7892, 9671, 9705]
【讨论】:
我认为这不能回答我的问题【参考方案6】:您可以使用itertools.islice
来推进生成器@,而不是通过设置range
对象和使用必须测试每个项目的真实性 的all
来产生额外开销987654325@ 前进,如果切片存在则返回切片中的 next 项,如果不存在则返回默认的False
:
from itertools import islice
def check_list(lst, x, n):
gen = (True for i in lst if i==x)
return next(islice(gen, n-1, None), False)
注意,像list.count
,itertools.islice
也以 C 速度运行。这具有处理非列表的可迭代对象的额外优势。
一些时机:
In [1]: from itertools import islice
In [2]: from random import randrange
In [3]: lst = [randrange(1,10) for i in range(100000)]
In [5]: %%timeit # using list.index
....: check_list(lst, 5, 1000)
....:
1000 loops, best of 3: 736 µs per loop
In [7]: %%timeit # islice
....: check_list(lst, 5, 1000)
....:
1000 loops, best of 3: 662 µs per loop
In [9]: %%timeit # using list.index
....: check_list(lst, 5, 10000)
....:
100 loops, best of 3: 7.6 ms per loop
In [11]: %%timeit # islice
....: check_list(lst, 5, 10000)
....:
100 loops, best of 3: 6.7 ms per loop
【讨论】:
谢谢,这是对我的解决方案的巧妙修改 这具有在任意迭代器上工作的优势,但如果你知道你正在处理一个列表,它的速度大约是list.index
的两倍
@donkopotamus 这并不完全正确。我添加了一些时间
@MosesKoledoye 我的测试时间是:lst = [random.randrange(0, 100) for i in range(10000)]
,%timeit check_list_index(lst, 0, 45)
=> 64.8 µs 和 %timeit check_list_iter(lst, 0, 45)
=> 127 µs。进一步调查,islice
比index
更快,当出现之间的隐含差距较小时(如使用范围 [1, 10) 中的随机数所暗示的那样)。一旦差距变大index
获胜……例如,如果您将10
撞到20
,然后50
,然后100
,您会看到指数向前拉。如果你让它更小(例如将10
更改为5
或3
),则迭代器获胜。课程用马
感谢您的时间安排,我假设这些时间不包括import
时间。鉴于最佳性能取决于列表大小/结构,我猜list.count
有时也会是最好的,所以也许没有明确的赢家@donkopotamus【参考方案7】:
另一种可能是:
def check_list(l, x, n):
return sum([1 for i in l if i == x]) >= n
【讨论】:
恐怕我认为这只会比其他选择更糟糕。它不会短路,也不会从 CPython 中的 C 实现中受益。以上是关于有效地检查一个元素是不是在列表中至少出现 n 次的主要内容,如果未能解决你的问题,请参考以下文章
过滤具有多对多关系的对象,检查它是不是包含列表中的至少一个元素
在比较列表中的元素时,如何有效地迭代并提高 O(n^2) 的时间复杂度?