575 votes

Comment trouver les doublons dans une liste et créer une autre liste avec eux ?

Comment puis-je trouver les doublons dans une liste Python et créer une autre liste des doublons ? La liste ne contient que des entiers.

1 votes

1 votes

Voulez-vous les duplicata une fois, ou à chaque fois qu'ils sont vus à nouveau ?

0 votes

Je pense que cette question a été traitée avec beaucoup plus d'efficacité ici. stackoverflow.com/a/642919/1748045 L'intersection est une méthode intégrée au jeu et devrait faire exactement ce qui est requis.

771voto

georg Points 52691

Pour supprimer les doublons, utilisez set(a) . Pour imprimer les doublons, quelque chose comme :

a = [1,2,3,2,1,5,6,5,5,5]

import collections
print([item for item, count in collections.Counter(a).items() if count > 1])

## [1, 2, 5]

Notez que Counter n'est pas particulièrement efficace ( horaires ) et probablement exagéré ici. set sera plus performant. Ce code calcule une liste d'éléments uniques dans l'ordre source :

seen = set()
uniq = []
for x in a:
    if x not in seen:
        uniq.append(x)
        seen.add(x)

ou, de manière plus concise :

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

Je ne recommande pas ce dernier style, car il n'est pas évident de savoir ce qu'il faut faire. not seen.add(x) fait (l'ensemble add() retourne toujours None d'où la nécessité de not ).

Calculer la liste des éléments dupliqués sans bibliothèques :

seen = set()
dupes = []

for x in a:
    if x in seen:
        dupes.append(x)
    else:
        seen.add(x)

ou, de manière plus concise :

seen = set()
dupes = [x for x in a if x in seen or seen.add(x)]    

Si les éléments d'une liste ne sont pas hachables, vous ne pouvez pas utiliser les sets/dicts et devez recourir à une solution en temps quadratique (comparez chacun d'entre eux). Par exemple :

a = [[1], [2], [3], [1], [5], [3]]

no_dupes = [x for n, x in enumerate(a) if x not in a[:n]]
print no_dupes # [[1], [2], [3], [5]]

dupes = [x for n, x in enumerate(a) if x in a[:n]]
print dupes # [[1], [3]]

0 votes

Je constate que ce code ne fonctionne pas avec une liste de listes. La solution proposée par @ritesh fonctionne avec une liste de listes. J'utilise la version 2.7.6 de python.

1 votes

"Le compteur n'est pas particulièrement efficace (timings)" dans le lien que vous fournissez, Counter est plus efficace que set pour le timing...

0 votes

Cela semble intéressant, pouvez-vous nous dire quelle est la complexité temporelle de ce projet ?

392voto

Ritesh Kumar Points 571
>>> l = [1,2,3,4,4,5,5,6,1]
>>> set([x for x in l if l.count(x) > 1])
set([1, 4, 5])

4 votes

Y a-t-il une raison pour laquelle vous utilisez une compréhension de liste au lieu d'une compréhension de générateur ?

92 votes

En effet, une solution simple, mais la complexité est au carré car chaque count() analyse la liste à nouveau, donc à ne pas utiliser pour les grandes listes.

4 votes

@JohnJ, le tri à bulles est également simple et fonctionne. Cela ne veut pas dire qu'il faut l'utiliser !

99voto

moooeeeep Points 10167

Vous n'avez pas besoin du nombre, juste de savoir si l'élément a déjà été vu ou non. Adapté de cette réponse à ce problème :

def list_duplicates(seq):
  seen = set()
  seen_add = seen.add
  # adds all elements it doesn't know yet to seen and all other to seen_twice
  seen_twice = set( x for x in seq if x in seen or seen_add(x) )
  # turn the set into a list (as requested)
  return list( seen_twice )

a = [1,2,3,2,1,5,6,5,5,5]
list_duplicates(a) # yields [1, 2, 5]

Juste au cas où la vitesse compte, voici quelques timings :

# file: test.py
import collections

def thg435(l):
    return [x for x, y in collections.Counter(l).items() if y > 1]

def moooeeeep(l):
    seen = set()
    seen_add = seen.add
    # adds all elements it doesn't know yet to seen and all other to seen_twice
    seen_twice = set( x for x in l if x in seen or seen_add(x) )
    # turn the set into a list (as requested)
    return list( seen_twice )

