如何在保留订单的同时从列表中删除重复项?

时间:2009-01-26 15:43:58

标签: python list duplicates unique

是否有内置功能可以从Python中的列表中删除重复项,同时保留顺序?我知道我可以使用一个集来删除重复项,但这会破坏原始顺序。我也知道我可以像这样滚动自己:

def uniq(input):
  output = []
  for x in input:
    if x not in output:
      output.append(x)
  return output

(感谢unwindcode sample。)

但如果可能的话,我想利用内置或更多的Pythonic习语。

相关问题:In Python, what is the fastest algorithm for removing duplicates from a list so that all elements are unique while preserving order?

33 个答案:

答案 0 :(得分:693)

您可以在这里找到一些替代方案:http://www.peterbe.com/plog/uniqifiers-benchmark

最快的一个:

def f7(seq):
    seen = set()
    seen_add = seen.add
    return [x for x in seq if not (x in seen or seen_add(x))]

为什么要将seen.add分配给seen_add而不只是调用seen.add? Python是一种动态语言,每次迭代解析seen.add比解析局部变量更昂贵。 seen.add可能在迭代之间发生了变化,而运行时不够聪明,无法排除这种情况。为了安全起见,每次都必须检查对象。

如果你打算在同一个数据集上大量使用这个功能,或许最好使用有序集:http://code.activestate.com/recipes/528878/

O (1)每次操作的插入,删除和成员检查。

(小额外注意:seen.add()始终返回None,因此上面的 or 仅作为尝试设置更新的方式,而不是作为逻辑测试的一个组成部分。)

答案 1 :(得分:320)

编辑2016

作为Raymond pointed out,在python 3.5+中,OrderedDict在C中实现,列表推导方法将慢于OrderedDict(除非你实际上需要最后的列表 - 即便如此,只有输入很短)。因此,3.5 +的最佳解决方案是OrderedDict

重要编辑2015

