我有一个很容易以丑陋的方式做的问题,但我想知道是否有更多的Pythonic方式。
说我有三个列表,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])
但这样做有更优雅,更有效(也更少重复)的方法吗?如果列表数量增加,这可能会变得很麻烦。
答案 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亿次比较。
要避免该二次时间,请使用set
。 set
可以测试元素的常量而非线性时间。因此,而不是2500亿次比较,这是100万次哈希查找。
如果您不需要维护订单或装饰 - 处理 - 取消设计值,只需将列表复制到set
即可。如果需要进行装饰,可以使用dict
代替集合(使用密钥作为dict
密钥,并将其他所有内容隐藏在值中)。要保留订单,您可以使用OrderedDict
,但此时更容易并排使用list
和set
。例如,对您的代码起作用的最小变化是:
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 seen
和seen.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]