def RiteshKumar(l):
    return list(set([x for x in l if l.count(x) > 1]))

def JohnLaRooy(L):
    seen = set()
    seen2 = set()
    seen_add = seen.add
    seen2_add = seen2.add
    for item in L:
        if item in seen:
            seen2_add(item)
        else:
            seen_add(item)
    return list(seen2)

l = [1,2,3,2,1,5,6,5,5,5]*100

Voici les résultats : (bien joué @JohnLaRooy !)

$ python -mtimeit -s 'import test' 'test.JohnLaRooy(test.l)'
10000 loops, best of 3: 74.6 usec per loop
$ python -mtimeit -s 'import test' 'test.moooeeeep(test.l)'
10000 loops, best of 3: 91.3 usec per loop
$ python -mtimeit -s 'import test' 'test.thg435(test.l)'
1000 loops, best of 3: 266 usec per loop
$ python -mtimeit -s 'import test' 'test.RiteshKumar(test.l)'
100 loops, best of 3: 8.35 msec per loop

Il est intéressant de noter que, outre les délais eux-mêmes, le classement change légèrement lorsque pypy est utilisé. Le plus intéressant est que l'approche basée sur les compteurs bénéficie énormément des optimisations de pypy, alors que l'approche de mise en cache des méthodes que j'ai suggérée semble n'avoir presque aucun effet.

$ pypy -mtimeit -s 'import test' 'test.JohnLaRooy(test.l)'
100000 loops, best of 3: 17.8 usec per loop
$ pypy -mtimeit -s 'import test' 'test.thg435(test.l)'
10000 loops, best of 3: 23 usec per loop
$ pypy -mtimeit -s 'import test' 'test.moooeeeep(test.l)'
10000 loops, best of 3: 39.3 usec per loop

Apparemment, cet effet est lié à la "duplicité" des données d'entrée. J'ai défini l = [random.randrange(1000000) for i in xrange(10000)] et j'ai obtenu ces résultats :

$ pypy -mtimeit -s 'import test' 'test.moooeeeep(test.l)'
1000 loops, best of 3: 495 usec per loop
$ pypy -mtimeit -s 'import test' 'test.JohnLaRooy(test.l)'
1000 loops, best of 3: 499 usec per loop
$ pypy -mtimeit -s 'import test' 'test.thg435(test.l)'
1000 loops, best of 3: 1.68 msec per loop

6 votes

Juste par curiosité - quel est le but de seen_add = seen.add ici ?

3 votes

@Rob De cette façon, vous n'avez qu'à appeler la fonction que vous avez cherchée une fois auparavant. Sinon, il vous faudrait rechercher (par une requête dans le dictionnaire) la fonction membre suivante add chaque fois qu'une insertion serait nécessaire.

0 votes

J'ai vérifié avec mes propres données et le %timeit d'Ipython que votre méthode semble la plus rapide sur le test MAIS : "L'exécution la plus lente a pris 4,34 fois plus de temps que la plus rapide. Cela pourrait signifier qu'un résultat intermédiaire est mis en cache".

62voto

MSeifert Points 6307

Vous pouvez utiliser iteration_utilities.duplicates :

>>> from iteration_utilities import duplicates

>>> list(duplicates([1,1,2,1,2,3,4,2]))
[1, 1, 2, 2]

ou si vous ne voulez qu'un seul de chaque duplicata, cela peut être combiné avec iteration_utilities.unique_everseen :

>>> from iteration_utilities import unique_everseen

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

Il peut également traiter des éléments non hachables (au détriment toutefois des performances) :

>>> list(duplicates([[1], [2], [1], [3], [1]]))
[[1], [1]]

>>> list(unique_everseen(duplicates([[1], [2], [1], [3], [1]])))
[[1]]

C'est quelque chose que seules quelques-unes des autres approches ici peuvent gérer.

Repères

J'ai fait un benchmark rapide contenant la plupart (mais pas toutes) des approches mentionnées ici.

Le premier repère n'incluait qu'une petite gamme de longueurs de liste, car certaines approches ont des longueurs de liste très différentes. O(n**2) comportement.

