【问题标题】:Removing duplicates from a list of lists从列表列表中删除重复项
【发布时间】:2011-01-13 21:50:15
【问题描述】:

我有一个 Python 列表:

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

我想从中删除重复的元素。如果它不是我可以使用set 的列表的普通列表。但不幸的是,该列表不可散列并且无法制作列表集。只有元组。所以我可以将所有列表转换为元组,然后使用 set 并返回到列表。但这并不快。

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

以上列表的结果应该是:

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

我不在乎维持秩序。

注意:this question 类似,但不是我需要的。搜索了 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

【问题讨论】:

  • “这不快”,您是说您已经计时并且它对您的应用程序来说还不够快,还是您认为它不够快?
  • @Torsten,这似乎是抄袭太多而不是聪明的方法。对不起,直觉。将列表复制到元组,然后复制到集合,然后返回到列表列表(再次将元组复制到列表)
  • @zaharpopov:这不是 Python 的工作方式,不会复制,只是现有元素的新容器(尽管对于 int,它几乎相同)
  • 1.使用排序的方法的时间被缩减了,因为“k”被反弹到排序的变体。 2. 最后一种方法更快,因为您生成测试数据的方式最多为您留下 4 个不同的元素。试试看。比如 K = [[int(u) for u in str(random.randrange(1, 1000))] for _ in range(100)]
  • @Torsten:解决了,谢谢。但即使 10 列表中只有一个重复项,循环方法仍然很快

标签: python


【解决方案1】:
>>> 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:

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

二次方法还不错,但 sort 和 groupby 方法更好。等等等等。

如果(正如对性能的痴迷所暗示的那样)此操作位于突破边界应用程序的核心内部循环中,则值得在其他代表性输入样本上尝试相同的测试集,可能会检测到一些简单的测量方法,启发式地让您选择一种或另一种方法(当然,测量必须快速)。

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

【讨论】:

  • @alex 感谢您的选择。这个方法和danben的速度差不多,快了几个%
  • @alex: 奇怪的是,这比用于较短列表的简单二次方法慢(见问题编辑)
  • @zaharpopov:只有在你的特殊情况下才会这样,参见。我对这个问题的评论。
  • @zaharpopov,如果您给出列表和子列表长度的概率分布以及重复的机会,则可以(付出巨大的努力)计算/测量任何给定代码的运行时概率分布并优化任何度量你需要(中位数,平均值,第 90 个百分位数,等等)。由于投资回报率非常低,它几乎从未做过:通常人们关注大输入的更简单的情况(大 O 方法),在这种情况下,劣质算法确实会严重损害性能。而且我没有看到您在 Q 中指定任何概率分布;-)。
  • @zaharpov,很高兴你喜欢它!
【解决方案2】:

手动创建一个新的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,它在您的基准测试中不是二次的,因为您一遍又一遍地复制相同的列表。您正在使用线性极端情况进行基准测试。
  • k = ([[1, 2], [4], [5, 6, 2], [1, 2], [3], [5, 2], [6], [8], [9]] +[[x] for x in range(1000)]) *5 将很好地显示二次行为
【解决方案3】:
>>> 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]]

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

【讨论】:

  • 谢谢丹本。这比转到元组然后“设置”然后返回列表要快吗?
  • 您可以轻松地进行测试 - 编写这两种重复数据删除方法,使用 random 生成一些随机列表,然后使用 time 计时。
【解决方案4】:

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

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

【讨论】:

    【解决方案5】:

    即使您的“长”列表也很短。另外,您是否选择它们以匹配实际数据?性能会因这些数据的实际外观而异。例如,您有一个简短的列表一遍又一遍地重复以形成一个更长的列表。这意味着二次解在您的基准测试中是线性的,但实际上并非如此。

    对于实际较大的列表,设置代码是您最好的选择——它是线性的(虽然很占空间)。 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)
    

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

    【讨论】:

      【解决方案6】:

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

      可以通过迭代列表列表并添加到“已见”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 转换是必要的,因为列表不可散列。

      【讨论】:

        【解决方案7】:
        a_list = [
                  [1,2],
                  [1,2],
                  [2,3],
                  [3,4]
        ]
        
        print (list(map(list,set(map(tuple,a_list)))))
        

        输出:[[1, 2], [3, 4], [2, 3]]

        【讨论】:

          【解决方案8】:

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

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

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

          【讨论】:

          • list(map(list, dict.fromkeys(map(tuple, k)))) 也是如此。但是您可能应该将其拆分以使其可读:D dict_of_unique_tuples = dict.fromkeys(map(tuple, k)) 和 list(map(list, dict_of_unique_tuples))
          【解决方案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]]
          

          【讨论】:

            【解决方案10】:

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

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

            【讨论】:

              【解决方案11】:

              有点背景,我刚开始学python,学了理解。

              k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]
              dedup = [elem.split('.') for elem in set(['.'.join(str(int_elem) for int_elem in _list) for _list in k])]
              

              【讨论】:

                【解决方案12】:

                最简单的解决方案是将列表列表转换为元组列表,然后应用dict.fromkeys() 方法,然后将其转换回列表。

                例如:

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

                转换为元组列表k = list(map(tuple, k))

                这会给你[(1, 2), (4,), (5, 6, 2), (1, 2), (3,), (4,)]

                然后执行以下操作:unique = list(dict.fromkeys(k))

                您将拥有[(1, 2), (4,), (5, 6, 2), (3,)]

                就是这样。

                【讨论】:

                  【解决方案13】:

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

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

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

                  【讨论】:

                    【解决方案14】:
                    k=[[1, 2], [4], [5, 6, 2], [1, 2], [3], [5, 2], [3], [8], [9]]
                    kl=[]
                    kl.extend(x for x in k if x not in kl)
                    k=list(kl)
                    print(k)
                    

                    哪个打印,

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

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 2021-04-02
                      • 2016-01-28
                      相关资源
                      最近更新 更多