如何从列表中删除重复项,同时保持顺序?

如何在保留顺序的同时从列表中删除重复项?使用集合删除重复项会破坏原始顺序。有内置的还是Pythonic的成语?

673044 次浏览

这里有一些替代方案:http://www.peterbe.com/plog/uniqifiers-benchmark

最快的一个:

def f7(seq):seen = set()seen_add = seen.addreturn [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只是作为尝试更新集合的一种方式,而不是逻辑测试的一个组成部分。

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

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

编辑:我假设“保持顺序”意味着列表实际上是有序的。如果不是这样,那么MizardX的解决方案是正确的。

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

如果你需要一个班轮,那么这可能会有所帮助:

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

…应该可以但如果我错了就纠正我

对于没有散列的类型(例如列表列表),基于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 ) )]

MizardX的回答提供了多种方法的良好集合。

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

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

您可以引用由符号“_[1]”构建的列表推导。
例如,以下函数通过引用其列表推导来唯一化元素列表而不更改它们的顺序。

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

我想如果你想维持秩序,

你可以试试这个:

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

最佳解决方案因Python版本和环境限制而异:

Python 3.7+(以及大多数支持3.6的解释器,作为实现细节):

在PyPy 2.5.0中首次引入,并在CPython 3.6中作为实现细节采用,在Python 3.7中成为语言保证之前,普通dict是插入排序的,甚至比(也是C从CPython 3.5实现的)collections.OrderedDict更有效。所以到目前为止,最快的解决方案也是最简单的:

>>> items = [1, 2, 0, 1, 3, 2]>>> list(dict.fromkeys(items))  # Or [*dict.fromkeys(items)] if you prefer[1, 2, 0, 3]

list(set(items))一样,这会将所有工作推送到C层(在CPython上),但由于dict是插入排序的,因此dict.fromkeys不会丢失排序。它比list(set(items))慢(通常需要50-100%的时间),但比任何其他保持顺序的解决方案都快(大约需要涉及在listcomp中使用#4s的黑客行为的一半时间)。

重要提示more_itertools中的unique_everseen解决方案(见下文)在懒惰性和支持不可散列输入项方面具有一些独特的优势;如果您需要这些功能,则可以使用只有解决方案。

Python 3.5(以及所有旧版本,如果性能不是关键

就像Raymond指出一样,在CPython 3.5中,OrderedDict是用C实现的,丑陋的列表理解黑客比OrderedDict.fromkeys慢(除非你实际上需要最后的列表-即使这样,只有当输入很短时)。因此,在性能和易读性方面,CPython 3.5的最佳解决方案是OrderedDict等效于3.6+使用普通dict

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

在CPython 3.4及更早版本上,这将比其他一些解决方案慢,因此如果分析显示您需要更好的解决方案,请继续阅读。

Python 3.4及更早版本,如果性能至关重要并且可以接受第三方模块

正如@陈志立所指出的,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]

只有一个简单的库导入,没有黑客。

该模块正在调整iterols食谱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 Dseen = set()seen_add = seen.addif key is None:for element in filterfalse(seen.__contains__, iterable):seen_add(element)yield elementelse:for element in iterable:k = key(element)if k not in seen:seen_add(k)yield element

但与itertools配方不同,它支持不可散列的项目(以性能成本为代价;如果iterable中的所有元素都是不可散列的,则算法变为O(n²),而如果它们都是可散列的,则为O(n))。

重要提示:与这里的所有其他解决方案不同,unique_everseen可以懒惰地使用;峰值内存使用将是相同的(最终,底层set增长到相同的大小),但是如果你不list ify结果,你只是迭代它,你将能够在找到唯一项时处理它们,而不是等到整个输入被消重后再处理第一个唯一项。

Python 3.4及更早版本,如果性能至关重要第三方模块不可用

你有两个选择:

  1. #0食谱复制并粘贴到您的代码中,并根据上面的more_itertools示例使用它

  2. 使用丑陋的黑客允许单个listcomp检查和更新set以跟踪所看到的内容:

    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

请注意,上面的解决方案中的所有O(n)(保存在不可散列项的迭代上调用unique_everseen,即O(n²),而其他解决方案会在TypeError时立即失败),因此所有解决方案在不是最热门的代码路径时都具有足够的性能。使用哪一个取决于你可以依赖的语言规范/解释器/第三方模块的版本,性能是否至关重要(不要假设它是关键;它通常不是),最重要的是,易读性(因为如果维护这段代码的人后来变得凶残,你聪明的微优化可能不值得)。

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

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

例如:

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 operatordef 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)

然后唯一(some_list,test_round)将提供列表中的唯一元素,其中唯一性不再意味着传统的相等(这是通过使用任何类型的基于集合或基于字典键的方法来解决这个问题所暗示的),而是意味着对于元素可能舍入到的每个可能的整数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]

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

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

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

