从列表中删除多个元素

206

有没有可能同时从列表中删除多个元素?如果我想删除索引为0和2的元素,并尝试类似于del somelist [0]然后跟着del somelist [2],第二条语句实际上会删除somelist[3]

我猜我可以总是先删除较高编号的元素,但我希望有更好的方法。


如果你关心效率,可以使用多个切片。 - tejasvi88
32个回答

223

由于某些原因,我不喜欢这里的任何答案。 是的,它们起作用,但严格来说,它们大多数并没有删除列表中的元素,对吧?(而是制作一个副本,然后用编辑过的副本替换原来的副本)。

为什么不首先删除较高的索引呢?

这样做有什么原因吗? 我只会这样做:

for i in sorted(indices, reverse=True):
    del somelist[i]

如果你真的不想向后删除项目,那么我猜你应该只减少索引值,这些索引值大于最后删除的索引(不能使用相同的索引,因为你有一个不同的列表),或者使用列表的副本(这不是“删除”,而是用编辑过的副本替换原始列表)。

我在这里错过了什么,有任何不删除反向排序的原因吗?


10
有两个原因。(a) 对于列表,时间复杂度通常会高于“制作副本”方法(使用索引集合),因为某些元素需要多次向前移动。(假设是随机索引)(b) 至少对我来说,这样很难阅读,因为存在一个排序函数,它与任何实际的程序逻辑都不对应,仅存在于技术原因上。即使我现在已经彻底理解了逻辑,我仍然感觉它很难阅读。 - Imperishable Night
1
@ImperishableNight,你能详细说明一下(a)吗?我不明白"有些元素需要移动"的意思。对于(b),如果你需要更好的可读性,可以定义一个函数。 - tglaria
值得一提的是,如果您想对要删除的项目执行某些操作,您也可以使用 somelist.pop(i) 而不是 del - user5359531
@tglaria 列表占据一段连续的内存空间;因此,如果您删除除最后一个元素以外的任何元素,则必须将“右侧”的元素向“左侧”移动。(在此,我使用“左侧”表示列表的开头,“右侧”表示列表的末尾) - luizfls
@luizfls 这很有趣。我想知道是否需要任何资源。将地址重新定位到列表的下一个元素而不是移动列表的元素会更容易吗? - tglaria
1
@tglaria 这是可能的,也是链表的实现方式。你会失去“Python列表”的连续性(单个块),这使你可以随机访问,即能够在常数时间内访问任何元素。在链表中,元素分散在内存中,你需要存储地址来指向下一个元素。另一方面,正如你所建议的,删除后不需要移动元素。因此,这是一个权衡问题。 - luizfls

141

您可以使用 enumerate 并删除其索引与要删除的索引匹配的值:

indices = 0, 2
somelist = [i for j, i in enumerate(somelist) if j not in indices]

3
如果您删除整个列表,那么结果就是“len(indices) * len(somelist)”(indices和somelist的长度相乘)。这也会创建一个副本,这可能是需要的,也可能不需要。 - Richard Levasseur
5
我选择元组作为索引的原因仅仅是为了记录的简单性。使用set()将会非常适合,时间复杂度为O(n)。 - SilentGhost
35
这并不是从 somelist 中删除元素,而是创建了一个全新的列表。如果有任何东西仍然持有对原始列表的引用,它仍将保留其中所有的项目。 - Tom Future
2
@SilentGhost 不需要进行枚举。这个怎么样:somelist = [ lst[i] for i in xrange(len(lst)) if i not in set(indices) ] - ToolmakerSteve
3
改变了自己的想法。通过给enumerate函数的结果命名更具描述性,这种方法更容易阅读。如果添加括号,它也会对我有所帮助。即:[ value for (i, value) in enumerate(lst) if i not in set(indices) ] - ToolmakerSteve
显示剩余7条评论

130

如果你要删除多个不相邻的项目,那么你所描述的方法是最好的(是的,请确保从最高索引开始)。

如果你的项目是相邻的,你可以使用切片赋值语法:

a[2:10] = []

114
你也可以使用del a[2:10]达到相同的效果。 - sth
8
有趣的是,del 操作比赋值略快。 - thefourtheye

36
您可以如下使用numpy.delete:
import numpy as np
a = ['a', 'l', 3.14, 42, 'u']
I = [0, 2]
np.delete(a, I).tolist()
# Returns: ['l', '42', 'u']

如果您不介意最终得到一个numpy数组,那么可以省略.tolist()。您应该会看到一些相当大的速度提升,使这成为一种更具可扩展性的解决方案。虽然我没有进行基准测试,但numpy操作是用C或Fortran编写的编译代码。


1
元素不连续加1的通用解决方案 - noɥʇʎԀʎzɐɹƆ
1
问题在于,删除['a', 42]怎么样? - evanhutomo
1
相比其他解决方案,这种方法可以获得巨额奖励积分,因为它更快。我可以说的是,在处理非常大的数据集时,使用好用的numpy只需要几秒钟即可完成,而使用其他解决方案则需要数分钟的时间。 - legel

18

作为 Greg 回答的一个特例,你甚至可以使用扩展切片语法。例如,如果你想删除项 0 和 2:

>>> a= [0, 1, 2, 3, 4]
>>> del a[0:3:2]
>>> a
[1, 3, 4]

