Как проверить, есть ли дубликаты в плоском списке?

185

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

teggy
источник

Ответы:

399

Используйте set()для удаления дубликатов, если все значения могут быть хэшируемыми :

>>> your_list = ['one', 'two', 'one']
>>> len(your_list) != len(set(your_list))
True
Денис Откидач
источник
17
Перед прочтением я попробовал your_list! = List (set (your_list)), который не будет работать, так как порядок элементов будет меняться. Использование len - хороший способ решить эту проблему
igniteflow
1
часто не работает с массивом чисел с плавающей запятой. См. stackoverflow.com/questions/60914705
Манас Догра
54

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

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 в квадрате), и с этим ничего не поделаешь :-(.

Алекс Мартелли
источник
21
Денис Откидач предложил решение, где вы просто создаете новый набор из списка, а затем проверяете его длину. Его преимущество в том, что он позволяет коду C внутри Python выполнять тяжелую работу. Ваше решение зацикливается на коде Python, но имеет преимущество в коротком замыкании, когда найдено одно совпадение. Если есть вероятность, что в списке, вероятно, нет дубликатов, мне нравится версия Дениса Откидача, но если есть вероятность, что в начале списка может быть дубликат, это решение будет лучше.
Steveha
1
Стоит потратить время на детали, хотя я думаю, что у Дениса было более точное решение.
Steve314
@steveha - преждевременная оптимизация?
Steve314
@ Steve314, какая преждевременная оптимизация? Я написал бы так, как написал Денис Откидач, поэтому я пытался понять, почему Алекс Мартелли (из известной книги Python Cookbook) написал по-другому. После того, как я немного подумал, я понял, что версия Алекса закорачивается, и я написал несколько мыслей о различиях. Как перейти от обсуждения различий к преждевременной оптимизации, корню всего зла?
Steveha
3
Если элементы могут быть хэшируемыми, решение для набора является более прямым и, как я выразился, быстрее (выходит, как только известен ответ - «короткое замыкание», сказал Стивеха). Построение предложенного вами слова (самый быстрый в виде коллекций. Счетчик), конечно же, гораздо медленнее (необходимо, чтобы allвсе составляло 1). Дикт со всеми значениями True, который вы также упоминаете, - это смехотворно, бесполезно раздутая имитация a set, без какой-либо добавленной стоимости. Big-O не все в программировании.
Алекс Мартелли
12

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

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))
pyrospade
источник
9

Если вы любите стиль функционального программирования, здесь полезная функция, самодокументированный и протестированный код с использованием 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
Ксавье Декорет
источник
Надо было сначала прочитать связанные вопросы. Это описано в stackoverflow.com/questions/1723072/…
Ксавье Декорет
1
Я
получаю сообщение
Это связано с тем, что распаковка в лямбда-списках аргументов была удалена в Python 3.x.
MSeifert
5

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

введите описание изображения здесь

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

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

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

введите описание изображения здесь

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

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

Код для теста:

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()
MSeifert
источник
Для справки: all_distinct функция написана на C .
пользователь
5

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

Это интересный подход, основанный на множествах, который я адаптировал прямо из 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
F1Rumors
источник
.next()Вызова в вашем втором блоке кода не работает на Python 3.x. Я думаю, что next(getDupes(l))должно работать на всех версиях Python, поэтому имеет смысл изменить это.
MSeifert
Также ifilterи ìzipможет быть просто заменен на встроенный filterи zipв Python 3.x.
MSeifert
@MSeifert решение работает для python 2.x как написано, и да, для py3 вы можете использовать фильтр и карту напрямую ... но кто-то, использующий решение py3 в базе кода py2, не получит преимущества, потому что он не будет работать как генератор. Явное лучше, чем неявное в этом случае;)
F1Rumors
3

Еще один способ сделать это кратко с помощью 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]
Перемена
источник
2
my_list = ['one', 'two', 'one']

duplicates = []

for value in my_list:
  if my_list.count(value) > 1:
    if value not in duplicates:
      duplicates.append(value)

print(duplicates) //["one"]
Джей Десаи
источник
1

Я обнаружил, что это дает лучшую производительность, потому что он закорачивает операцию, когда первый дубликат обнаружил ее, а затем этот алгоритм имеет сложность времени и пространства 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

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

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

Более простое решение заключается в следующем. Просто проверьте True / False с помощью .duplicated()метода панд, а затем возьмите сумму. Пожалуйста, смотрите также pandas.Series.duplicated - документация для 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
Keiku
источник
0

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

def has_duplicates(iterable):
    seen = []
    for x in iterable:
        if x in seen:
            return True
        seen.append(x)
    return False
wjandrea
источник
0

Я использовал подход 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
def check_duplicates(my_list):
    seen = {}
    for item in my_list:
        if seen.get(item):
            return True
        seen[item] = True
    return False
Ахмед Мерадж
источник
Как работает функция? Мне любопытно, как словарь "видел" заполнен.
Гора