Dans les graphiques, l'axe des y représente le temps, de sorte qu'une valeur plus faible signifie un meilleur résultat. Le graphique est également tracé en log-log afin de mieux visualiser la large gamme de valeurs :

enter image description here

Retirer le O(n**2) approche J'ai fait un autre benchmark jusqu'à un demi-million d'éléments dans une liste :

enter image description here

Comme vous pouvez le constater, le iteration_utilities.duplicates est plus rapide que toutes les autres approches et même le chaînage unique_everseen(duplicates(...)) était plus rapide ou aussi rapide que les autres approches.

Une autre chose intéressante à noter ici est que les approches des pandas sont très lentes pour les petites listes mais peuvent facilement rivaliser pour les listes plus longues.

Toutefois, comme le montrent ces bancs d'essai, la plupart des approches ont des performances à peu près égales, de sorte que l'une ou l'autre n'a pas beaucoup d'importance (à l'exception des 3 qui avaient O(n**2) d'exécution).

from iteration_utilities import duplicates, unique_everseen
from collections import Counter
import pandas as pd
import itertools

def georg_counter(it):
    return [item for item, count in Counter(it).items() if count > 1]

def georg_set(it):
    seen = set()
    uniq = []
    for x in it:
        if x not in seen:
            uniq.append(x)
            seen.add(x)

def georg_set2(it):
    seen = set()
    return [x for x in it if x not in seen and not seen.add(x)]   

def georg_set3(it):
    seen = {}
    dupes = []

    for x in it:
        if x not in seen:
            seen[x] = 1
        else:
            if seen[x] == 1:
                dupes.append(x)
            seen[x] += 1

def RiteshKumar_count(l):
    return set([x for x in l if l.count(x) > 1])

def moooeeeep(seq):
    seen = set()
    seen_add = seen.add
    # adds all elements it doesn't know yet to seen and all other to seen_twice
    seen_twice = set( x for x in seq if x in seen or seen_add(x) )
    # turn the set into a list (as requested)
    return list( seen_twice )

def F1Rumors_implementation(c):
    a, b = itertools.tee(sorted(c))
    next(b, None)
    r = None
    for k, g in zip(a, b):
        if k != g: continue
        if k != r:
            yield k
            r = k

def F1Rumors(c):
    return list(F1Rumors_implementation(c))

def Edward(a):
    d = {}
    for elem in a:
        if elem in d:
            d[elem] += 1
        else:
            d[elem] = 1
    return [x for x, y in d.items() if y > 1]

def wordsmith(a):
    return pd.Series(a)[pd.Series(a).duplicated()].values

def NikhilPrabhu(li):
    li = li.copy()
    for x in set(li):
        li.remove(x)

    return list(set(li))

def firelynx(a):
    vc = pd.Series(a).value_counts()
    return vc[vc > 1].index.tolist()

def HenryDev(myList):
    newList = set()

    for i in myList:
        if myList.count(i) >= 2:
            newList.add(i)

    return list(newList)

def yota(number_lst):
    seen_set = set()
    duplicate_set = set(x for x in number_lst if x in seen_set or seen_set.add(x))
    return seen_set - duplicate_set

def IgorVishnevskiy(l):
    s=set(l)
    d=[]
    for x in l:
        if x in s:
            s.remove(x)
        else:
            d.append(x)
    return d

def it_duplicates(l):
    return list(duplicates(l))

def it_unique_duplicates(l):
    return list(unique_everseen(duplicates(l)))

Point de repère 1

from simple_benchmark import benchmark
import random

funcs = [
    georg_counter, georg_set, georg_set2, georg_set3, RiteshKumar_count, moooeeeep, 
    F1Rumors, Edward, wordsmith, NikhilPrabhu, firelynx,
    HenryDev, yota, IgorVishnevskiy, it_duplicates, it_unique_duplicates
]

args = {2**i: [random.randint(0, 2**(i-1)) for _ in range(2**i)] for i in range(2, 12)}

b = benchmark(funcs, args, 'list size')

b.plot()

Repère 2

funcs = [
    georg_counter, georg_set, georg_set2, georg_set3, moooeeeep, 
    F1Rumors, Edward, wordsmith, firelynx,
    yota, IgorVishnevskiy, it_duplicates, it_unique_duplicates
]

args = {2**i: [random.randint(0, 2**(i-1)) for _ in range(2**i)] for i in range(2, 20)}