当然,这并不能涵盖任意选择,但对于删除任意两个项目肯定可行。


16

作为一个函数:

def multi_delete(list_, *args):
    indexes = sorted(list(args), reverse=True)
    for index in indexes:
        del list_[index]
    return list_

运行时间为n log(n),这应该是迄今为止最快的正确解决方案。


1
使用 args.sort().reverse() 的版本肯定更好。它还可以处理字典而不是抛出异常或者更糟的是悄悄地损坏数据。 - Roger Pate
sort() 对于元组未定义,您需要先将其转换为列表。sort() 返回 None,因此您无法在其上使用 reverse()。 - SilentGhost
@Nikhil:不,你没有 ;) args = list(args) args.sort() args.reverse() 但更好的选择是:args = sorted(args, reverse=True) - SilentGhost
@JoãoPortela 是正确的。在第一次编辑之后,将 list(args) 放入 sorted() 中,list() 包装器不再必要,正如 SilentGhost 的回复评论所述。使用元组输入进行测试和验证。已提交为编辑。 - ToolmakerSteve
5
n log n?真的吗?我不认为 del list[index] 的时间复杂度是 O(1)。 - user202729
显示剩余8条评论

12

所以,您基本上想一次性删除多个元素?在这种情况下,要删除的下一个元素的位置将偏移由之前删除的元素数量确定。

我们的目标是删除所有预先计算出来的元音字母,它们的索引分别为1、4和7。请注意,to_delete索引按升序排列很重要,否则它将无法正常工作。

to_delete = [1, 4, 7]
target = list("hello world")
for offset, index in enumerate(to_delete):
  index -= offset
  del target[index]

如果您想以任何顺序删除元素,那么这将变得更加复杂。在我看来,对to_delete进行排序可能比弄清何时应该或不应该从index中减去更容易。


9

我是Python的完全新手,目前我的编程水平可以说是相当粗糙和低劣的。但是我采用了一种方法,结合我在早期教程中学到的基本命令:

some_list = [1,2,3,4,5,6,7,8,10]
rem = [0,5,7]

for i in rem:
    some_list[i] = '!' # mark for deletion

for i in range(0, some_list.count('!')):
    some_list.remove('!') # remove
print some_list

很明显,由于必须选择一个“标记删除”字符,这有其局限性。

至于随着列表规模的增长,性能如何,我相信我的解决方案是次优的。然而,它很简单,我希望它能吸引其他初学者,并且在一些简单的情况下能够起作用,比如some_list具有已知的格式,例如始终为数字...


4
使用None代替'!'作为您的特殊字符,这样可以使每个字符有效,并且释放出更多可能性。 - benathon

6

这里有一种替代方法,不使用enumerate()创建元组(就像SilentGhost的原始回答中所示)。

我认为这种方法更易读。(也许如果我习惯使用enumerate()的话会感觉不同。)注意:我没有测试这两种方法的性能。

# Returns a new list. "lst" is not modified.
def delete_by_indices(lst, indices):
    indices_as_set = set(indices)
    return [ lst[i] for i in xrange(len(lst)) if i not in indices_as_set ]

注意:此处使用的是Python 2.7语法。对于Python 3,xrange => range

用法:

lst = [ 11*x for x in xrange(10) ]
somelist = delete_by_indices( lst, [0, 4, 5])

somelist:

[11, 22, 33, 66, 77, 88, 99]

--- 奖励 ---

从列表中删除多个值。也就是说,我们有要删除的值:

# Returns a new list. "lst" is not modified.
def delete__by_values(lst, values):
    values_as_set = set(values)
    return [ x for x in lst if x not in values_as_set ]

使用方法:

somelist = delete__by_values( lst, [0, 44, 55] )

somelist:

[11, 22, 33, 66, 77, 88, 99]

这是与之前相同的答案,但这次我们提供了要删除的值[0, 44, 55]


我决定 @SilentGhost 的代码难以阅读,是因为变量名没有描述性,不利于对 enumerate 结果的理解。同时加上括号可以提高可读性。下面是我会写的代码(为了提高性能,增加了 "set"):[ value for (i, value) in enumerate(lst) if i not in set(indices) ]。由于我还展示了如何按值删除,这也是一个更简单的情况,但可能会对其他人有所帮助,所以我会留下我的答案。 - ToolmakerSteve
@Veedrac- 谢谢; 我已经重写了代码,先构建集合。你认为现在比 SilentGhost 的解决方案更快吗?(我不认为它很重要,只是询问你的意见。)同样地,我会将 SilentGhost 的版本重新编写为 indices_as_set = set(indices)[ value for (i, value) in enumerate(lst) if i not in indices_as_set ],以加快速度。 - ToolmakerSteve
1
delete__by_values() 中的双下划线有什么样式上的原因吗? - Tom

5

一种使用列表索引值的替代列表推导方法:

stuff = ['a', 'b', 'c', 'd', 'e', 'f', 'woof']
index = [0, 3, 6]
new = [i for i in stuff if stuff.index(i) not in index]

这将返回:
['b', 'c', 'e', 'f']

2
回答不错,但将索引列表命名为“index”是具有误导性的,因为在列表迭代器中使用了方法“index()”。 - Joe
如果“stuff”有重复的值,该怎么办? - Scott

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