关于python:从列表列表中删除重复项

Removing duplicates from a list of lists

我在python中有一个列表:

1
k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

我想从中删除重复的元素。如果它不是一个普通的列表,我可以使用set。但不幸的是,该列表不可散列,无法生成一组列表。只有元组。所以我可以将所有列表转换为元组,然后使用set和back to list。但这并不快。

如何以最有效的方式做到这一点?

以上列表的结果应为:

1
k = [[5, 6, 2], [1, 2], [3], [4]]

我不在乎维持秩序。

注意:这个问题很相似,但不完全符合我的需要。搜索过,但没有找到确切的副本。

标杆管理:

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
41
42
43
44
45
import itertools, time


class Timer(object):
    def __init__(self, name=None):
        self.name = name

    def __enter__(self):
        self.tstart = time.time()

    def __exit__(self, type, value, traceback):
        if self.name:
            print '[%s]' % self.name,
        print 'Elapsed: %s' % (time.time() - self.tstart)


k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [5, 2], [6], [8], [9]] * 5
N = 100000

print len(k)

with Timer('set'):
    for i in xrange(N):
        kt = [tuple(i) for i in k]
        skt = set(kt)
        kk = [list(i) for i in skt]


with Timer('sort'):
    for i in xrange(N):
        ks = sorted(k)
        dedup = [ks[i] for i in xrange(len(ks)) if i == 0 or ks[i] != ks[i-1]]


with Timer('groupby'):
    for i in xrange(N):
        k = sorted(k)
        dedup = list(k for k, _ in itertools.groupby(k))

with Timer('loop in'):
    for i in xrange(N):
        new_k = []
        for elem in k:
            if elem not in new_k:
                new_k.append(elem)

"循环"(二次法)最快的所有短名单。对于长列表,它比GroupBy方法以外的所有人都快。这有道理吗?

对于短列表(代码中的列表),100000次迭代:

1
2
3
4
[set] Elapsed: 1.3900001049
[sort] Elapsed: 0.891000032425
[groupby] Elapsed: 0.780999898911
[loop in] Elapsed: 0.578000068665

对于较长的列表(代码中的列表重复5次):

1
2
3
4
[set] Elapsed: 3.68700003624
[sort] Elapsed: 3.43799996376
[groupby] Elapsed: 1.03099989891
[loop in] Elapsed: 1.85900020599


1
2
3
4
5
>>> k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]
>>> import itertools
>>> k.sort()
>>> list(k for k,_ in itertools.groupby(k))
[[1, 2], [3], [4], [5, 6, 2]]

itertools经常为此类问题提供最快速、最强大的解决方案,值得密切了解!-)

编辑:正如我在一篇评论中提到的,正常的优化工作集中在大投入(大O方法)上,因为它非常简单,可以提供很好的工作回报。但有时(本质上是因为在代码的深层内部循环中存在"可悲的关键瓶颈",这会推高性能极限),人们可能需要更详细地说明,提供概率分布,决定要优化的性能度量(可能是上限或90百分位比平均值更重要)或者中位数,取决于一个人的应用程序),在开始时执行可能的启发式检查,根据输入数据特征选择不同的算法,等等。

仔细测量"点"性能(特定输入的代码A与代码B)是这个极其昂贵的过程的一部分,标准库模块timeit在此提供了帮助。但是,在shell提示下使用它更容易。例如,这里有一个简短的模块来展示这个问题的一般方法,另存为nodup.py

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
import itertools

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

def doset(k, map=map, list=list, set=set, tuple=tuple):
  return map(list, set(map(tuple, k)))

def dosort(k, sorted=sorted, xrange=xrange, len=len):
  ks = sorted(k)
  return [ks[i] for i in xrange(len(ks)) if i == 0 or ks[i] != ks[i-1]]

def dogroupby(k, sorted=sorted, groupby=itertools.groupby, list=list):
  ks = sorted(k)
  return [i for i, _ in itertools.groupby(ks)]

def donewk(k):
  newk = []
  for i in k:
    if i not in newk:
      newk.append(i)
  return newk

# sanity check that all functions compute the same result and don't alter k
if __name__ == '__main__':
  savek = list(k)
  for f in doset, dosort, dogroupby, donewk:
    resk = f(k)
    assert k == savek
    print '%10s %s' % (f.__name__, sorted(resk))

请注意健全性检查(仅在执行python nodup.py时执行)和基本提升技术(为速度在每个函数中使用恒定的全局名称),以使事物处于平等的基础上。

现在,我们可以在小示例列表上运行检查:

1
2
3
4
5
6
7
8
$ python -mtimeit -s'import nodup' 'nodup.doset(nodup.k)'
100000 loops, best of 3: 11.7 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dosort(nodup.k)'
100000 loops, best of 3: 9.68 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dogroupby(nodup.k)'
100000 loops, best of 3: 8.74 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.donewk(nodup.k)'
100000 loops, best of 3: 4.44 usec per loop

确认二次方方法具有足够小的常量,使其对具有很少重复值的小列表具有吸引力。没有重复的短列表:

1
2
3
4
5
6
7
8
$ python -mtimeit -s'import nodup' 'nodup.donewk([[i] for i in range(12)])'
10000 loops, best of 3: 25.4 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dogroupby([[i] for i in range(12)])'
10000 loops, best of 3: 23.7 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.doset([[i] for i in range(12)])'
10000 loops, best of 3: 31.3 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dosort([[i] for i in range(12)])'
10000 loops, best of 3: 25 usec per loop

二次型方法不错,但是排序和分组方法更好。等

