从一个Python列表中删除重复项,根据它修剪其他列表

时间:2014-07-31 22:12:55

标签: python duplicates

我有一个很容易以丑陋的方式做的问题,但我想知道是否有更多的Pythonic方式。

说我有三个列表,ABC

A = [1, 1, 2, 3, 4, 4, 5, 5, 3]
B = [1, 2, 3, 4, 5, 6, 7, 8, 9]
C = [1, 2, 3, 4, 5, 6, 7, 8, 9]
# The actual data isn't important.

我需要从列表A中删除所有重复项,但是当删除重复项时,我希望从BC中删除相应的索引:

A = [1, 2, 3, 4, 5]
B = [1, 3, 4, 5, 7]
C = [1, 3, 4, 5, 7]

通过将所有内容移动到新列表,这很容易做到更长的代码:

new_A = []
new_B = []
new_C = []
for i in range(len(A)):
  if A[i] not in new_A:
    new_A.append(A[i])
    new_B.append(B[i])
    new_C.append(C[i])

但这样做有更优雅,更有效(也更少重复)的方法吗?如果列表数量增加,这可能会变得很麻烦。

2 个答案:

答案 0 :(得分:5)

Zip将三个列表放在一起,根据第一个元素进行unquify,然后解压缩:

from operator import itemgetter
from more_itertools import unique_everseen

abc = zip(a, b, c)
abc_unique = unique_everseen(abc, key=itemgetter(0))
a, b, c = zip(*abc_unique)

这是一种非常常见的模式。无论何时你想在锁定步骤中执行任何操作(或其他迭代),都可以将它们压缩在一起并循环遍历结果。

另外,如果你从3个列表转到其中的42个(“如果列表数量增长,这可能会变得很麻烦,可能会这样。”),这是微不足道的扩展:

abc = zip(*list_of_lists)
abc_unique = unique_everseen(abc, key=itemgetter(0))
list_of_lists = zip(*abc_unique)

一旦你掌握了zip,“uniquify”就是唯一的难点,所以让我解释一下。

现有代码通过在new_A中搜索每个元素来检查是否已看到每个元素。由于new_A是一个列表,这意味着如果你有N个元素,其中M个是唯一的,平均而言,你将要对这N个元素中的每个元素进行M / 2比较。插入一些大数字,NM / 2变得非常大 - 例如,100万个值,其中一半是唯一的,并且你正在进行2500亿次比较。

要避免该二次时间,请使用setset可以测试元素的常量而非线性时间。因此,而不是2500亿次比较,这是100万次哈希查找。

如果您不需要维护订单或装饰 - 处理 - 取消设计值,只需将列表复制到set即可。如果需要进行装饰,可以使用dict代替集合(使用密钥作为dict密钥,并将其他所有内容隐藏在值中)。要保留订单,您可以使用OrderedDict,但此时更容易并排使用listset。例如,对您的代码起作用的最小变化是:

new_A_set = set()
new_A = []
new_B = []
new_C = []
for i in range(len(A)):
    if A[i] not in new_A_set:
        new_A_set.add(A[i])
        new_A.append(A[i])
        new_B.append(B[i])
        new_C.append(C[i])

但这可以概括 - 而且应该是,尤其是如果你计划从3个列表扩展到其中的很多列表。

recipes in the itertools documentation包含一个名为unique_everseen的函数,可以完全概括我们想要的内容。您可以将其复制并粘贴到代码中,自己编写简化版本,或pip install more-itertools并使用其他人的实现(如上所述)。


PadraicCunningham问道:

  

zip(*unique_everseen(zip(a, b, c), key=itemgetter(0)))效率如何?

如果有N个元素,M唯一,那就是O(N)时间和O(M)空间。

事实上,它正在有效地完成与上述10行版本相同的工作。在这两种情况下,在循环中唯一不显而易见的工作是key in seenseen.add(key),并且因为两个操作都是set的摊销常量时间,这意味着整个事情是O( N)时间。在实践中,对于N = 1000000, M=100000,两个版本大约是278毫秒和297毫秒(我忘了哪个是哪个)与二次版本的分钟相比。您可能可以将其优化微调至250毫秒左右 - 但很难想象您需要的情况,但不会因在PyPy中运行而不是CPython,或者在Cython或C中编写它,或者将它收缩,或者使用更快的计算机,或者将其并行化。

至于空间,显式版本非常明显。像任何可以想象的非变异算法一样,我们在原始列表的同时获得了三个new_Foo列表,并且我们还添加了相同大小的new_A_set。由于所有这些都是M长度,这是4M空间。我们可以通过一次传递来获得指数,然后做同样的事情mu无的答案:

indices = set(zip(*unique_everseen(enumerate(a), key=itemgetter(1))[0])
a = [a[index] for index in indices]
b = [b[index] for index in indices]
c = [c[index] for index in indices]

但是没有办法比这更低;您必须至少有一个集合和长度为M的列表才能在线性时间内统一长度N的列表。

如果您确实需要节省空间,可以就地改变所有三个列表。但这要复杂得多,而且有点慢(虽然仍然是线性的)。

此外,值得注意的是zip版本的另一个优点:它适用于任何迭代。你可以为它提供三个惰性迭代器,它不必急切地实例化它们。我不认为它在2M空间中是可行的,但在3M中并不太难:

indices, a = zip(*unique_everseen(enumerate(a), key=itemgetter(1))
indices = set(indices)
b = [value for index, value in enumerate(b) if index in indices]
c = [value for index, value in enumerate(c) if index in indices]

*请注意,只有del c[i]会使其成为二次方,因为从列表中间删除需要线性时间。幸运的是,线性时间是一个巨大的memmove,比同等数量的Python赋值快几个数量级,所以如果N不是那么你可以侥幸逃脱它 - 事实上,N=100000, M=10000它的速度是不可变版本的两倍......但是如果N可能太大,你必须用一个标记替换每个重复的元素,然后在第二遍中遍历列表,你只能将每个元素移位一次,这比不可变版本慢50%。

答案 1 :(得分:0)

这个怎么样 - 基本上得到一组A的所有独特元素,然后得到它们的索引,并根据这些索引创建一个新的列表。

new_A = list(set(A))
indices_to_copy = [A.index(element) for element in new_A]
new_B = [B[index] for index in indices_to_copy]
new_C = [C[index] for index in indices_to_copy]

您可以为第二个语句编写一个函数,以便重用:

def get_new_list(original_list, indices):
    return [original_list[idx] for idx in indices]