从一个Python列表中删除重复项,并基于它修剪其他列表

从一个Python列表中删除重复项,并基于它修剪其他列表,python,duplicates,Python,Duplicates,我有一个问题很容易用一种丑陋的方式来解决,但我想知道是否有一种更像蟒蛇的方式来解决它 假设我有三个列表,A,B和C 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中删除所有重复项,但当删除重复项时,我希望从B和C中删除相应的索引: A = [1, 2, 3, 4, 5

我有一个问题很容易用一种丑陋的方式来解决,但我想知道是否有一种更像蟒蛇的方式来解决它

假设我有三个列表,
A
B
C

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
中删除所有重复项,但当删除重复项时,我希望从
B
C
中删除相应的索引:

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])

但是,有没有一种更优雅、更高效(且重复性更少)的方法呢?如果列表的数量增加,这可能会变得很麻烦,这可能会发生。

如何-基本上获取一组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]
将三个列表放在一起,根据第一个元素进行uniquify,然后解压缩:

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)
这是一种非常常见的模式。无论何时,只要您想在一组列表(或其他iterables)上执行锁定步骤中的任何操作,您就可以将它们压缩在一起并循环结果

此外,如果从3个列表扩展到42个(“如果列表数量增加,这可能会变得很麻烦,这可能会。”),那么扩展起来就很简单了:

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

一旦你掌握了
zip
的诀窍,“uniquify”是唯一困难的部分,所以让我来解释一下

现有代码通过在
new\u A
中搜索每个元素来检查是否已看到每个元素。由于
new\u A
是一个列表,这意味着如果您有N个元素,其中M个元素是唯一的,那么平均而言,您将对这N个元素中的每一个进行M/2比较。插入一些大的数字,NM/2会得到相当大的值,例如,100万个值,其中一半是唯一的,而您正在进行2500亿次比较

为了避免二次时间,您可以使用
集合
集合
可以在恒定时间而不是线性时间内测试元素的成员资格。因此,不是2500亿次比较,而是100万次哈希查找

如果您不需要维护顺序或装饰流程,只需将列表复制到
集合
即可。如果需要装饰,可以使用
dict
而不是set(将键作为
dict
键,其他所有内容都隐藏在值中)。要保持顺序,可以使用
OrderedDict
,但在这一点上,只需将
列表
集合
并排使用就更容易了。例如,对代码进行的最小更改是:

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个列表扩展到很多列表的话

这个函数包含一个名为
unique\u everseed
的函数,它概括了我们想要的东西。您可以将其复制并粘贴到代码中,自己编写一个简化版本,或者
pip安装更多itertools
并使用其他人的实现(如上所述)


帕德雷坎宁厄姆问道:

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

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

事实上,它有效地完成了与上面10行版本相同的工作。在这两种情况下,循环中唯一明显不琐碎的工作是
key In seen
seen.add(key)
,因为这两个操作都是
set
的固定时间摊销,这意味着整个过程是O(N)时间。实际上,对于N=
1000000,M=100000
,与二次型的分钟数相比,这两个版本大约是278ms和297ms(我忘了是哪个了)。您可能会将其微优化到250ms左右,但很难想象您会需要它,但在PyPy而不是CPython中运行它,或者在Cython或C中编写它,或者对其进行numpy化,或者获得更快的计算机,或者对其进行并行化,都不会带来好处

至于空间,显式版本让它变得非常明显。像任何可能的非变异算法一样,我们在原始列表的同时获得了三个
new\u Foo
列表,并且我们还添加了大小相同的
new\u A\u set
。因为所有这些都是长度
M
,所以这是4M的空间。我们可以通过做一次传递来获得指数,然后做同样的事情来将其减半無'政府的答案是:

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
的列表进行uniquify

如果你真的需要节省空间,你可以在适当的地方修改所有三个列表。但这要复杂得多,速度也慢了一点(尽管仍然是线性的*)

另外,值得注意的是
zip
版本的另一个优点:它适用于任何iterables。您可以为它提供三个懒惰的迭代器,它不必急于实例化它们。我认为在2米的空间里不可行,但在3米的空间里也不难:

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]


*请注意,只需
delc[i]
将使其成为二次型,因为从列表中间删除需要线性时间。幸运的是,线性时间是一个巨大的memmove,它比Python赋值的等效数量快几个数量级,因此如果
N
不是太大,那么实际上,当
N=100000,M=10000时,它的速度是不变版本的两倍……但是如果
N
可能太大,你必须用一个哨兵替换每个重复的元素,然后在第二次循环中遍历列表,这样你就可以只移动每个元素一次,这比不可变的版本慢50%。

在这个特定的形式中,我要说的是你是如何做的。您所描述的问题可能有一个潜在的模式,但正如所描述的,我看不到任何模式。如果列表很大,这将变得缓慢,因为您