b = benchmark(funcs, args, 'list size')
b.plot()

Avis de non-responsabilité

1 Ceci provient d'une bibliothèque tierce que j'ai écrite : iteration_utilities .

32voto

F1Rumors Points 831

Je suis tombé sur cette question en cherchant quelque chose de connexe - et je me demande pourquoi personne n'a proposé une solution basée sur un générateur ? La solution de ce problème serait :

>>> print list(getDupes_9([1,2,3,2,1,5,6,5,5,5]))
[1, 2, 5]

J'étais préoccupé par l'extensibilité, et j'ai donc testé plusieurs approches, y compris des éléments naïfs qui fonctionnent bien sur de petites listes, mais qui s'étendent horriblement lorsque les listes deviennent plus grandes (note : il aurait été préférable d'utiliser timeit, mais ceci est illustratif).

J'ai inclus @moooeeeep pour comparaison (il est impressionnant de rapidité : le plus rapide si la liste d'entrée est complètement aléatoire) et une approche itertools qui est encore plus rapide pour des listes majoritairement triées... Inclut maintenant l'approche pandas de @firelynx -- lente, mais pas horriblement, et simple. Note - l'approche sort/tee/zip est systématiquement la plus rapide sur ma machine pour les grandes listes majoritairement ordonnées, moooeeeep est la plus rapide pour les listes mélangées, mais votre kilométrage peut varier.

Avantages

  • très rapide et simple pour tester les doublons en utilisant le même code.

Hypothèses

  • Les doublons doivent être signalés une seule fois
  • Le double de l'ordre ne doit pas être conservé
  • Le doublon peut se trouver n'importe où dans la liste

Solution la plus rapide, entrées 1m :

def getDupes(c):
        '''sort/tee/izip'''
        a, b = itertools.tee(sorted(c))
        next(b, None)
        r = None
        for k, g in itertools.izip(a, b):
            if k != g: continue
            if k != r:
                yield k
                r = k

Approches testées

import itertools
import time
import random

def getDupes_1(c):
    '''naive'''
    for i in xrange(0, len(c)):
        if c[i] in c[:i]:
            yield c[i]

def getDupes_2(c):
    '''set len change'''
    s = set()
    for i in c:
        l = len(s)
        s.add(i)
        if len(s) == l:
            yield i

def getDupes_3(c):
    '''in dict'''
    d = {}
    for i in c:
        if i in d:
            if d[i]:
                yield i
                d[i] = False
        else:
            d[i] = True

def getDupes_4(c):
    '''in set'''
    s,r = set(),set()
    for i in c:
        if i not in s:
            s.add(i)
        elif i not in r:
            r.add(i)
            yield i

def getDupes_5(c):
    '''sort/adjacent'''
    c = sorted(c)
    r = None
    for i in xrange(1, len(c)):
        if c[i] == c[i - 1]:
            if c[i] != r:
                yield c[i]
                r = c[i]

def getDupes_6(c):
    '''sort/groupby'''
    def multiple(x):
        try:
            x.next()
            x.next()
            return True
        except:
            return False
    for k, g in itertools.ifilter(lambda x: multiple(x[1]), itertools.groupby(sorted(c))):
        yield k

def getDupes_7(c):
    '''sort/zip'''
    c = sorted(c)
    r = None
    for k, g in zip(c[:-1],c[1:]):
        if k == g:
            if k != r:
                yield k
                r = k

def getDupes_8(c):
    '''sort/izip'''
    c = sorted(c)
    r = None
    for k, g in itertools.izip(c[:-1],c[1:]):
        if k == g:
            if k != r:
                yield k
                r = k

def getDupes_9(c):
    '''sort/tee/izip'''
    a, b = itertools.tee(sorted(c))
    next(b, None)
    r = None
    for k, g in itertools.izip(a, b):
        if k != g: continue
        if k != r:
            yield k
            r = k

def getDupes_a(l):
    '''moooeeeep'''
    seen = set()
    seen_add = seen.add
    # adds all elements it doesn't know yet to seen and all other to seen_twice
    for x in l:
        if x in seen or seen_add(x):
            yield x