正如@abarnert所述,more_itertools库(pip install more_itertools)包含一个unique_everseen函数,用于解决此问题而不会出现任何不可读列表推导中的(not seen.add突变。这也是最快的解决方案:

>>> from  more_itertools import unique_everseen
>>> items = [1, 2, 0, 1, 3, 2]
>>> list(unique_everseen(items))
[1, 2, 0, 3]

只需一个简单的图书馆导入,无需破解。 这来自itertools recipe unique_everseen的实现,如下所示:

def unique_everseen(iterable, key=None):
    "List unique elements, preserving order. Remember all elements ever seen."
    # unique_everseen('AAAABBBCCDAABBB') --> A B C D
    # unique_everseen('ABBCcAD', str.lower) --> A B C D
    seen = set()
    seen_add = seen.add
    if key is None:
        for element in filterfalse(seen.__contains__, iterable):
            seen_add(element)
            yield element
    else:
        for element in iterable:
            k = key(element)
            if k not in seen:
                seen_add(k)
                yield element

在Python 2.7+中,接受了常用的习惯用法(虽然有效,但未针对速度进行优化,我现在会使用unique_everseen)来使用collections.OrderedDict

运行时: O(N)

>>> from collections import OrderedDict
>>> items = [1, 2, 0, 1, 3, 2]
>>> list(OrderedDict.fromkeys(items))
[1, 2, 0, 3]

这看起来比以下更好:

seen = set()
[x for x in seq if x not in seen and not seen.add(x)]

并没有使用丑陋的黑客

not seen.add(x)

这取决于set.add是一个始终返回None的就地方法,因此not None评估为True

但请注意,虽然hack解决方案具有相同的运行时复杂度O(N),但它的原始速度更快。

答案 2 :(得分:84)

在Python 2.7 中,从迭代中删除重复项同时保持原始顺序的新方法是:

>>> from collections import OrderedDict
>>> list(OrderedDict.fromkeys('abracadabra'))
['a', 'b', 'r', 'c', 'd']

在Python 3.5 中,OrderedDict有一个C实现。我的时间表明,现在这是Python 3.5的各种方法中最快和最短的。

在Python 3.6 中,常规字典变得有序且紧凑。 (此功能适用于CPython和PyPy,但在其他实现中可能不存在)。这为我们提供了一种新的最快的扣除方式,同时保留了订单:

>>> list(dict.fromkeys('abracadabra'))
['a', 'b', 'r', 'c', 'd']

在Python 3.7 中,保证常规字典在所有实现中都有序。 因此,最短和最快的解决方案是:

>>> list(dict.fromkeys('abracadabra'))
['a', 'b', 'r', 'c', 'd']

对@max的响应:一旦你移动到3.6或3.7并使用常规字典而不是 OrderedDict ,你就无法以任何其他方式击败性能。字典很密集,很容易转换成几乎没有开销的列表。目标列表预先调整为len(d),其保存列表推导中出现的所有调整大小。此外,由于内部密钥列表密集,复制指针几乎快速作为列表副本。

答案 3 :(得分:38)

sequence = ['1', '2', '3', '3', '6', '4', '5', '6']
unique = []
[unique.append(item) for item in sequence if item not in unique]

唯一→['1', '2', '3', '6', '4', '5']

答案 4 :(得分:23)

from itertools import groupby
[ key for key,_ in groupby(sortedList)]

列表甚至不必排序,充分条件是将相等的值组合在一起。

编辑:我认为“保留订单”意味着列表实际上是有序的。如果不是这种情况,那么MizardX的解决方案是正确的。

社区编辑:然而,这是“将重复的连续元素压缩为单个元素”的最优雅方式。

答案 5 :(得分:21)

不要踢死马(这个问题很老,而且已经有很多好的答案),但这里有一个使用熊猫的解决方案,在许多情况下都很快,并且使用起来很简单。

import pandas as pd

my_list = [0, 1, 2, 3, 4, 1, 2, 3, 5]

>>> pd.Series(my_list).drop_duplicates().tolist()
# Output:
# [0, 1, 2, 3, 4, 5]

答案 6 :(得分:18)

我想如果你想保持秩序,

你可以试试这个:

list1 = ['b','c','d','b','c','a','a']    
list2 = list(set(list1))    
list2.sort(key=list1.index)    
print list2

或类似地,你可以这样做:

list1 = ['b','c','d','b','c','a','a']  
list2 = sorted(set(list1),key=list1.index)  
print list2 

你也可以这样做:

list1 = ['b','c','d','b','c','a','a']    
list2 = []    
for i in list1:    
    if not i in list2:  
        list2.append(i)`    
print list2

它也可以写成:

list1 = ['b','c','d','b','c','a','a']    
list2 = []    
[list2.append(i) for i in list1 if not i in list2]    
print list2 

答案 7 :(得分:11)

对于另一个非常古老的问题的另一个非常晚的答案:

itertools recipes有一个使用seen设置技术执行此操作的功能,但是:

  • 处理标准key功能。
  • 不会使用任何不合时宜的黑客。
  • 通过预绑定seen.add优化循环,而不是查找N次。 (f7也会这样做,但有些版本没有。)
  • 使用ifilterfalse优化循环,因此您只需循环遍历Python中的唯一元素,而不是所有元素。 (当然,你仍然会在ifilterfalse内迭代所有这些,但这是在C中,并且要快得多。)

它实际上比f7更快吗?这取决于您的数据,因此您必须对其进行测试并查看。如果你想要一个列表,f7使用listcomp,这里就没办法了。 (您可以直接append代替yield,或者您可以将生成器提供给list函数,但两者都不能像listcomp中的LIST_APPEND一样快。)通常,挤出几微秒的速率并不像具有易于理解,可重复使用,已经编写的功能那样重要,在您想要装饰时不需要DSU。

与所有食谱一样,它也可以在more-iterools中找到。

如果您只想要no - key案例,可以将其简化为:

def unique(iterable):
    seen = set()
    seen_add = seen.add
    for element in itertools.ifilterfalse(seen.__contains__, iterable):
        seen_add(element)
        yield element

答案 8 :(得分:10)

只需从外部模块 1 添加另一个(非常高效的)此类功能的实现:iteration_utilities.unique_everseen

>>> from iteration_utilities import unique_everseen
>>> lst = [1,1,1,2,3,2,2,2,1,3,4]

>>> list(unique_everseen(lst))
[1, 2, 3, 4]

计时

我做了一些时间安排(Python 3.6),这些表明它比我测试的所有其他选项更快,包括OrderedDict.fromkeysf7more_itertools.unique_everseen

%matplotlib notebook

from iteration_utilities import unique_everseen
from collections import OrderedDict
from more_itertools import unique_everseen as mi_unique_everseen

def f7(seq):
    seen = set()
    seen_add = seen.add
    return [x for x in seq if not (x in seen or seen_add(x))]

def iteration_utilities_unique_everseen(seq):
    return list(unique_everseen(seq))

def more_itertools_unique_everseen(seq):
    return list(mi_unique_everseen(seq))

def odict(seq):
    return list(OrderedDict.fromkeys(seq))

from simple_benchmark import benchmark

b = benchmark([f7, iteration_utilities_unique_everseen, more_itertools_unique_everseen, odict],
              {2**i: list(range(2**i)) for i in range(1, 20)},
              'list size (no duplicates)')
b.plot()

enter image description here

只是为了确保我还做了更多重复的测试,以检查它是否有所作为:

import random

b = benchmark([f7, iteration_utilities_unique_everseen, more_itertools_unique_everseen, odict],
              {2**i: [random.randint(0, 2**(i-1)) for _ in range(2**i)] for i in range(1, 20)},
              'list size (lots of duplicates)')
b.plot()

enter image description here

只包含一个值:

b = benchmark([f7, iteration_utilities_unique_everseen, more_itertools_unique_everseen, odict],
              {2**i: [1]*(2**i) for i in range(1, 20)},
              'list size (only duplicates)')
b.plot()

enter image description here

在所有这些情况下,iteration_utilities.unique_everseen功能最快(在我的计算机上)。

iteration_utilities.unique_everseen函数还可以处理输入中不可恢复的值(但是,如果值是可清除的,则性能为O(n*n)而不是O(n))。

>>> lst = [{1}, {1}, {2}, {1}, {3}]

>>> list(unique_everseen(lst))
[{1}, {2}, {3}]

1 免责声明:我是该套餐的作者。

答案 9 :(得分:9)

Python 3.7 及更高版本中,字典guaranteed可以记住其密钥插入顺序。 this问题的答案总结了当前的事态。

OrderedDict解决方案因此变得过时,如果没有任何导入语句,我们可以简单地发出:

>>> lst = [1, 2, 1, 3, 3, 2, 4]
>>> list(dict.fromkeys(lst))
[1, 2, 3, 4]

答案 10 :(得分:6)

对于没有可清除类型(例如列表列表),基于MizardX:

def f7_noHash(seq)
    seen = set()
    return [ x for x in seq if str( x ) not in seen and not seen.add( str( x ) )]

答案 11 :(得分:4)

这是一种简单的方法:

list1 = ["hello", " ", "w", "o", "r", "l", "d"]
sorted(set(list1 ), key=lambda x:list1.index(x))

给出输出:

["hello", " ", "w", "o", "r", "l", "d"]

答案 12 :(得分:4)

熊猫用户应签出pandas.unique

>>> import pandas as pd
>>> lst = [1, 2, 1, 3, 3, 2, 4]
>>> pd.unique(lst)
array([1, 2, 3, 4])

该函数返回一个NumPy数组。如果需要,您可以使用tolist方法将其转换为列表。

答案 13 :(得分:3)

减少变量5倍,但更复杂

>>> l = [5, 6, 6, 1, 1, 2, 2, 3, 4]
>>> reduce(lambda r, v: v in r[1] and r or (r[0].append(v) or r[1].add(v)) or r, l, ([], set()))[0]
[5, 6, 1, 2, 3, 4]

说明:

default = (list(), set())
# use list to keep order
# use set to make lookup faster

def reducer(result, item):
    if item not in result[1]:
        result[0].append(item)
        result[1].add(item)
    return result

>>> reduce(reducer, l, default)[0]
[5, 6, 1, 2, 3, 4]

答案 14 :(得分:3)

借用用于为列表定义Haskell的nub函数的递归思想,这将是一种递归方法:

def unique(lst):
    return [] if lst==[] else [lst[0]] + unique(filter(lambda x: x!= lst[0], lst[1:]))

e.g:

In [118]: unique([1,5,1,1,4,3,4])
Out[118]: [1, 5, 4, 3]

我尝试用它来增加数据大小并看到亚线性时间复杂度(不确定,但建议这对于普通数据应该没问题。)

In [122]: %timeit unique(np.random.randint(5, size=(1)))
10000 loops, best of 3: 25.3 us per loop

In [123]: %timeit unique(np.random.randint(5, size=(10)))
10000 loops, best of 3: 42.9 us per loop

In [124]: %timeit unique(np.random.randint(5, size=(100)))
10000 loops, best of 3: 132 us per loop

In [125]: %timeit unique(np.random.randint(5, size=(1000)))
1000 loops, best of 3: 1.05 ms per loop

In [126]: %timeit unique(np.random.randint(5, size=(10000)))
100 loops, best of 3: 11 ms per loop

我还认为有趣的是,这可以很容易地被其他操作推广到唯一性。像这样:

import operator
def unique(lst, cmp_op=operator.ne):
    return [] if lst==[] else [lst[0]] + unique(filter(lambda x: cmp_op(x, lst[0]), lst[1:]), cmp_op)

例如,您可以将使用舍入概念的函数传递给相同的整数,就像它为了唯一性目的而“相等”一样,如下所示:

def test_round(x,y):
    return round(x) != round(y)

那么unique(some_list,test_round)将提供列表中的唯一元素,其中唯一性不再意味着传统的相等性(通过使用任何类型的基于集合或基于dict-key的方法来暗示这个问题)而是意味着只对元素可能舍入到的每个可能的整数K采用向K舍入的第一个元素,例如:

In [6]: unique([1.2, 5, 1.9, 1.1, 4.2, 3, 4.8], test_round)
Out[6]: [1.2, 5, 1.9, 4.2, 3]

答案 15 :(得分:2)

您可以引用列表推导,因为它是由符号'_ [1]'构建的。
例如,以下函数unique-ifies元素列表,而不通过引用其列表推导来改变它们的顺序。

def unique(my_list): 
    return [x for x in my_list if x not in locals()['_[1]']]

演示:

l1 = [1, 2, 3, 4, 1, 2, 3, 4, 5]
l2 = [x for x in l1 if x not in locals()['_[1]']]
print l2

输出:

[1, 2, 3, 4, 5]

答案 16 :(得分:2)

MizardX的答案提供了多种方法的很好的集合。

这是我在大声思考时想出的:

mylist = [x for i,x in enumerate(mylist) if x not in mylist[i+1:]]

答案 17 :(得分:1)

l = [1,2,2,3,3,...]
n = []
n.extend(ele for ele in l if ele not in set(n))

一个生成器表达式,它使用O(1)查找集合来确定是否在新列表中包含元素。

答案 18 :(得分:1)

一个简单的递归解决方案:

def uniquefy_list(a):
    return uniquefy_list(a[1:]) if a[0] in a[1:] else [a[0]]+uniquefy_list(a[1:]) if len(a)>1 else [a[0]]

答案 19 :(得分:1)

你可以做一些丑陋的列表理解黑客。

[l[i] for i in range(len(l)) if l.index(l[i]) == i]

答案 20 :(得分:1)

使用_sorted_numpy数组的相对有效的方法:

b = np.array([1,3,3, 8, 12, 12,12])    
numpy.hstack([b[0], [x[0] for x in zip(b[1:], b[:-1]) if x[0]!=x[1]]])

输出:

array([ 1,  3,  8, 12])

答案 21 :(得分:0)

x = [1, 2, 1, 3, 1, 4]

# brute force method
arr = []
for i in x:
  if not i in arr:
    arr.insert(x[i],i)

# recursive method
tmp = []
def remove_duplicates(j=0):
    if j < len(x):
      if not x[j] in tmp:
        tmp.append(x[j])
      i = j+1  
      remove_duplicates(i)

      

remove_duplicates()

答案 22 :(得分:0)

将dict.fromdict方法的想法提供给@wjandrea:

def solve(arr): 
    return list(dict.fromkeys(arr[::-1]))[::-1]

这将反转输入和输出以正确进行迭代

答案 23 :(得分:0)

一个班轮名单理解:

values_non_duplicated = [value for index, value in enumerate(values) if value not in values[ : index]]

答案 24 :(得分:0)

1.这些都很棒
为了在保留顺序的同时删除重复项,本页其他地方提出的优秀解决方案...

seen = set()
[x for x in seq if not (x in seen or seen.add(x))]

及其等效变体...

seen = set()
[x for x in seq if x not in seen and not seen.add(x)]

确实很受欢迎,因为它们简单、简约,并且部署了正确的散列以获得最佳效率。主要的抱怨似乎是将 not seen.add(x) 用作不可操作的、不变的表达式是笨拙的、臃肿的和/或令人困惑的。

2.但他们每次迭代都会浪费一次哈希查找
令人惊讶的是,考虑到关于这个主题的讨论和辩论的数量,实际上对似乎被忽视的代码进行了重大改进。如图所示,每次“测试并设置”迭代需要两次哈希查找:第一次测试成员资格x not in seen,然后再次实际添加值seen.add(x)。由于第一个操作保证了第二个操作总是成功的,因此这里存在浪费性的重复工作。并且由于这里的整体技术非常有效,多余的哈希查找将是剩下的很少的部分中最昂贵的部分。

3.相反,让 set 完成它的工作
请注意,上面的示例仅在调用 set.add 时预先知道这样做会导致集合成员资格发生变化。 set 本身从来没有机会拒绝重复;我们的代码片段基本上已经为自己篡夺了这个角色。使用显式的两步测试和设置代码正在剥夺其存在本身排除这些重复项的核心能力set

4.改进的代码
以下版本将每次迭代的哈希查找次数减半——从两次减少到一次。这大大提高了一种已经非常快速的方法的性能。

seen = set()
[x for x in seq if len(seen) < len(seen.add(x) or seen)]

至于令人不快的黑客攻击,现在与以前相比发生了一些变化,但它似乎可以继续存在。

答案 25 :(得分:0)

zmk的方法使用列表理解,它非常快速,但自然保持顺序。对于区分大小写的字符串,可以轻松对其进行修改。这也保留了原始情况。

def DelDupes(aseq) :
    seen = set()
    return [x for x in aseq if (x.lower() not in seen) and (not seen.add(x.lower()))]

密切相关的功能是:

def HasDupes(aseq) :
    s = set()
    return any(((x.lower() in s) or s.add(x.lower())) for x in aseq)

def GetDupes(aseq) :
    s = set()
    return set(x for x in aseq if ((x.lower() in s) or s.add(x.lower())))

答案 26 :(得分:0)

消除序列中的重复值,但保留其余项目的顺序。使用通用生成器功能。

# for hashable sequence
def remove_duplicates(items):
    seen = set()
    for item in items:
        if item not in seen:
            yield item
            seen.add(item)

a = [1, 5, 2, 1, 9, 1, 5, 10]
list(remove_duplicates(a))
# [1, 5, 2, 9, 10]



# for unhashable sequence
def remove_duplicates(items, key=None):
    seen = set()
    for item in items:
        val = item if key is None else key(item)
        if val not in seen:
            yield item
            seen.add(val)

a = [ {'x': 1, 'y': 2}, {'x': 1, 'y': 3}, {'x': 1, 'y': 2}, {'x': 2, 'y': 4}]
list(remove_duplicates(a, key=lambda d: (d['x'],d['y'])))
# [{'x': 1, 'y': 2}, {'x': 1, 'y': 3}, {'x': 2, 'y': 4}]

答案 27 :(得分:0)

就地方法

此方法是二次方的,因为我们对列表的每个元素都进行了线性查找(为此,由于del,我们不得不增加重新排列列表的费用)。

也就是说,如果我们从列表的末尾开始并朝着原点方向移动,则可以删除子列表左侧的每个术语。

代码中的想法很简单

for i in range(len(l)-1,0,-1): 
    if l[i] in l[:i]: del l[i] 

实现的简单测试

In [91]: from random import randint, seed                                                                                            
In [92]: seed('20080808') ; l = [randint(1,6) for _ in range(12)] # Beijing Olympics                                                                 
In [93]: for i in range(len(l)-1,0,-1): 
    ...:     print(l) 
    ...:     print(i, l[i], l[:i], end='') 
    ...:     if l[i] in l[:i]: 
    ...:          print( ': remove', l[i]) 
    ...:          del l[i] 
    ...:     else: 
    ...:          print() 
    ...: print(l)
[6, 5, 1, 4, 6, 1, 6, 2, 2, 4, 5, 2]
11 2 [6, 5, 1, 4, 6, 1, 6, 2, 2, 4, 5]: remove 2
[6, 5, 1, 4, 6, 1, 6, 2, 2, 4, 5]
10 5 [6, 5, 1, 4, 6, 1, 6, 2, 2, 4]: remove 5
[6, 5, 1, 4, 6, 1, 6, 2, 2, 4]
9 4 [6, 5, 1, 4, 6, 1, 6, 2, 2]: remove 4
[6, 5, 1, 4, 6, 1, 6, 2, 2]
8 2 [6, 5, 1, 4, 6, 1, 6, 2]: remove 2
[6, 5, 1, 4, 6, 1, 6, 2]
7 2 [6, 5, 1, 4, 6, 1, 6]
[6, 5, 1, 4, 6, 1, 6, 2]
6 6 [6, 5, 1, 4, 6, 1]: remove 6
[6, 5, 1, 4, 6, 1, 2]
5 1 [6, 5, 1, 4, 6]: remove 1
[6, 5, 1, 4, 6, 2]
4 6 [6, 5, 1, 4]: remove 6
[6, 5, 1, 4, 2]
3 4 [6, 5, 1]
[6, 5, 1, 4, 2]
2 1 [6, 5]
[6, 5, 1, 4, 2]
1 5 [6]
[6, 5, 1, 4, 2]

In [94]:                                                                                                                             

答案 28 :(得分:0)

不使用导入的模块或集合的解决方案:

text = "ask not what your country can do for you ask what you can do for your country"
sentence = text.split(" ")
noduplicates = [(sentence[i]) for i in range (0,len(sentence)) if sentence[i] not in sentence[:i]]
print(noduplicates)

提供输出:

['ask', 'not', 'what', 'your', 'country', 'can', 'do', 'for', 'you']

答案 29 :(得分:0)

这将保留顺序并在O(n)时间内运行。基本上这个想法是在找到重复的地方创建一个洞并将其下沉到底部。利用读写指针。每当发现重复时,只有读指针前进,并且写指针停留在重复条目上以覆盖它。

def deduplicate(l):
    count = {}
    (read,write) = (0,0)
    while read < len(l):
        if l[read] in count:
            read += 1
            continue
        count[l[read]] = True
        l[write] = l[read]
        read += 1
        write += 1
    return l[0:write]

答案 30 :(得分:0)

如果您经常使用pandas,并且美观性优于性能,那么请考虑内置函数pandas.Series.drop_duplicates

    import pandas as pd
    import numpy as np

    uniquifier = lambda alist: pd.Series(alist).drop_duplicates().tolist()

    # from the chosen answer 
    def f7(seq):
        seen = set()
        seen_add = seen.add
        return [ x for x in seq if not (x in seen or seen_add(x))]

    alist = np.random.randint(low=0, high=1000, size=10000).tolist()

    print uniquifier(alist) == f7(alist)  # True

时间安排:

    In [104]: %timeit f7(alist)
    1000 loops, best of 3: 1.3 ms per loop
    In [110]: %timeit uniquifier(alist)
    100 loops, best of 3: 4.39 ms per loop

答案 31 :(得分:0)

如果您需要一个衬垫,那么这可能会有所帮助:

reduce(lambda x, y: x + y if y[0] not in x else x, map(lambda x: [x],lst))

......应该有效但如果我错了就纠正我

答案 32 :(得分:-2)

      def remove_duplicates_thenSort():
         t = ['b', 'c', 'd','d','a','c','c']
         t2 = []
         for i,k in enumerate(t):
              index = t.index(k)
              if i == index:
                 t2.append(t[i])
         return sorted(t2)
      print(remove_duplicates_thenSort())