在 Python 中,从列表中删除重复项以使所有元素都是唯一的*同时保留顺序*的最快算法是啥? [复制]

Posted

技术标签:

【中文标题】在 Python 中,从列表中删除重复项以使所有元素都是唯一的*同时保留顺序*的最快算法是啥? [复制]【英文标题】:In Python, what is the fastest algorithm for removing duplicates from a list so that all elements are unique *while preserving order*? [duplicate]在 Python 中,从列表中删除重复项以使所有元素都是唯一的*同时保留顺序*的最快算法是什么? [复制] 【发布时间】:2010-09-10 11:46:56 【问题描述】:

例如:

>>> x = [1, 1, 2, 'a', 'a', 3]
>>> unique(x)
[1, 2, 'a', 3]

假设列表元素是可散列的。

澄清:结果应保留列表中的第一个重复项。例如,[1, 2, 3, 2, 3, 1] 变为 [1, 2, 3]。

【问题讨论】:

我们是保留第一个副本,还是最后一个,还是在中间的某个地方?例如,[1,2,3,2,3,1],那会变成 [1,2,3],还是 [2,3,1],还是别的什么? 基准和明确答案here. 如何将作业标签应用于某物?当它说假设元素是可散列的时,您的教授要求您将条目放在散列表中,然后在您遍历列表时很容易看到您之前是否遇到过它们。 【参考方案1】:
def unique(items):
    found = set()
    keep = []

    for item in items:
        if item not in found:
            found.add(item)
            keep.append(item)
            
    return keep

print unique([1, 1, 2, 'a', 'a', 3])

【讨论】:

set() 优于 set([])。 就地算法更快。请参阅 james 和我的答案。 这是一个旧线程,但是如果您使 add() 和 append() 方法成为本地函数(在循环之前放置 add = found.addapp = keep.append 然后使用 add(item) 和 @987654327 @,那么这是迄今为止最快的。字典使用更快的原因是它不需要为每个添加和附加查找属性。只需我的两分钱。 如果您之后将其放入列表推导式中,您将获得另一次速度提升。综合考虑所有的变化,速度几乎翻了一番。在本页下方查看我的比较。 @so.very.tired 因为keep 是一个列表,并且检查列表中的成员资格平均需要列表长度的线性时间。同时,检查集合中的成员资格需要平均恒定时间(请参阅this)。在性能方面,使用适当的数据结构是一个交易破坏者。无论如何,这个答案已经过时了。请查看this question。【参考方案2】:
a=[1,2,3,4,5,7,7,8,8,9,9,3,45]

def unique(l):

    ids=
    for item in l:
        if not ids.has_key(item):
            ids[item]=item
    return  ids.keys()
print a

print unique(a)

插入元素将采用 theta(n) 检索元素是否退出将花费恒定时间 测试所有项目也需要 theta(n) 所以我们可以看到这个解决方案将采用 theta(n)。 请记住,python 中的字典是由哈希表实现的。

【讨论】:

问题说“同时保持顺序”。 Python 字典不保留顺序。【参考方案3】:

更新:on Python3.7+:

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

旧答案:

这是迄今为止最快的解决方案(对于以下输入):

def del_dups(seq):
    seen = 
    pos = 0
    for item in seq:
        if item not in seen:
            seen[item] = True
            seq[pos] = item
            pos += 1
    del seq[pos:]

lst = [8, 8, 9, 9, 7, 15, 15, 2, 20, 13, 2, 24, 6, 11, 7, 12, 4, 10, 18, 
       13, 23, 11, 3, 11, 12, 10, 4, 5, 4, 22, 6, 3, 19, 14, 21, 11, 1, 
       5, 14, 8, 0, 1, 16, 5, 10, 13, 17, 1, 16, 17, 12, 6, 10, 0, 3, 9, 
       9, 3, 7, 7, 6, 6, 7, 5, 14, 18, 12, 19, 2, 8, 9, 0, 8, 4, 5]
del_dups(lst)
print(lst)
# -> [8, 9, 7, 15, 2, 20, 13, 24, 6, 11, 12, 4, 10, 18, 23, 3, 5, 22, 19, 14, 
#     21, 1, 0, 16, 17]

字典查找比 Python 3 中的集合稍快。

【讨论】:

您能解释一下为什么在这种情况下字典查找比集合成员资格测试更快吗? @Stephen Emslie:我不知道。它可能是一个基准工件。 Try it yourself。纯粹的推测:字典是 CPython 的基本数据结构(命名空间、类是/通过字典实现的),因此字典比集合更调整/优化。 时机不错,但结论错误。时序仅表明 d[k] = v 之类的运算符访问比d.__setitem__(k, v) 之类的方法调用访问快,即使后者已使用d_setitem = d.__setitem__ 预先绑定然后时序d_setitem(k, v) 使用 Python 3.4,我尝试了你的测试脚本;并且使用集合的函数始终快。 @jfs:在 3.6+ 中,这可以变得更简单,even faster 通过简化为 def del_dups(seq): seq[:] = dict.fromkeys(seq)(就地修改)或 def del_dups(seq): return list(dict.fromkeys(seq)) 制作没有重复的新副本.【参考方案4】:

使用:

lst = [8, 8, 9, 9, 7, 15, 15, 2, 20, 13, 2, 24, 6, 11, 7, 12, 4, 10, 18, 13, 23, 11, 3, 11, 12, 10, 4, 5, 4, 22, 6, 3, 19, 14, 21, 11, 1, 5, 14, 8, 0, 1, 16, 5, 10, 13, 17, 1, 16, 17, 12, 6, 10, 0, 3, 9, 9, 3, 7, 7, 6, 6, 7, 5, 14, 18, 12, 19, 2, 8, 9, 0, 8, 4, 5]

并使用 timeit 模块:

$ python -m timeit -s 'import uniquetest' 'uniquetest.etchasketch(uniquetest.lst)'

对于其他各种功能(我以他们的海报命名)等等,我有以下结果(在我的第一代英特尔 MacBook Pro 上):

Allen:                  14.6 µs per loop [1]
Terhorst:               26.6 µs per loop
Tarle:                  44.7 µs per loop
ctcherry:               44.8 µs per loop
Etchasketch 1 (short):  64.6 µs per loop
Schinckel:              65.0 µs per loop
Etchasketch 2:          71.6 µs per loop
Little:                 89.4 µs per loop
Tyler:                 179.0 µs per loop

[1] 请注意,Allen 修改了列表 - 我相信这已经歪曲了时间,因为 timeit 模块运行代码 100000 次,其中 99999 次使用无重复列表。


总结:使用集合的直接实现胜过令人困惑的单行代码:-)