def getDupes_b(x):
    '''iter*/sorted'''
    x = sorted(x)
    def _matches():
        for k,g in itertools.izip(x[:-1],x[1:]):
            if k == g:
                yield k
    for k, n in itertools.groupby(_matches()):
        yield k

def getDupes_c(a):
    '''pandas'''
    import pandas as pd
    vc = pd.Series(a).value_counts()
    i = vc[vc > 1].index
    for _ in i:
        yield _

def hasDupes(fn,c):
    try:
        if fn(c).next(): return True    # Found a dupe
    except StopIteration:
        pass
    return False

def getDupes(fn,c):
    return list(fn(c))

STABLE = True
if STABLE:
    print 'Finding FIRST then ALL duplicates, single dupe of "nth" placed element in 1m element array'
else:
    print 'Finding FIRST then ALL duplicates, single dupe of "n" included in randomised 1m element array'
for location in (50,250000,500000,750000,999999):
    for test in (getDupes_2, getDupes_3, getDupes_4, getDupes_5, getDupes_6,
                 getDupes_8, getDupes_9, getDupes_a, getDupes_b, getDupes_c):
        print 'Test %-15s:%10d - '%(test.__doc__ or test.__name__,location),
        deltas = []
        for FIRST in (True,False):
            for i in xrange(0, 5):
                c = range(0,1000000)
                if STABLE:
                    c[0] = location
                else:
                    c.append(location)
                    random.shuffle(c)
                start = time.time()
                if FIRST:
                    print '.' if location == test(c).next() else '!',
                else:
                    print '.' if [location] == list(test(c)) else '!',
                deltas.append(time.time()-start)
            print ' -- %0.3f  '%(sum(deltas)/len(deltas)),
        print
    print

Les résultats du test "tous les doublons" étaient cohérents, trouvant le "premier" doublon puis "tous" les doublons dans ce tableau :

Finding FIRST then ALL duplicates, single dupe of "nth" placed element in 1m element array
Test set len change :    500000 -  . . . . .  -- 0.264   . . . . .  -- 0.402  
Test in dict        :    500000 -  . . . . .  -- 0.163   . . . . .  -- 0.250  
Test in set         :    500000 -  . . . . .  -- 0.163   . . . . .  -- 0.249  
Test sort/adjacent  :    500000 -  . . . . .  -- 0.159   . . . . .  -- 0.229  
Test sort/groupby   :    500000 -  . . . . .  -- 0.860   . . . . .  -- 1.286  
Test sort/izip      :    500000 -  . . . . .  -- 0.165   . . . . .  -- 0.229  
Test sort/tee/izip  :    500000 -  . . . . .  -- 0.145   . . . . .  -- 0.206  *
Test moooeeeep      :    500000 -  . . . . .  -- 0.149   . . . . .  -- 0.232  
Test iter*/sorted   :    500000 -  . . . . .  -- 0.160   . . . . .  -- 0.221  
Test pandas         :    500000 -  . . . . .  -- 0.493   . . . . .  -- 0.499  

Lorsque les listes sont mélangées en premier, le prix du tri devient apparent - l'efficacité diminue sensiblement et l'approche @moooeeeep domine, les approches set et dict étant similaires mais moins performantes :

Finding FIRST then ALL duplicates, single dupe of "n" included in randomised 1m element array
Test set len change :    500000 -  . . . . .  -- 0.321   . . . . .  -- 0.473  
Test in dict        :    500000 -  . . . . .  -- 0.285   . . . . .  -- 0.360  
Test in set         :    500000 -  . . . . .  -- 0.309   . . . . .  -- 0.365  
Test sort/adjacent  :    500000 -  . . . . .  -- 0.756   . . . . .  -- 0.823  
Test sort/groupby   :    500000 -  . . . . .  -- 1.459   . . . . .  -- 1.896  
Test sort/izip      :    500000 -  . . . . .  -- 0.786   . . . . .  -- 0.845  
Test sort/tee/izip  :    500000 -  . . . . .  -- 0.743   . . . . .  -- 0.804  
Test moooeeeep      :    500000 -  . . . . .  -- 0.234   . . . . .  -- 0.311  *
Test iter*/sorted   :    500000 -  . . . . .  -- 0.776   . . . . .  -- 0.840  
Test pandas         :    500000 -  . . . . .  -- 0.539   . . . . .  -- 0.540

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X