web-dev-qa-db-fra.com

Comment vérifier s'il y a des doublons dans une liste à plat?

Par exemple, étant donné la liste ['one', 'two', 'one'], l'algorithme devrait renvoyer True, alors que pour ['one', 'two', 'three'] il devrait renvoyer False.

137
teggy

Utilisez set() pour supprimer les doublons si toutes les valeurs sont hashable :

>>> your_list = ['one', 'two', 'one']
>>> len(your_list) != len(set(your_list))
True
312
Denis Otkidach

Recommandé pour les listes short uniquement:

any(thelist.count(x) > 1 for x in thelist)

Utilisez pas sur une longue liste - cela peut prendre du temps proportionnellement au carré du nombre d'éléments de la liste!

Pour les listes plus longues comportant des éléments hashable (chaînes, nombres, etc.):

def anydup(thelist):
  seen = set()
  for x in thelist:
    if x in seen: return True
    seen.add(x)
  return False

Si vos articles ne sont pas haschables (sous-listes, cartes, etc.), ils deviennent plus poilus, bien qu'il soit encore possible d'obtenir O (N logN) s'ils sont au moins comparables. Mais vous devez connaître ou tester les caractéristiques des éléments (modifiables ou non, comparables ou non) pour obtenir la meilleure performance possible - O(N) pour hashables, O (N log N) pour les non comparables comparables, sinon c’est à O (N carré) et on ne peut rien y faire :-(.

40
Alex Martelli

C'est vieux, mais les réponses ici m'ont amené à une solution légèrement différente. Si vous êtes prêt à abuser de la compréhension, vous pouvez court-circuiter de cette façon.

xs = [1, 2, 1]
s = set()
any(x in s or s.add(x) for x in xs)
# You can use a similar approach to actually retrieve the duplicates.
s = set()
duplicates = set(x for x in xs if x in s or s.add(x))
10
pyrospade

Si vous aimez le style de programmation fonctionnelle, voici une fonction utile: code auto-documenté et testé avec doctest .

def decompose(a_list):
    """Turns a list into a set of all elements and a set of duplicated elements.

    Returns a pair of sets. The first one contains elements
    that are found at least once in the list. The second one
    contains elements that appear more than once.

    >>> decompose([1,2,3,5,3,2,6])
    (set([1, 2, 3, 5, 6]), set([2, 3]))
    """
    return reduce(
        lambda (u, d), o : (u.union([o]), d.union(u.intersection([o]))),
        a_list,
        (set(), set()))

if __== "__main__":
    import doctest
    doctest.testmod()

À partir de là, vous pouvez tester l'unicité en vérifiant si le deuxième élément de la paire renvoyée est vide:

def is_set(l):
    """Test if there is no duplicate element in l.

    >>> is_set([1,2,3])
    True
    >>> is_set([1,2,1])
    False
    >>> is_set([])
    True
    """
    return not decompose(l)[1]

Notez que cela n’est pas efficace puisque vous construisez explicitement la décomposition. Mais tout au long de l’utilisation de réduire, vous pouvez trouver quelque chose d’équivalent (mais légèrement moins efficace) pour répondre à 5:

def is_set(l):
    try:
        def func(s, o):
            if o in s:
                raise Exception
            return s.union([o])
        reduce(func, l, set())
        return True
    except:
        return False
10
Xavier Decoret

J'ai récemment répondu à une question connexe pour établir tous les doublons dans une liste, en utilisant un générateur. Cela a l'avantage que, si on ne l'utilise que pour établir "s'il y a un doublon", il suffit d'obtenir le premier élément et le reste peut être ignoré, ce qui constitue le raccourci ultime.

C’est une approche intéressante basée sur un ensemble que j’ai adaptée directement de moooeeeep :

def getDupes(l):
    seen = set()
    seen_add = seen.add
    for x in l:
        if x in seen or seen_add(x):
            yield x

En conséquence, une liste complète de dupes serait list(getDupes(etc)). Pour tester simplement "si" il y a une dupe, elle devrait être emballée comme suit:

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

Cela évolue bien et fournit des durées de fonctionnement cohérentes, où que la duplication se trouve dans la liste - j'ai testé avec des listes pouvant aller jusqu'à 1 m. Si vous savez quelque chose sur les données, en particulier, que des dupes sont susceptibles d'apparaître au cours de la première moitié, ou d'autres choses qui vous permettent de biaiser vos exigences, comme le besoin d'obtenir les dupes réelles, alors il existe deux ou plusieurs localisateurs de dupe vraiment différents cela pourrait surperformer. Les deux que je recommande sont ...

Approche simple basée sur le dict, très lisible:

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