【讨论】:

james 建议使用更快的版本。见***.com/questions/89178/#91430 使用 OSX 时遇到的问题。 ;-) @jfs:james 和 Allen 的版本都在原地发生变异,所以除非使用的微基准测试说明了这一点(例如,通过每次使用新的 list 调用函数和/或在所有),时间没有可比性。现在最快的解决方案(从 3.6 开始)要么是来自 itertoolsunique_everseen 配方(如果您需要处理找到的每个元素),它比 Terhorst 的解决方案快 10% 左右,或者 40-80% unique_everseenlist(dict.fromkeys(iterable))(或lst[:] = dict.fromkeys(lst)“就地”操作)的运行时间。 @ShadowRanger:我的微基准测试包括f(lst[:])(即,每次调用都会发生复制)。尽管 Python 在 12 年多的时间里发生了变化。我不会依赖这么多年前的微基准测试结果。【参考方案5】:

这可能是最简单的方法:

list(OrderedDict.fromkeys(iterable))

从 Python 3.5 开始,OrderedDict 现在是用 C 实现的,所以它现在是最短、最干净、最快的。

【讨论】:

优雅,但不幸的是比最快的解决方案慢了大约一个数量级,并且令人惊讶地是一般最慢的解决方案之一。 OrderedDict 似乎是真正的性能杀手。 也许如果OrderedSet 成为内置函数,我们会有一个非常快速的解决方案【参考方案6】:
x = [] # Your list  of items that includes Duplicates

# Assuming that your list contains items of only immutable data types

dict_x =  

dict_x = item : item for i, item in enumerate(x) if item not in dict_x.keys()
# Average t.c. = O(n)* O(1) ; furthermore the dict comphrehension and generator like behaviour of enumerate adds a certain efficiency and pythonic feel to it.

