从列表列表中删除重复项


116

我在Python中有一个列表列表:

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

我想从中删除重复的元素。如果这是正常列表,而不是我可以使用的列表set。但不幸的是,该列表不可散列,因此无法建立一组列表。只有元组。因此,我可以将所有列表转换为元组,然后使用set并返回列表。但这不是很快。

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

上面的结果应为:

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

我不在乎保留订单。

注意:这个问题很相似,但不是我所需要的。搜索了SO,但没有找到确切的重复项。


基准测试:

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次迭代:

[set] Elapsed: 1.3900001049
[sort] Elapsed: 0.891000032425
[groupby] Elapsed: 0.780999898911
[loop in] Elapsed: 0.578000068665

对于更长的列表(代码中的一个重复了5次):

[set] Elapsed: 3.68700003624
[sort] Elapsed: 3.43799996376
[groupby] Elapsed: 1.03099989891
[loop in] Elapsed: 1.85900020599

1
所谓“这不是很快的”,您是说您已经计时了,并且它对于您的应用程序来说不够快,还是您认为它不够快?
Torsten Marek

@Torten,似乎太过模仿以致无法复制。对不起,直觉。将列表复制到元组,然后放入集合,然后返回列表列表(再次将元组复制到列表)
zaharpopov 2010年

@zaharpopov:那不是Python的工作方式,什么也不会被复制,只是现有元素的新容器(尽管对于int来说,几乎是相同的)
Jochen Ritzel 2010年

3
1.使用排序方法的时间已确定,因为“ k”反弹到排序变量。2.最后一种方法更快,因为生成测试数据的方式最多可以包含4个不同的元素。尝试某事。像K = [[int(u)for u in str(random.randrange(1,1000))] _ in range(100)]
Torsten Marek 2010年

@Torsten:固定谢谢。但即使10个列表中只有一个重复项,循环方法仍然是快速的
zaharpopov 2010年

Answers:


167
>>> 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通常会为此类问题提供最快,最强大的解决方案,非常值得您熟悉!-)

编辑:正如我在评论中提到的那样,正常的优化工作主要集中在大型输入(big-O方法)上,因为它要容易得多,可以提供良好的回报。但是有时(本质上是对推动性能极限界限的深层代码内循环中的“悲剧性关键瓶颈”),可能需要更详细地介绍概率分布,从而确定要优化的性能指标(可能是上限或第90个百分位数比平均值或中位数更重要,具体取决于一个人的应用程序),一开始执行启发式检查,然后根据输入数据特征选择不同的算法,依此类推。

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

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)和基本的提升技术(使每个函数局部具有恒定的全局名称以提高速度)可以使事物处于平等的地位。

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

$ 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

证实了二次方法具有足够小的常数,使其对于具有很少重复值的小列表具有吸引力。有一个没有重复的简短列表:

$ 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

二次法也不错,但排序和分组比较好。等等

如果(如对性能的痴迷所示)此操作位于“推动边界”应用程序的核心内循环中,则值得在其他代表性输入样本上尝试同一组测试,可能会检测到一些可以启发式地让您感到满意的简单措施选择一种或另一种方法(但是措施一定要很快)。

还值得考虑使用其他表示形式k-为什么它必须首先是列表列表而不是一组元组?如果重复删除任务很频繁,并且性能分析表明它是程序的性能瓶颈,则始终保留一组元组,并仅在需要和需要时才从中获取列表列表,例如,整体上可能会更快。


@alex感谢您的选择。此方法的速度与danben的速度相同,但快了几个百分点
zaharpopov 2010年

@alex:奇怪的是,这比短列表的幼稚二次方方法要慢(请参阅问题编辑)
zaharpopov 2010年

@zaharpopov:只有在您的特殊情况下,才这样。我对这个问题的评论。
Torsten Marek'2

@zaharpopov,如果您给出列表和子列表长度的概率分布以及重复的机会,则有可能(花费巨大的精力)计算/测量任何给定代码的运行时概率分布,并优化所需的任何度量(中位数,均值,第90位)百分位数)。由于投资回报率很低,几乎从未做过这件事:通常情况下,我们会着眼于大型输入的更轻松案例(big-O方法),劣等算法的确会严重损害性能。而且我也看不到您在Q中指定任何概率分布;-)。
Alex Martelli'2

@zaharpov,很高兴您喜欢它!
亚历克斯·马特利

21

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

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每个元素的整体时,其复杂度是二次的。


@ paul:非常奇怪-这种方法比其他方法都快
zaharpopov 2010年

我怀疑这种方法在很长的列表中不会更快。这将取决于您的应用程序:如果您确实只有六个元素的列表,其中包含两个重复的列表,那么任何解决方案都可能足够快,因此您应该使用最清晰的代码。
保罗·斯蒂芬森

@zaharpopov,在您的基准测试中不是二次方的,因为您反复重复相同的列表。您正在使用线性转角情况进行基准测试。
Mike Graham 2010年

k = ([[1, 2], [4], [5, 6, 2], [1, 2], [3], [5, 2], [6], [8], [9]] +[[x] for x in range(1000)]) *5将会很好地显示二次方行为
John La Rooy 2010年

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

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


谢谢你,丹本。这比转向元组然后“设置”然后再返回列表要快吗?
zaharpopov

您可以轻松地进行测试-编写两种重复数据删除方法,使用生成一些随机列表random,然后使用进行计时time
danben 2010年

4

set到目前为止,所有与该问题相关的解决方案都需要set在迭代之前创建一个完整的整体。

通过迭代列表列表并添加到“ seen”中,可以使这种懒惰并同时保留顺序set。然后仅在此跟踪器中找不到列表时才产生列表set

unique_everseen食谱可在itertools docs中找到。也可以在第3方toolz库中使用:

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转换是必需的,因为列表不可散列。


3

甚至您的“长”列表也很短。另外,您是否选择了它们以匹配实际数据?性能将随这些数据的实际外观而变化。例如,您有一遍又一遍的简短列表,以使列表更长。这意味着二次解在您的基准测试中是线性的,但实际上并非如此。

对于实际较大的列表,设置代码是最好的选择-它是线性的(尽管需要大量空间)。sort和groupby方法为O(n log n),方法中的循环显然是二次的,所以您知道随着n的变大,它们将如何扩展。如果这是您正在分析的数据的实际大小,那么谁在乎呢?很小

顺便说一句,如果我没有形成中间列表来进行设置,那我会看到明显的加速,也就是说,如果我替换

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

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

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


3

元组和{}的列表可用于删除重复项

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

1

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

  • 创建以元组为键,索引为值的字典
  • 打印字典键列表

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

1

这应该工作。

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

0

奇怪的是,以上答案删除了“重复项”,但是如果我也想删除重复的值怎么办?以下内容应该有用,并且不会在内存中创建新对象!

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)

和/ /是

[[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

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

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

要注意的是,这仅适用于字符串表示形式是足够好的唯一键的对象(对于大多数本机对象而言都是如此)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.