Exploitez itertools (essentiellement un ifilter/izip/tee) dans la liste triée, très efficace si vous obtenez toutes les dupes, mais pas aussi rapidement que d’obtenir le premier:

def getDupes(c):
    a, b = itertools.tee(sorted(c))
    next(b, None)
    r = None
    for k, g in itertools.ifilter(lambda x: x[0]==x[1], itertools.izip(a, b)):
        if k != r:
            yield k
            r = k

Celles-ci étaient les plus performantes des approches que j’ai essayées pour la liste complète de dupes , la première dupe s’est produite n'importe où dans une liste d’éléments de 1 m du début à la moitié. Il était surprenant de voir à quel point l’étape de tri a été réduite au minimum. Votre kilométrage peut varier, mais voici mes résultats chronométrés spécifiques:

Finding FIRST duplicate, single dupe places "n" elements in to 1m element array

Test set len change :        50 -  . . . . .  -- 0.002
Test in dict        :        50 -  . . . . .  -- 0.002
Test in set         :        50 -  . . . . .  -- 0.002
Test sort/adjacent  :        50 -  . . . . .  -- 0.023
Test sort/groupby   :        50 -  . . . . .  -- 0.026
Test sort/Zip       :        50 -  . . . . .  -- 1.102
Test sort/izip      :        50 -  . . . . .  -- 0.035
Test sort/tee/izip  :        50 -  . . . . .  -- 0.024
Test moooeeeep      :        50 -  . . . . .  -- 0.001 *
Test iter*/sorted   :        50 -  . . . . .  -- 0.027

Test set len change :      5000 -  . . . . .  -- 0.017
Test in dict        :      5000 -  . . . . .  -- 0.003 *
Test in set         :      5000 -  . . . . .  -- 0.004
Test sort/adjacent  :      5000 -  . . . . .  -- 0.031
Test sort/groupby   :      5000 -  . . . . .  -- 0.035
Test sort/Zip       :      5000 -  . . . . .  -- 1.080
Test sort/izip      :      5000 -  . . . . .  -- 0.043
Test sort/tee/izip  :      5000 -  . . . . .  -- 0.031
Test moooeeeep      :      5000 -  . . . . .  -- 0.003 *
Test iter*/sorted   :      5000 -  . . . . .  -- 0.031

Test set len change :     50000 -  . . . . .  -- 0.035
Test in dict        :     50000 -  . . . . .  -- 0.023
Test in set         :     50000 -  . . . . .  -- 0.023
Test sort/adjacent  :     50000 -  . . . . .  -- 0.036
Test sort/groupby   :     50000 -  . . . . .  -- 0.134
Test sort/Zip       :     50000 -  . . . . .  -- 1.121
Test sort/izip      :     50000 -  . . . . .  -- 0.054
Test sort/tee/izip  :     50000 -  . . . . .  -- 0.045
Test moooeeeep      :     50000 -  . . . . .  -- 0.019 *
Test iter*/sorted   :     50000 -  . . . . .  -- 0.055

Test set len change :    500000 -  . . . . .  -- 0.249
Test in dict        :    500000 -  . . . . .  -- 0.145
Test in set         :    500000 -  . . . . .  -- 0.165
Test sort/adjacent  :    500000 -  . . . . .  -- 0.139
Test sort/groupby   :    500000 -  . . . . .  -- 1.138
Test sort/Zip       :    500000 -  . . . . .  -- 1.159
Test sort/izip      :    500000 -  . . . . .  -- 0.126
Test sort/tee/izip  :    500000 -  . . . . .  -- 0.120 *
Test moooeeeep      :    500000 -  . . . . .  -- 0.131
Test iter*/sorted   :    500000 -  . . . . .  -- 0.157
5
F1Rumors

Une autre façon de faire ceci de manière succincte est avec Counter .

Pour déterminer s'il y a des doublons dans la liste d'origine:

from collections import Counter

def has_dupes(l):
    # second element of the Tuple has number of repetitions
    return Counter(l).most_common()[0][1] > 1

Ou pour obtenir une liste d'éléments qui ont des doublons:

def get_dupes(l):
    return [k for k, v in Counter(l).items() if v > 1]
3
Turn

J'ai pensé qu'il serait utile de comparer les timings des différentes solutions présentées ici. Pour cela, j'ai utilisé ma propre bibliothèque simple_benchmark :

enter image description here

Donc, en effet, dans ce cas, la solution de Denis Otkidach est la plus rapide.

