关于数组:检查python列表/ numpy ndarray中是否存在重复项的最快方法

Fastest way to check if duplicates exist in a python list / numpy ndarray

我想确定我的列表(实际上是一个numpy.ndarray)是否在最快的执行时间内包含重复项。请注意,我不在乎删除副本,我只是想知道是否有副本。

注意:如果这不是一个副本,我会非常惊讶,但我已经尽力了,找不到了。最近的是这个问题和这个问题,这两个问题都要求返回唯一列表。


这是我想用的四种方法。

tl;dr:如果您期望很少(少于1/1000)个副本:

1
2
def contains_duplicates(X):
    return len(np.unique(X)) != len(X)

如果您希望经常(超过1/1000)重复:

1
2
3
4
5
6
7
def contains_duplicates(X):
    seen = set()
    seen_add = seen.add
    for x in X:
        if (x in seen or seen_add(x)):
            return True
    return False

第一种方法是提前退出这个答案,它希望返回唯一的值,第二种方法是应用于这个答案的相同思想。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> import numpy as np
>>> X = np.random.normal(0,1,[10000])
>>> def terhorst_early_exit(X):
...:     elems = set()
...:     for i in X:
...:         if i in elems:
...:             return True
...:         elems.add(i)
...:     return False
>>> %timeit terhorst_early_exit(X)
100 loops, best of 3: 10.6 ms per loop
>>> def peterbe_early_exit(X):
...:     seen = set()
...:     seen_add = seen.add
...:     for x in X:
...:         if (x in seen or seen_add(x)):
...:             return True
...:     return False
>>> %timeit peterbe_early_exit(X)
100 loops, best of 3: 9.35 ms per loop
>>> %timeit len(set(X)) != len(X)
100 loops, best of 3: 4.54 ms per loop
>>> %timeit len(np.unique(X)) != len(X)
1000 loops, best of 3: 967 μs per loop

如果您从普通的python列表开始,而不是从numpy.ndarray开始,事情会发生变化吗?

1
2
3
4
5
6
7
8
9
>>> X = X.tolist()
>>> %timeit terhorst_early_exit(X)
100 loops, best of 3: 9.34 ms per loop
>>> %timeit peterbe_early_exit(X)
100 loops, best of 3: 8.07 ms per loop
>>> %timeit len(set(X)) != len(X)
100 loops, best of 3: 3.09 ms per loop
>>> %timeit len(np.unique(X)) != len(X)
1000 loops, best of 3: 1.83 ms per loop

编辑:如果我们对重复的数量有一个预先的期望呢?

上述比较是在假设a)可能没有重复的情况下运行的,或者b)我们比平均情况更担心最坏的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
>>> X = np.random.normal(0, 1, [10000])
>>> for n_duplicates in [1, 10, 100]:
>>>     print("{} duplicates".format(n_duplicates))
>>>     duplicate_idx = np.random.choice(len(X), n_duplicates, replace=False)
>>>     X[duplicate_idx] = 0
>>>     print("terhost_early_exit")
>>>     %timeit terhorst_early_exit(X)
>>>     print("peterbe_early_exit")
>>>     %timeit peterbe_early_exit(X)
>>>     print("set length")
>>>     %timeit len(set(X)) != len(X)
>>>     print("numpy unique length")
>>>     %timeit len(np.unique(X)) != len(X)
1 duplicates
terhost_early_exit
100 loops, best of 3: 12.3 ms per loop
peterbe_early_exit
100 loops, best of 3: 9.55 ms per loop
set length
100 loops, best of 3: 4.71 ms per loop
numpy unique length
1000 loops, best of 3: 1.31 ms per loop
10 duplicates
terhost_early_exit
1000 loops, best of 3: 1.81 ms per loop
peterbe_early_exit
1000 loops, best of 3: 1.47 ms per loop
set length
100 loops, best of 3: 5.44 ms per loop
numpy unique length
1000 loops, best of 3: 1.37 ms per loop
100 duplicates
terhost_early_exit
10000 loops, best of 3: 111 μs per loop
peterbe_early_exit
10000 loops, best of 3: 99 μs per loop
set length
100 loops, best of 3: 5.16 ms per loop
numpy unique length
1000 loops, best of 3: 1.19 ms per loop

因此,如果您期望很少的重复,那么numpy.unique函数就是一种方法。随着预期重复数的增加,早期退出方法占主导地位。


根据数组的大小和重复的可能性,答案会有所不同。

例如,如果您希望平均数组有大约3个重复,那么提前退出将使您的平均case时间(和空间)减少2/3rds;如果您希望1000个数组中只有1个有任何重复,那么它只会增加一点复杂性,而不会有任何改进。

同时,如果数组足够大,构建一个临时的数组大小的集合可能会很昂贵,那么在它前面放置一个概率测试,就像一个布卢姆滤波器一样,可能会大大加快速度,但如果不是这样,那又是浪费了精力。

最后,如果可能的话,你想呆在麻木的地方。循环遍历一组浮点数(或其他浮点数)并将每个浮点数装箱到一个python对象中所需的时间几乎与散列和检查值所需的时间相同,当然,将数据存储在python set中而不是优化的麻木存储中也是浪费时间的。但是你必须把它与其他你不能用numpy提前退出的问题进行权衡,而且可能会有一个不错的C优化bloom过滤器实现一个pip install,但不是任何一个numpy友好的。

所以,对于所有可能的场景,没有一个最佳解决方案。

为了让你知道写一个bloom过滤器有多简单,我在几分钟内就把它拼凑到了一起:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from bitarray import bitarray # pip3 install bitarray

def dupcheck(X):
    # Hardcoded values to give about 5% false positives for 10000 elements
    size = 62352
    hashcount = 4
    bits = bitarray(size)
    bits.setall(0)
    def check(x, hash=hash): # TODO: default-value bits, hashcount, size?
        for i in range(hashcount):
            if not bits[hash((x, i)) % size]: return False
        return True
    def add(x):
        for i in range(hashcount):
            bits[hash((x, i)) % size] = True
    seen = set()
    seen_add = seen.add
    for x in X:
        if check(x) or add(x):
            if x in seen or seen_add(x):
                return True
    return False

它只使用12kb(62352位bitarray加上500个浮点set)而不是80kb(10000个浮点setnp.array)。当你只处理10万个元素的时候,这并不重要,但是如果10亿个元素占用了你一半以上的物理内存,情况就不同了。

当然,它几乎肯定会比使用np.unique慢一个数量级,甚至比使用set慢一个数量级,因为我们在python中做的都是缓慢的循环。但是,如果这是值得做的,那么在Cython中重写应该是轻而易举的(并且可以直接访问麻木的数组,而无需装箱和拆箱)。