Python: 从列表中删除大量项目

11

我正处于一个我一直在工作的项目的最后阶段。一切都很顺利,但是我有一个瓶颈问题,我无法解决。

我有一个元组列表。该列表长度范围从 40,000 到 1,000,000 条记录不等。现在我有一个字典,其中每个 (value, key) 对都是列表中的一个元组。

所以,我可能会有以下情况:

myList = [(20000, 11), (16000, 4), (14000, 9)...]
myDict = {11:20000, 9:14000, ...}

我想从列表中删除每个(v, k)元组。

目前我正在做:

for k, v in myDict.iteritems():
    myList.remove((v, k))

从包含20,000个元组的列表中删除838个元组需要3-4秒钟。我很可能要从1,000,000个元组的列表中删除更多,因此需要更快的方法。

有更好的方法吗?

如果需要,我可以提供用于测试的代码以及实际应用程序的pickled数据。

8个回答

20

你需要进行测量,但我可以想象这种方式更加高效:

myList = filter(lambda x: myDict.get(x[1], None) != x[0], myList)

因为在字典中查找更适合这种情况。需要注意的是,在删除旧列表之前,这将创建一个新列表;因此会有内存上的权衡。如果这是一个问题,那么重新考虑容器类型,就像jkp建议的那样。

编辑: 但要小心,如果 None 实际上在您的列表中 - 您必须使用不同的“占位符”。


1
哇,这将我的测试时间从3.2秒降至0.025秒...我想我们可能有一个赢家 - 至少在Alex Martelli发表意见之前 :) - sberry
2
我可以接受在他之后成为第二名 :-) - balpha
如果你测量的是25毫秒,实际的墙上时间可能会比这个更小——这可能是你的操作系统计时器分辨率将其“舍入”到25毫秒。例如,尝试进行1000次运行的平均值。 - Mark Rushakoff
运行测试2000次。平均时间为0.024。太棒了! - sberry
2
真糟糕,昨天我很忙——啊,好吧,即使晚了,我还是会发布我的答案;-)。 - Alex Martelli

9

如果要从大约1,000,000个列表中删除约10,000个元组,如果值是可哈希的,则最快的方法应该是:

totoss = set((v,k) for (k,v) in myDict.iteritems())
myList[:] = [x for x in myList if x not in totoss]

准备好这个集合是一次小的固定成本,它可以避免做元组解包和重新打包或元组索引很多次。与其将值赋给myList,赋值给myList[:]在语义上也很重要(如果还有其他对myList的引用,仅重新绑定名称是不够的 - 你真的想要重新绑定内容!)。
抱歉,我没有你的测试数据来自己进行时间测量,但是,让我知道它在你的测试数据上表现如何!
如果值不可哈希(例如,它们是子列表),最快的方法可能是:
sentinel = object()
myList[:] = [x for x in myList if myDict.get(x[0], sentinel) != x[1]]

或者(这样做应该没有太大区别,但我认为前者更好——索引比解包和重新打包更便宜):
sentinel = object()
myList[:] = [(a,b) for (a,b) in myList if myDict.get(a, sentinel) != b]

在这两个变体中,哨兵习惯用法用于防止 None 值的出现(如果值是可哈希的,则首选基于集合的方法不会有问题!),因为它比 if a not in myDict or myDict[a] != b 更便宜(需要对 myDict 进行两次索引)。

1
我想我们都期待着看到你的答案。(注意:你的第一行代码有一个小错别字('i')) - Anon
1
谢谢指出笔误,我现在正在修复。 - Alex Martelli

5
每次调用myList.remove时,Python都要扫描整个列表以搜索并删除该项。在最坏的情况下,每次查找的每个项都位于列表末尾。您是否尝试过执行“反向”操作:
newMyList = [(v,k) for (v,k) in myList if not k in myDict]

但我真的不确定那种方法在扩展性方面有多好,因为你将复制原始列表 -- 可能会使用大量内存。

在这里最好的替代方案可能是等待Alex Martelli发布一些令人惊叹的直观、简单和高效的方法。


这比我的原始代码快得多。然而,它比balpha和Nick Lewis的答案慢3-4倍左右。 - sberry

2
这个问题在我看来似乎是因为你正在使用一个无序的类型list作为你要移除的容器。因此,要找到列表中的每个项目都是一个线性操作(O(n)),它必须遍历整个列表直到找到匹配项。
如果你可以将list替换为某个其他容器(例如set),该容器使用每个项目的hash()进行排序,则每个匹配项可以更快地执行。
以下代码展示了如何使用我和Nick在此线程上提供的思路的组合来实现此目标:
list_set = set(original_list)
dict_set = set(zip(original_dict.values(), original_dict.keys()))
difference_set = list(list_set - dict_set)
final_list = []
for item in original_list:
    if item in difference_set:
        final_list.append(item)