如果(正如对性能的痴迷所暗示的那样)此操作处于推动边界应用程序的核心内部循环中,那么值得在其他具有代表性的输入样本上尝试相同的一组测试,可能会检测到一些简单的度量,这些度量可以启发性地让您选择一种或另一种方法(但度量必须快速,以确保URSE)。

同样值得考虑为k保留一个不同的表示——为什么它首先必须是一个列表而不是一组元组?例如,如果重复删除任务频繁,并且分析显示它是程序的性能瓶颈,那么始终保留一组元组,并且仅在需要时从中获取列表可能会更快。


1
2
3
4
5
6
7
>>> k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]
>>> k = sorted(k)
>>> k
[[1, 2], [1, 2], [3], [4], [4], [5, 6, 2]]
>>> dedup = [k[i] for i in range(len(k)) if i == 0 or k[i] != k[i-1]]
>>> dedup
[[1, 2], [3], [4], [5, 6, 2]]

我不知道它是否需要更快,但你不必使用元组和集合。


手动操作,创建一个新的k列表,并添加到目前为止未找到的条目:

1
2
3
4
5
6
7
8
k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]
new_k = []
for elem in k:
    if elem not in new_k:
        new_k.append(elem)
k = new_k
print k
# prints [[1, 2], [4], [5, 6, 2], [3]]

简单易懂,如果每个元素都有用的话,您可以保留第一次出现的顺序,但我猜这是二次复杂性,因为您要搜索每个元素的整个new_k


即使你的"长"名单也很短。另外,您是否选择了与实际数据相匹配的数据?性能将随这些数据的实际外观而变化。例如,您有一个简短的列表反复重复,以形成一个较长的列表。这意味着二次解在基准中是线性的,但在现实中不是。

对于实际的大列表,设置代码是您最好的选择,它是线性的(尽管需要空间)。sort和groupby方法是O(n logn),loop-in方法显然是二次的,所以你知道当n变大时,这些方法是如何伸缩的。如果这是您正在分析的数据的实际大小,那么谁会在意呢?它很小。

顺便说一下,如果我没有形成一个中间的列表来制作这个集合,我会看到一个明显的加速,也就是说如果我替换

1
2
kt = [tuple(i) for i in k]
skt = set(kt)

具有

1
skt = set(tuple(i) for i in k)

真正的解决方案可能取决于更多的信息:您确定列表确实是您需要的表示形式吗?


迄今为止,所有与set相关的解决方案都需要在迭代之前创建一个完整的set

通过迭代列表并添加到一个"seen"set,可以使这个过程变得懒惰,同时保持顺序。然后,只有在这个跟踪器set中找不到的情况下才会生成一个列表。

unique_everseen配方可在itertools文档中获得。也可在第三方toolz库中找到:

1
2
3
4
5
6
7
8
9
10
from toolz import unique

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

# lazy iterator
res = map(list, unique(map(tuple, k)))

print(list(res))

[[1, 2], [4], [5, 6, 2], [3]]

注意,tuple转换是必要的,因为列表是不可哈希的。


tuple和的列表可用于删除重复项

1
2
3
>>> [list(tupl) for tupl in {tuple(item) for item in k }]
[[1, 2], [5, 6, 2], [3], [4]]
>>>


这应该有效。

1
2
3
4
5
6
7
8
9
k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

k_cleaned = []
for ele in k:
    if set(ele) not in [set(x) for x in k_cleaned]:
        k_cleaned.append(ele)
print(k_cleaned)

# output: [[1, 2], [4], [5, 6, 2], [3]]

奇怪的是,上面的答案会删除"重复的",但是如果我也要删除重复的值呢??以下内容应该有用,并且不会在内存中创建新对象!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def dictRemoveDuplicates(self):
    a=[[1,'somevalue1'],[1,'somevalue2'],[2,'somevalue1'],[3,'somevalue4'],[5,'somevalue5'],[5,'somevalue1'],[5,'somevalue1'],[5,'somevalue8'],[6,'somevalue9'],[6,'somevalue0'],[6,'somevalue1'],[7,'somevalue7']]


print(a)
temp = 0
position = -1
for pageNo, item in a:
    position+=1
    if pageNo != temp:
        temp = pageNo
        continue
    else:
        a[position] = 0
        a[position - 1] = 0
a = [x for x in a if x != 0]        
print(a)

O/P是:

1
2
[[1, 'somevalue1'], [1, 'somevalue2'], [2, 'somevalue1'], [3, 'somevalue4'], [5, 'somevalue5'], [5, 'somevalue1'], [5, 'somevalue1'], [5, 'somevalue8'], [6, 'somevalue9'], [6, 'somevalue0'], [6, 'somevalue1'], [7, 'somevalue7']]
[[2, 'somevalue1'], [3, 'somevalue4'], [7, 'somevalue7']]

创建一个以元组为键的字典,并打印键。

  • 创建以元组为键、索引为值的字典
  • 打印字典键列表
1
2
3
4
5
6
7
k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

dict_tuple = {tuple(item): index for index, item in enumerate(k)}

print [list(itm) for itm in dict_tuple.keys()]

# prints [[1, 2], [5, 6, 2], [3], [4]]

另一个可能更通用、更简单的解决方案是创建一个由对象的字符串版本键控的字典,并在末尾获取values():

1
2
>>> dict([(unicode(a),a) for a in [["A","A"], ["A","A"], ["A","B"]]]).values()
[['A', 'B'], ['A', 'A']]

关键是,这只适用于字符串表示形式足够好的唯一键(对于大多数本机对象都是这样)的对象。