Как я могу проверить, есть ли дубликаты в плоском списке? - PullRequest
150 голосов
/ 09 октября 2009

Например, для списка ['one', 'two', 'one'] алгоритм должен вернуть True, а для ['one', 'two', 'three'] он должен вернуть False.

Ответы [ 12 ]

334 голосов
/ 09 октября 2009

Используйте set() для удаления дубликатов, если все значения hashable :

>>> your_list = ['one', 'two', 'one']
>>> len(your_list) != len(set(your_list))
True
42 голосов
/ 09 октября 2009

Рекомендуется только для коротких списков:

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

Не не используйте в длинном списке - это может занять время, пропорциональное квадрату количества элементов в списке!

Для более длинных списков с хэшируемыми элементами (строки, числа и т. Д.):

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

Если ваши элементы не могут быть хэшируемыми (подсписки, дикты и т. Д.), Они становятся более привлекательными, хотя все еще можно получить O (N logN), если они хотя бы сопоставимы. Но вам нужно знать или тестировать характеристики элементов (хэшируемые или нет, сопоставимые или нет), чтобы получить наилучшую производительность, какую вы можете - O (N) для хеш-файлов, O (N log N) для не хэшируемых сопоставлений, в противном случае это до O (N в квадрате) и с этим ничего не поделаешь: - (.

10 голосов
/ 29 августа 2013

Это старый, но ответы здесь привели меня к немного другому решению. Если вы склонны злоупотреблять пониманием, вы можете получить короткое замыкание таким образом.

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))
9 голосов
/ 06 июня 2011

Если вы любите стиль функционального программирования, вот полезная функция, самодокументированный и протестированный код с использованием 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 __name__ == "__main__":
    import doctest
    doctest.testmod()

Оттуда вы можете проверить уникальность, проверив, пуст ли второй элемент возвращаемой пары:

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]

Обратите внимание, что это неэффективно, поскольку вы явно строите декомпозицию. Но по принципу использования Reduce вы можете найти что-то эквивалентное (но чуть менее эффективное), чтобы ответить 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
5 голосов
/ 22 июля 2015

Я недавно ответил на связанный вопрос, чтобы установить все дубликаты в списке, используя генератор. Преимущество этого метода в том, что если его использовать просто для определения «есть ли дубликат», то вам просто нужно получить первый элемент, а остальные можно игнорировать, что является окончательным ярлыком.

Это интересный подход, основанный на множествах, который я адаптировал прямо из moooeeeep :

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

Соответственно, полный список обманщиков будет list(getDupes(etc)). Чтобы просто проверить «если» есть дублирование, его следует обернуть следующим образом:

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

Это хорошо масштабируется и обеспечивает стабильное время работы, где бы ни был дубликат в списке - я протестировал со списками до 1 м записей. Если вы знаете что-то о данных, в частности, о том, что дупсы, скорее всего, появятся в первой половине, или о других вещах, которые позволяют исказить ваши требования, например о необходимости получения реальных дупликов, то есть пара действительно альтернативных локаторов дупе это может превзойти. Я рекомендую два ...

Простой подход, основанный на диктовке, очень удобочитаемый:

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

Использование itertools (по сути, ifilter / izip / tee) в отсортированном списке, очень эффективно, если вы получаете все дубли, хотя и не так быстро, чтобы получить только первое:

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

Это были лучшие исполнители из подходов, которые я пробовал для полного списка , причем первый дублирование встречалось в любом месте списка элементов 1 м от начала до середины. Было удивительно, насколько мало добавилось в шаге сортировки. Ваш пробег может отличаться, но вот мои конкретные результаты:

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
3 голосов
/ 05 февраля 2018

Другой способ сделать это кратко - с помощью Counter .

Чтобы просто определить, есть ли дубликаты в исходном списке:

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

Или получить список предметов, имеющих дубликаты:

def get_dupes(l):
    return [k for k, v in Counter(l).items() if v > 1]
1 голос
/ 25 июня 2019

Я подумал, что было бы полезно сравнить время различных решений, представленных здесь. Для этого я использовал свою собственную библиотеку simple_benchmark:

enter image description here

Так что, действительно, для этого случая решение от Денис Откидач является самым быстрым.

Некоторые из подходов также демонстрируют гораздо более крутые кривые, это подходы, которые масштабируются квадратично с количеством элементов (первое решение Алекса Мартеллиса, wjandrea и оба решения Xavier Decorets). Также важно отметить, что решение для панд от Keiku имеет очень большой постоянный фактор. Но для больших списков он почти догоняет другие решения.

А в случае, если дубликат находится на первой позиции. Это полезно, чтобы увидеть, какие решения являются короткими замыканиями:

enter image description here

Здесь несколько подходов не замыкаются накоротко: Kaiku, Frank, Xavier_Decoret (первое решение), Turn, Alex Martelli (первое решение) и подход, представленный Денисом Откидачем (который был самым быстрым в случае без дублирования).

Я включил сюда функцию из моей собственной библиотеки: iteration_utilities.all_distinct, которая может конкурировать с самым быстрым решением в случае отсутствия дубликатов и работает в постоянном времени для случая duplicate-at-begin ( хотя и не так быстро).

Код для эталона:

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)

А за аргументы:


# 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()
1 голос
/ 09 июля 2018

Я обнаружил, что это обеспечивает лучшую производительность, потому что он закорачивает операцию, когда обнаружен первый дубликат, тогда этот алгоритм имеет сложность во времени и пространстве O (n), где n - длина списка:

def has_duplicated_elements(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
0 голосов
/ 27 июня 2019

Я использовал подход Pyrospade, для его простоты, и немного изменил его в коротком списке, сделанном из регистра Windows без учета регистра.

Если необработанная строка значения PATH разбита на отдельные пути, все «нулевые» пути (пустые или только пробельные строки) можно удалить с помощью:

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

Исходный PATH содержит как нулевые записи, так и дубликаты для тестирования:

[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

Нулевые пути были удалены, но все еще есть дубликаты, например, (1, 3) и (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\

И наконец, обманщики были удалены:

[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 голосов
/ 23 июня 2019

Если список содержит не подлежащие изменению элементы, вы можете использовать решение Алекса Мартелли , но со списком вместо набора, хотя для более крупных входов он медленнее: O (N ^ 2).

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