Certaines des approches présentent également une courbe beaucoup plus raide. Ce sont les approches d'échelle quadratique avec le nombre d'éléments (première solution d'Alex Martellis, wjandrea et les deux solutions de Xavier Decorets). Il est également important de mentionner que la solution pandas de Keiku a un très gros facteur constant. Mais pour les listes plus volumineuses, il rattrape presque les autres solutions.

Et au cas où le duplicata est à la première position. Ceci est utile pour voir quelles solutions court-circuiter:

enter image description here

Ici, plusieurs approches ne font pas court-circuiter: Kaiku, Frank, Xavier_Decoret (première solution), Turn, Alex Martelli (première solution) et l'approche présentée par Denis Otkidach (la plus rapide dans le cas sans duplicata).

J'ai inclus ici une fonction de ma propre bibliothèque: iteration_utilities.all_distinct qui peut rivaliser avec la solution la plus rapide dans le cas des non-doublons et s'exécute en temps constant pour le cas de la duplication au début ( bien que pas aussi rapide).

Le code pour le repère:

_from collections import Counter
from functools import reduce

import pandas as pd
from simple_benchmark import BenchmarkBuilder
from iteration_utilities import all_distinct

b = BenchmarkBuilder()

@b.add_function()
def Keiku(l):
    return pd.Series(l).duplicated().sum() > 0

@b.add_function()
def Frank(num_list):
    unique = []
    dupes = []
    for i in num_list:
        if i not in unique:
            unique.append(i)
        else:
            dupes.append(i)
    if len(dupes) != 0:
        return False
    else:
        return True

@b.add_function()
def wjandrea(iterable):
    seen = []
    for x in iterable:
        if x in seen:
            return True
        seen.append(x)
    return False

@b.add_function()
def user(iterable):
    clean_elements_set = set()
    clean_elements_set_add = clean_elements_set.add

    for possible_duplicate_element in iterable:

        if possible_duplicate_element in clean_elements_set:
            return True

        else:
            clean_elements_set_add( possible_duplicate_element )

    return False

@b.add_function()
def Turn(l):
    return Counter(l).most_common()[0][1] > 1

def getDupes(l):
    seen = set()
    seen_add = seen.add
    for x in l:
        if x in seen or seen_add(x):
            yield x

@b.add_function()          
def F1Rumors(l):
    try:
        if next(getDupes(l)): return True    # Found a dupe
    except StopIteration:
        pass
    return False

def decompose(a_list):
    return reduce(
        lambda u, o : (u[0].union([o]), u[1].union(u[0].intersection([o]))),
        a_list,
        (set(), set()))

@b.add_function()
def Xavier_Decoret_1(l):
    return not decompose(l)[1]

@b.add_function()
def Xavier_Decoret_2(l):
    try:
        def func(s, o):
            if o in s:
                raise Exception
            return s.union([o])
        reduce(func, l, set())
        return True
    except:
        return False

@b.add_function()
def pyrospade(xs):
    s = set()
    return any(x in s or s.add(x) for x in xs)

@b.add_function()
def Alex_Martelli_1(thelist):
    return any(thelist.count(x) > 1 for x in thelist)

@b.add_function()
def Alex_Martelli_2(thelist):
    seen = set()
    for x in thelist:
        if x in seen: return True
        seen.add(x)
    return False

@b.add_function()
def Denis_Otkidach(your_list):
    return len(your_list) != len(set(your_list))

@b.add_function()
def MSeifert04(l):
    return not all_distinct(l)
_

Et pour les arguments:

_
# No duplicate run
@b.add_arguments('list size')
def arguments():
    for exp in range(2, 14):
        size = 2**exp
        yield size, list(range(size))

# Duplicate at beginning run
@b.add_arguments('list size')
def arguments():
    for exp in range(2, 14):
        size = 2**exp
        yield size, [0, *list(range(size)]

# Running and plotting
r = b.run()
r.plot()
_
2
MSeifert

Je trouve que cela donne les meilleures performances car il court-circuite l'opération lorsque le premier dupliqué l'a trouvé, alors cet algorithme a une complexité temporelle et spatiale O(n) où n est la longueur de la liste:

def has_duplicated_elements(self, iterable):
    """ Given an `iterable`, return True if there are duplicated entries. """
    clean_elements_set = set()
    clean_elements_set_add = clean_elements_set.add

    for possible_duplicate_element in iterable:

        if possible_duplicate_element in clean_elements_set:
            return True

        else:
            clean_elements_set_add( possible_duplicate_element )

    return False
1
user

J'ai utilisé l'approche de pyrospade, pour sa simplicité, et l'ai légèrement modifiée sur une liste restreinte faite à partir du registre Windows insensible à la casse.

Si la chaîne de valeur PATH brute est divisée en chemins individuels, tous les chemins "nuls" (chaînes vides ou espaces uniquement) peuvent être supprimés à l'aide de:

PATH_nonulls = [s for s in PATH if s.strip()]

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

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

Le PATH d'origine contient à la fois des entrées 'null' et des doublons à des fins de test:

[list]  Root paths in HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment:PATH[list]  Root paths in HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment
  1  C:\Python37\
  2
  3
  4  C:\Python37\Scripts\
  5  c:\python37\
  6  C:\Program Files\ImageMagick-7.0.8-Q8
  7  C:\Program Files (x86)\poppler\bin
  8  D:\DATA\Sounds
  9  C:\Program Files (x86)\GnuWin32\bin
 10  C:\Program Files (x86)\Intel\iCLS Client\
 11  C:\Program Files\Intel\iCLS Client\
 12  D:\DATA\CCMD\FF
 13  D:\DATA\CCMD
 14  D:\DATA\UTIL
 15  C:\
 16  D:\DATA\UHELP
 17  %SystemRoot%\system32
 18
 19
 20  D:\DATA\CCMD\FF%SystemRoot%
 21  D:\DATA\Sounds
 22  %SystemRoot%\System32\Wbem
 23  D:\DATA\CCMD\FF
 24
 25
 26  c:\
 27  %SYSTEMROOT%\System32\WindowsPowerShell\v1.0\
 28

Les chemins nuls ont été supprimés, mais il existe toujours des doublons, par exemple, (1, 3) et (13, 20):

    [list]  Null paths removed from HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment:PATH
  1  C:\Python37\
  2  C:\Python37\Scripts\
  3  c:\python37\
  4  C:\Program Files\ImageMagick-7.0.8-Q8
  5  C:\Program Files (x86)\poppler\bin
  6  D:\DATA\Sounds
  7  C:\Program Files (x86)\GnuWin32\bin
  8  C:\Program Files (x86)\Intel\iCLS Client\
  9  C:\Program Files\Intel\iCLS Client\
 10  D:\DATA\CCMD\FF
 11  D:\DATA\CCMD
 12  D:\DATA\UTIL
 13  C:\
 14  D:\DATA\UHELP
 15  %SystemRoot%\system32
 16  D:\DATA\CCMD\FF%SystemRoot%
 17  D:\DATA\Sounds
 18  %SystemRoot%\System32\Wbem
 19  D:\DATA\CCMD\FF
 20  c:\
 21  %SYSTEMROOT%\System32\WindowsPowerShell\v1.0\

Et finalement, les dupes ont été enlevées:

[list]  Massaged path list from in HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment:PATH
  1  C:\Python37\
  2  C:\Python37\Scripts\
  3  C:\Program Files\ImageMagick-7.0.8-Q8
  4  C:\Program Files (x86)\poppler\bin
  5  D:\DATA\Sounds
  6  C:\Program Files (x86)\GnuWin32\bin
  7  C:\Program Files (x86)\Intel\iCLS Client\
  8  C:\Program Files\Intel\iCLS Client\
  9  D:\DATA\CCMD\FF
 10  D:\DATA\CCMD
 11  D:\DATA\UTIL
 12  C:\
 13  D:\DATA\UHELP
 14  %SystemRoot%\system32
 15  D:\DATA\CCMD\FF%SystemRoot%
 16  %SystemRoot%\System32\Wbem
 17  %SYSTEMROOT%\System32\WindowsPowerShell\v1.0\
0
Hewey Dewey

Si la liste contient des éléments indispensables, vous pouvez utiliser solution d'Alex Martelli mais avec une liste au lieu d'un ensemble, bien que cela soit plus lent pour les entrées plus volumineuses: O (N ^ 2).

def has_duplicates(iterable):
    seen = []
    for x in iterable:
        if x in seen:
            return True
        seen.append(x)
    return False
0
wjandrea

Je ne sais pas vraiment ce que fait l'ensemble dans les coulisses, alors j'aime simplement rester simple.

def dupes(num_list):
    unique = []
    dupes = []
    for i in num_list:
        if i not in unique:
            unique.append(i)
        else:
            dupes.append(i)
    if len(dupes) != 0:
        return False
    else:
        return True
0
Frank

Une solution plus simple est la suivante. Il suffit de cocher Vrai/Faux avec la méthode .duplicated() de pandas puis de prendre somme Veuillez également consulter pandas.Series.duplicated - documentation de pandas 0.24.1

import pandas as pd

def has_duplicated(l):
    return pd.Series(l).duplicated().sum() > 0

print(has_duplicated(['one', 'two', 'one']))
# True
print(has_duplicated(['one', 'two', 'three']))
# False
0
Keiku