#0食谱有一个函数可以做到这一点,使用seen set技术,但是:

  • 处理标准key函数。
  • 不使用不体面的黑客。
  • 通过预绑定seen.add而不是查找N次来优化循环。(f7也这样做,但某些版本没有。)
  • 使用ifilterfalse优化循环,因此您只需循环遍历Python中的唯一元素,而不是所有元素。(当然,您仍然在ifilterfalse中迭代所有元素,但那是在C中,速度要快得多。)

它真的比f7快吗?这取决于你的数据,所以你必须测试它并看看。如果你最终想要一个列表,f7使用一个listcomp,这里没有办法做到这一点。(你可以直接append而不是yielding,或者你可以将生成器馈送到list函数中,但两者都不能像listcomp内部的LIST_APPEND那样快。)无论如何,通常,挤出几微秒并不会像拥有一个易于理解、可重用、已经编写好的函数那样重要,当你想装饰时不需要DSU。

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

如果你只是想要no-key的情况,你可以简化为:

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

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

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

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

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]

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

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

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

    import pandas as pdimport numpy as np
uniquifier = lambda alist: pd.Series(alist).drop_duplicates().tolist()
# from the chosen answerdef f7(seq):seen = set()seen_add = seen.addreturn [ 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 loopIn [110]: %timeit uniquifier(alist)100 loops, best of 3: 4.39 ms per loop

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

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

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

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

在CPython 3.6+中(以及从python3.7+开始的所有其他Python实现),字典被命令,因此从可迭代对象中删除重复项并保持原始顺序的方法是:

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

在python3.5及以下(包括python2.7),使用OrderedDict。我的时间显示,这是Python 3.5的各种方法中最快和最短的(当它获得C实现时;在3.5之前,它仍然是最清晰的解决方案,尽管不是最快的)。

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

只需从外部模块1iteration_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_everseenfrom collections import OrderedDictfrom more_itertools import unique_everseen as mi_unique_everseen
def f7(seq):seen = set()seen_add = seen.addreturn [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()

在此处输入图片描述

为了确保我也做了一个有更多重复的测试,只是为了检查它是否有区别:

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()

在此处输入图片描述

一个只包含一个值:

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()

在此处输入图片描述

在所有这些情况下,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免责声明:我是该软件包的作者。

不要踢一匹死马(这个问题很古老,已经有很多很好的答案),但这里有一个使用熊猫的解决方案,在许多情况下速度相当快,使用起来非常简单。

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]

python3.7及以上,字典是保证来记住它们的键插入顺序。这个问题的答案总结了当前的状态。

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

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

一种就地方法

这个方法是二次的,因为我们对列表中的每个元素都有一个线性查找(由于del s,我们必须添加重新排列列表的成本)。

也就是说,如果我们从列表的末尾开始,并朝着原点前进,删除子列表左侧存在的每个术语,那么就有可能进行操作

代码中的这个想法很简单

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

实现的简单测试

In [91]: from random import randint, seedIn [92]: seed('20080808') ; l = [randint(1,6) for _ in range(12)] # Beijing OlympicsIn [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]:

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

# for hashable sequencedef remove_duplicates(items):seen = set()for item in items:if item not in seen:yield itemseen.add(item)
a = [1, 5, 2, 1, 9, 1, 5, 10]list(remove_duplicates(a))# [1, 5, 2, 9, 10]


# for unhashable sequencedef 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 itemseen.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}]

这里有一个简单的方法来做到这一点:

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

这给出了输出:

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

熊猫用户应该查看pandas.unique

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

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

一线清单理解:

values_non_duplicated = [value for index, value in enumerate(values) if value not in values[ : index]]
x = [1, 2, 1, 3, 1, 4]
# brute force methodarr = []for i in x:if not i in arr:arr.insert(x[i],i)
# recursive methodtmp = []def remove_duplicates(j=0):if j < len(x):if not x[j] in tmp:tmp.append(x[j])i = j+1remove_duplicates(i)
      

remove_duplicates()

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

它们确实很受欢迎,因为它们简单、简约,并部署正确的散列以获得最佳效率。关于这些的主要抱怨似乎是,在逻辑表达式中使用方法seen.add(x)“返回”的不变None作为常量(因此是多余/不必要的)值-只是为了它的副作用-是笨拙和/或令人困惑的。

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

3.相反,让#0做它的工作!
请注意,上面的示例只是在预先知道这样做总是会导致集合成员资格增加的情况下调用set.addset本身从来没有机会拒绝是重复的;我们的代码片段基本上篡夺了它自己的角色。使用明确的两步测试和设置代码剥夺了set排除这些重复的核心能力。

4.单哈希查找代码:
以下版本减少哈希查找的数量每次迭代<强>一半-从两个减少到只有一个。

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