x = dict_x.keys() # if you want your output in list format 

【讨论】:

可变类型的项目会出现什么问题? 这不像你想象的那样工作。 if item not in dict_x.keys() 正在检查原始的键,空的 dict_x,而不是正在创建的字典。总是如此。删除重复项仅仅是因为尝试创建重复键被忽略了。 你为什么使用enumerate() @ByteEater 可变类型不能用作字典键。 @Bramar,也许他试图通过写 i: item 而不是 item : item 来启用可变类型(尽管这对的单个名称就足够了)然后使用 .values() 而不是 .keys()在这两个地方。但由于您的第一条评论,这将不起作用。【参考方案7】:

这是最快的一个,比较了来自这个lengthy discussion 的所有内容和这里给出的其他答案,参考这个benchmark。它比讨论中最快的函数f8 快了 25%。感谢 David Kirby 的想法。

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

一些时间比较:

$ python uniqifiers_benchmark.py 
* f8_original 3.76
* uniquify 3.0
* terhorst 5.44
* terhorst_localref 4.08
* del_dups 4.76

【讨论】:

我看不到与此处最佳答案中的解决方案的时间比较。以我的经验,explicit loops 比 CPython 中的列表理解要快(至少它需要一个基准来测试每个特定情况)。 我添加了上面的时间。所提出的解决方案中的主要开销是 add、append 等的属性查找,但即使您将其排除在外,列表理解也比 terhorst_localreferences 快 25%。 您能否在答案中包含完整的基准代码?我在the file you linked 中看不到terhorst(或任何其他相关代码)。 pastebin.com/C5SQmT1R【参考方案8】:

以下是 itertools 文档中的两个配方:

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

def unique_justseen(iterable, key=None):
    "List unique elements, preserving order. Remember only the element just seen."
    # unique_justseen('AAAABBBCCDAABBB') --> A B C D A B
    # unique_justseen('ABBCcAD', str.lower) --> A B C A D
    return imap(next, imap(itemgetter(1), groupby(iterable, key)))

【讨论】:

【参考方案9】:

删除重复并保留顺序:

这是一个快速的 2-liner,它利用了列表理解和字典的内置功能。

x = [1, 1, 2, 'a', 'a', 3]

tmpUniq =  # temp variable used below 
results = [tmpUniq.setdefault(i,i) for i in x if i not in tmpUniq]

print results
[1, 2, 'a', 3]

dict.setdefaults() 函数返回值并将其直接添加到列表推导中的临时字典中。使用内置函数和 dict 的哈希值将有助于最大限度地提高进程的效率。

【讨论】:

【参考方案10】:

一个就地的单线:

>>> x = [1, 1, 2, 'a', 'a', 3]
>>> [ item for pos,item in enumerate(x) if x.index(item)==pos ]
[1, 2, 'a', 3]

【讨论】:

嗨马里奥,这是如何工作的,请解释一下,我的理解是索引只返回一个值,所以它是唯一的? list.index(item) 方法返回列表中找到的第一个项的位置,并将其与该项的实际位置(来自枚举)进行比较因此可以判断该项目是否是第一个出现的,只保留第一个出现的。 谢谢,这是一个非常优雅的解决方案。 非常好的解决方案。但它真的是就地吗?当您使用列表推导时,不是正在创建一个新列表吗?【参考方案11】:

我没有做过任何测试,但一种可能的算法可能是创建第二个列表,然后遍历第一个列表。如果某个项目不在第二个列表中,请将其添加到第二个列表中。

x = [1, 1, 2, 'a', 'a', 3]
y = []
for each in x:
    if each not in y:
        y.append(each)

【讨论】:

我发现您对变量名“each”的使用读起来确实令人困惑,可能是因为在许多语言中它是一个关键字。使用 item 或只是 i 更清晰。 'i' 对我来说意味着一个索引——我们不是遍历索引,而是遍历对象。我更喜欢 item,但我不认为 'each' 很糟糕 - 只是因为它是另一种语言的关键字,为什么要阻止它在这里使用。语法突出显示(如上所示)可以很好地选择它... 除了 AppleScript,还有哪些语言使用“每个”这个词作为关键字? 你应该用过一套。这不太可能是最快的。 Marcin:“……同时保持秩序”。【参考方案12】:

这里有一些很棒、有效的解决方案。但是,对于那些不关心绝对最有效的O(n) 解决方案的人,我会选择简单的单线O(n^2*log(n)) 解决方案:

def unique(xs):
    return sorted(set(xs), key=lambda x: xs.index(x))

或者更高效的两线O(n*log(n))解决方案:

def unique(xs):
    positions = dict((e,pos) for pos,e in reversed(list(enumerate(xs))))
    return sorted(set(xs), key=lambda x: positions[x])

【讨论】:

该代码难以理解,而且您说它的效率低于此处已经介绍的其他解决方案。那你为什么要这么做呢? 我认为这很容易理解;传递一个 lambda 函数作为 sorted 的关键参数实际上是在 Python 中对列表进行排序的规范方法。我的大部分 Python 工作都涉及生成统计列表报告,所以对我来说,这似乎是最简单、最符合 Python 的方法。 虽然我同意您的解决方案简洁,但该问题要求的是最快的算法,而不是最 Pythonic。【参考方案13】:

强制性的基于生成器的变体:

def unique(seq):
  seen = set()
  for x in seq:
    if x not in seen:
      seen.add(x)
      yield x

【讨论】:

【参考方案14】:

如果您在 Terhost 的回答中从对 set() 的调用中取出空列表,您会获得一点速度提升。

改变: 找到 = 设置([]) 到: 找到 = set()

但是,你根本不需要这个集合。

def unique(items):
    keep = []

    for item in items:
        if item not in keep:
            keep.append(item)

    return keep

使用 timeit 我得到了这些结果:

使用 set([]) -- 4.97210427363 使用 set() -- 4.65712377445 没有设置 -- 3.44865284975

【讨论】:

是的,当您的数据很少时,我敢打赌,设置的内部机制比遍历列表要慢。但是如果你有 maaaaaaaaaaaany 元素,我认为 set 更快。或者这个数据结构的意义是什么;-)【参考方案15】:

这是我发现的最快的就地方法(假设有很大一部分重复):

def unique(l):
    s = set(); n = 0
    for x in l:
        if x not in s: s.add(x); l[n] = x; n += 1
    del l[n:]

这比 Allen 的实现快 10%,这是它所基于的(使用 timeit.repeat 计时,由 psyco 编译的 JIT)。它保留任何重复项的第一个实例。

repton-infinity:如果你能确认我的时间安排,我会很感兴趣。

【讨论】:

字典比集合稍快。看我的回答***.com/questions/89178/#282589【参考方案16】:

我不知道这个快不快,但至少它很简单。

简单地说,先将其转换为集合,然后再转换为列表

def unique(container):
  return list(set(container))

【讨论】:

这不会保留顺序。【参考方案17】:

一次通过。

a = [1,1,'a','b','c','c']

new_list = []
prev = None

while 1:
    try:
        i = a.pop(0)
        if i != prev:
            new_list.append(i)
        prev = i
    except IndexError:
        break

【讨论】:

需要排序输入,不是吗?【参考方案18】:

has_key 在 python 中是 O(1)。从哈希中插入和检索也是 O(1)。循环遍历 n 个项目两次,所以 O(n)。

def unique(list):
  s = 
  output = []
  for x in list:
    count = 1
    if(s.has_key(x)):
      count = s[x] + 1

    s[x] = count
  for x in list:
    count = s[x]
    if(count > 0):
      s[x] = 0
      output.append(x)
  return output

【讨论】:

【参考方案19】:

您实际上可以在 Python 中做一些非常酷的事情来解决这个问题。您可以创建一个列表推导式,该列表推导式将在构建时引用自身。如下:

   # remove duplicates...
   def unique(my_list):
       return [x for x in my_list if x not in locals()['_[1]'].__self__]

编辑:我删除了“self”,它适用于 Mac OS X、Python 2.5.1。

_[1] 是 Python 对新列表的“秘密”引用。当然,上述内容有点混乱,但您可以根据需要调整它以满足您的需求。例如,您实际上可以编写一个返回对推导的引用的函数;它看起来更像:

return [x for x in my_list if x not in this_list()]

【讨论】:

给定的示例无法为我编译 -- 结尾的 ".__self__" 无效 [[Linux 2.6 w/ Python 2.5.1]] 天哪,您正在通过神奇的下划线业务将 Python 转换为 Perl。直接说不吧。【参考方案20】:

如果 dict 是哈希,则 O(n),如果 dict 是树,则 O(nlogn),并且简单,固定。感谢马修的建议。抱歉,我不知道底层类型。

def unique(x):    
  output = []
  y = 
  for item in x:
    y[item] = ""

  for item in x:
    if item in y:
      output.append(item)

  return output

【讨论】:

仅供参考,您也可以使用集合来执行此操作,因此您不必将其设置为等于空字符串。【参考方案21】:

重复项是否必须首先出现在列表中?就查找元素而言,没有任何开销,但添加元素时会有更多开销(尽管开销应该是 O(1) )。

>>> x  = []
>>> y = set()
>>> def add_to_x(val):
...     if val not in y:
...             x.append(val)
...             y.add(val)
...     print x
...     print y
... 
>>> add_to_x(1)
[1]
set([1])
>>> add_to_x(1)
[1]
set([1])
>>> add_to_x(1)
[1]
set([1])
>>> 

【讨论】:

【参考方案22】:

单线:

new_list = reduce(lambda x,y: x+[y][:1-int(y in x)], my_list, [])

【讨论】:

【参考方案23】:
>>> x=[1,1,2,'a','a',3]
>>> y = [ _x for _x in x if not _x in locals()['_[1]'] ]
>>> y
[1, 2, 'a', 3]

“locals()['_[1]']”是正在创建的列表的“秘密名称”。

【讨论】:

语言不保证存在 _[1] local。 “ 中的”是 O(n),所以这很慢。【参考方案24】:

最快的取决于您的列表中有多少百分比是重复的。如果它几乎都是重复的,只有很少的独特项目,那么创建一个新列表可能会更快。如果它主要是独特的项目,从原始列表(或副本)中删除它们会更快。

这是一个用于修改列表的地方:

def unique(items):
  seen = set()
  for i in xrange(len(items)-1, -1, -1):
    it = items[i]
    if it in seen:
      del items[i]
    else:
      seen.add(it)

在索引上向后迭代可确保删除项目不会影响迭代。

【讨论】:

这给出了与其他解决方案不同的结果(OP 没有指定哪个是正确的),关于保留哪些副本。此解决方案:[1, 2, 1] -> [2, 1] 其他解决方案:[1, 2, 1] -> [1, 2] 我在问题文本中添加了对此的说明。【参考方案25】:
>>> def unique(list):
...   y = []
...   for x in list:
...     if x not in y:
...       y.append(x)
...   return y

【讨论】:

解释原因:在列表结构(y)中搜索x是O(n),而在集合(或字典)中搜索x是O(1)。【参考方案26】:

取自http://www.peterbe.com/plog/uniqifiers-benchmark

def f5(seq, idfun=None):  
    # order preserving 
    if idfun is None: 
        def idfun(x): return x 
    seen =  
    result = [] 
    for item in seq: 
        marker = idfun(item) 
        # in old Python versions: 
        # if seen.has_key(marker) 
        # but in new ones: 
        if marker in seen: continue 
        seen[marker] = 1 
        result.append(item) 
    return result

【讨论】:

它比相应的就地版本慢(至少对于某些输入)。见***.com/questions/89178/#282589【参考方案27】:

我没有使用 python 的经验,但是一种算法是对列表进行排序,然后删除重复项(通过与列表中的先前项目进行比较),最后通过与旧列表比较找到新列表中的位置。

更长的答案:http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52560

【讨论】:

以上是关于在 Python 中,从列表中删除重复项以使所有元素都是唯一的*同时保留顺序*的最快算法是啥? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

从列表中删除重复的无序元组

应用 set() 删除元组内列表中的重复项 - Python

如何根据元组的索引值从列表中删除重复的元组,同时保持元组的顺序? [复制]

从Python列表中的特定位置删除项目[重复]

从python列表中删除坐标[重复]

python 字典