没错,然而我需要它们是有序的。一开始我使用字典来存储myList中每个(k,v)对应的v:k。但是因为我需要这些数据是有序的,所以每次添加、修改数据时都需要对字典中的k,v对进行排序。 - sberry
好的,如果你采用Nick Lewis提供的答案,那么一旦你有了要保留的项目集合,你可以执行以下操作:遍历原始列表并查询每个项目的成员资格:如果该项目在集合中,则将其附加到最终列表中。你最终会得到一个你想要的有序项目列表。 - jkp

2
[(i, j) for i, j in myList if myDict.get(j) != i]

这与balpha的代码相同,只是使用列表推导式而不是filter()函数。 - hughdbrown
这应该与Mark Rushakoff的相同。 - hughdbrown
这与balpha或Mark Rushakoff的不同吗?如注释所述,这与balpha的不同之处在于使用了列表推导式,而与Mark Rushakoff的不同之处在于使用“if myDict.get(j)!= i”而不是“if not k in myDict”。如果键存在但未映射到相同的值,则后者可能会有所不同。您强调的就是这个区别吗? - hughdbrown
你想证明什么?你的意思是基本算法是遍历列表并检查字典中相应值的代码都是一样的吗?因为如果这是你的定义,那么这个页面上的所有答案都是一样的! - SilentGhost
我并没有证明任何事情。我也不确定你为什么生气。我认为集合操作与使用列表推导/过滤器是完全不同的解决方案。我认为从列表中删除元素与构建新列表是不同的。我认为有趣的是,“[使用in]的运行速度几乎是使用[dict].get(key)的两倍,因为in运算符比方法调用更快。” [Python Essential Reference, p.197]在Python中构建算法有很多有趣的可能性,有些相似,有些不同。 - hughdbrown

2
尝试像这样做:

尝试像这样做:

myListSet = set(myList)
myDictSet = set(zip(myDict.values(), myDict.keys()))
myList = list(myListSet - myDictSet)

这将把myList转换成一个集合,交换myDict中的键/值并放入一个集合中,然后找到差异,将其转换回列表,并将其重新赋值给myList. :)

这里的时间非常接近于balpha建议的时间。它们之间相差约4毫秒。对于更大的列表,有可能有一个更好的选择吗? - sberry
balpha的可能消耗更少的内存。 - recursive

0
[i for i in myList if i not in list(zip(myDict.values(), myDict.keys()))]

2
你试过这个吗?我读了一下你的代码,发现你在列表中进行线性搜索元组——所以整个操作的时间复杂度是O(n^2)。到目前为止,每一个得到赞同的解决方案都比这个具有更好的性能。 - hughdbrown
这也会为每个项目评估右侧的表达式 - 每次都要遍历dict - agf

0
一个包含一百万个二元组的列表在大多数运行Python的机器上并不算大。然而,如果你绝对必须在原地进行删除,下面是一个正确且干净的方法:
def filter_by_dict(my_list, my_dict):
    sentinel = object()
    for i in xrange(len(my_list) - 1, -1, -1):
        key = my_list[i][1]
        if my_dict.get(key, sentinel) is not sentinel:
            del my_list[i]

更新 实际上,每个删除操作都需要使用C语言的memmove()将列表指针向下移动,因此如果有d个删除操作,则时间复杂度为O(n*d)而不是O(n**2)。请注意:(1)原帖中建议d约等于0.01 * n,(2)O(n*d)的工作量是将一个指针复制到内存中的其他位置...因此这种方法实际上可能比一眼看上去要快一些。有人做过基准测试吗?

在从字典中删除项目后,您打算如何处理列表?是否可以将字典过滤与下一步操作结合起来?


如果你要这样做,最好生成要删除的键列表并按相反的顺序执行。在我看来,这种方法更符合惯用法。delete_me = [i for i, v in enumerate(my_list) if v not in my_dict]; for i in reversed(delete_me): del my_list[i];此外,Beazley声称in运算符比dict.get()方法更快,供参考。 - hughdbrown
删除我 = [i for i,v in enumerate(my_list) if v[1]不在我的字典中]; - hughdbrown
(1) 如果用三个步骤(包括构建临时列表和反转它)是“惯用的”,那么“惯用的”就不好。 (2) 使用dict.get与OP使用list.remove具有相同的语义:列表和字典之间的k和v必须匹配。OP没有表明其他情况。 (3) 无论如何,您的意思是“v [1]在我的dict中”,而不是“v [1]不在dict中”-- dict包含要删除的内容。非常过早的优化;-) - John Machin
有一种观点认为,“xrange(len(my_list) - 1, -1, -1)”不如“reversed(xrange(len(my_list)))”好。我知道我曾因建议使用步长为-1的range/xrange()而被踩过。 - hughdbrown

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接