Удаление дубликатов из списка списков

116

У меня есть список списков на Python:

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

И я хочу удалить из него повторяющиеся элементы. Было бы, если бы это был обычный список, а не списки, которые я мог бы использовать set. Но, к сожалению, этот список не хэшируемый и не может составлять набор списков. Только кортежей. Итак, я могу превратить все списки в кортежи, затем использовать набор и вернуться к спискам. Но это не так быстро.

Как это сделать наиболее эффективно?

Результатом приведенного выше списка должен быть:

k = [[5, 6, 2], [1, 2], [3], [4]]

Я не забочусь о сохранении порядка.

Примечание: этот вопрос похож, но не совсем то, что мне нужно. Искал SO, но не нашел точной копии.


Бенчмаркинг:

import itertools, time


class Timer(object):
    def __init__(self, name=None):
        self.name = name

    def __enter__(self):
        self.tstart = time.time()

    def __exit__(self, type, value, traceback):
        if self.name:
            print '[%s]' % self.name,
        print 'Elapsed: %s' % (time.time() - self.tstart)


k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [5, 2], [6], [8], [9]] * 5
N = 100000

print len(k)

with Timer('set'):
    for i in xrange(N):
        kt = [tuple(i) for i in k]
        skt = set(kt)
        kk = [list(i) for i in skt]


with Timer('sort'):
    for i in xrange(N):
        ks = sorted(k)
        dedup = [ks[i] for i in xrange(len(ks)) if i == 0 or ks[i] != ks[i-1]]


with Timer('groupby'):
    for i in xrange(N):
        k = sorted(k)
        dedup = list(k for k, _ in itertools.groupby(k))

with Timer('loop in'):
    for i in xrange(N):
        new_k = []
        for elem in k:
            if elem not in new_k:
                new_k.append(elem)

"loop in" (квадратичный метод) быстрее всех для коротких списков. Для длинных списков это быстрее, чем у всех, кроме метода группировки. Имеет ли это смысл?

Для короткого списка (тот, что в коде) 100000 итераций:

[set] Elapsed: 1.3900001049
[sort] Elapsed: 0.891000032425
[groupby] Elapsed: 0.780999898911
[loop in] Elapsed: 0.578000068665

Для более длинного списка (тот, что в коде продублирован 5 раз):

[set] Elapsed: 3.68700003624
[sort] Elapsed: 3.43799996376
[groupby] Elapsed: 1.03099989891
[loop in] Elapsed: 1.85900020599
zaharpopov
источник
1
Говоря «это не быстро», вы имеете в виду, что вы рассчитали время, и это недостаточно быстро для вашего приложения, или что вы думаете, что это не быстро?
Торстен Марек
@Torsten, кажется, слишком много копирования, чтобы быть умным методом. извините, интуиция. копировать списки в кортежи, затем в набор, затем обратно в список списков (снова копировать кортежи в списки)
zaharpopov 06
@zaharpopov: Python работает не так, ничего не будет скопировано , только новые контейнеры для существующих элементов (хотя для int это почти то же самое)
Йохен Ритцель,
3
1. Тайминги для методов, использующих сортировку, сбрасываются, потому что «k» отскакивает от отсортированного варианта. 2. Последний метод более быстрый, потому что способ генерации тестовых данных оставляет вам не более 4 отдельных элементов. Попробуйте что-нибудь. как K = [[int (u) для u в str (random.randrange (1, 1000))] для _ in range (100)]
Торстен Марек
@Torsten: исправлено спасибо. но все же метод цикла работает быстро, даже если в списке из 10 есть только один дубликат
zaharpopov 06

Ответы:

167
>>> k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]
>>> import itertools
>>> k.sort()
>>> list(k for k,_ in itertools.groupby(k))
[[1, 2], [3], [4], [5, 6, 2]]

itertoolsчасто предлагает самые быстрые и эффективные решения такого рода проблем, и с ними стоит хорошо ознакомиться! -)

Изменить : как я упоминал в комментарии, обычные усилия по оптимизации сосредоточены на больших входных данных (подход большого О), потому что это намного проще, поскольку он обеспечивает хорошую отдачу от усилий. Но иногда (в основном для «трагически критических узких мест» в глубоких внутренних циклах кода, которые раздвигают границы пределов производительности), может потребоваться более подробное рассмотрение, обеспечение распределения вероятностей, решение, какие показатели производительности следует оптимизировать (возможно, верхняя граница или 90-й центиль важнее среднего или медианного значения, в зависимости от приложения), выполняя, возможно, эвристические проверки в начале для выбора различных алгоритмов в зависимости от характеристик входных данных и т. д.

Тщательные измерения «точечной» производительности (код A и код B для конкретного входа) являются частью этого чрезвычайно дорогостоящего процесса, и timeitздесь помогает стандартный библиотечный модуль . Однако его проще использовать в приглашении оболочки. Например, вот небольшой модуль, демонстрирующий общий подход к этой проблеме, сохраните его как nodup.py:

import itertools

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

def doset(k, map=map, list=list, set=set, tuple=tuple):
  return map(list, set(map(tuple, k)))

def dosort(k, sorted=sorted, xrange=xrange, len=len):
  ks = sorted(k)
  return [ks[i] for i in xrange(len(ks)) if i == 0 or ks[i] != ks[i-1]]

def dogroupby(k, sorted=sorted, groupby=itertools.groupby, list=list):
  ks = sorted(k)
  return [i for i, _ in itertools.groupby(ks)]

def donewk(k):
  newk = []
  for i in k:
    if i not in newk:
      newk.append(i)
  return newk

# sanity check that all functions compute the same result and don't alter k
if __name__ == '__main__':
  savek = list(k)
  for f in doset, dosort, dogroupby, donewk:
    resk = f(k)
    assert k == savek
    print '%10s %s' % (f.__name__, sorted(resk))

Обратите внимание на проверку работоспособности (выполняется, когда вы просто делаете python nodup.py) и базовую технику подъема (сделайте постоянные глобальные имена локальными для каждой функции для скорости), чтобы уравнять все.

Теперь мы можем запустить проверку крошечного списка примеров:

$ python -mtimeit -s'import nodup' 'nodup.doset(nodup.k)'
100000 loops, best of 3: 11.7 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dosort(nodup.k)'
100000 loops, best of 3: 9.68 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dogroupby(nodup.k)'
100000 loops, best of 3: 8.74 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.donewk(nodup.k)'
100000 loops, best of 3: 4.44 usec per loop

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

$ python -mtimeit -s'import nodup' 'nodup.donewk([[i] for i in range(12)])'
10000 loops, best of 3: 25.4 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dogroupby([[i] for i in range(12)])'
10000 loops, best of 3: 23.7 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.doset([[i] for i in range(12)])'
10000 loops, best of 3: 31.3 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dosort([[i] for i in range(12)])'
10000 loops, best of 3: 25 usec per loop

квадратичный подход неплох, но лучше сортировка и группировка. И т. Д. И т. Д.

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

Также стоит подумать о том, чтобы сохранить другое представление k- почему это должен быть список списков, а не набор кортежей? Если задача удаления дубликатов выполняется часто и профилирование показывает, что это узкое место в производительности программы, например, постоянное сохранение набора кортежей и получение из него списка списков только в том случае и там, где это необходимо, может быть в целом быстрее.

Алекс Мартелли
источник
@alex спасибо за альтернативу. скорость этого метода примерно такая же, как у данбена, но на несколько% быстрее
zaharpopov 06
@alex: как ни странно, это медленнее, чем наивный квадратичный метод для более коротких списков (см. редактирование вопроса)
zaharpopov
@zaharpopov: так только в вашем частном случае, ср. мой комментарий к вопросу.
Торстен Марек
@zaharpopov, если вы дадите распределение вероятностей длин списков и подсписок и вероятность дублирования, можно (с огромными усилиями) вычислить / измерить распределение вероятностей времени выполнения для любого данного кода и оптимизировать любую нужную вам меру (медиана, среднее, 90-е центиль, что угодно). Это почти никогда не делается из-за очень низкой рентабельности инвестиций: обычно фокусируется на гораздо более простом случае больших входных данных (подход большого О), когда низкокачественные алгоритмы действительно ужасно ухудшают производительность. И я все равно не вижу, чтобы вы указывали какие-либо распределения вероятностей в своем Q ;-).
Alex Martelli
@zaharpov, рад, что вам понравилось!
Alex Martelli
21

Делаем это вручную, создаем новый kсписок и добавляем записи, которые пока не найдены:

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]
new_k = []
for elem in k:
    if elem not in new_k:
        new_k.append(elem)
k = new_k
print k
# prints [[1, 2], [4], [5, 6, 2], [3]]

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

Пол Стивенсон
источник
@paul: очень странно - этот метод быстрее всех остальных
zaharpopov 06
Я подозреваю, что этот метод не будет быстрее для очень длинных списков. Это будет зависеть от вашего приложения: если у вас действительно есть списки из шести элементов с двумя дубликатами, то любое решение, скорее всего, будет достаточно быстрым, и вам следует использовать самый ясный код.
Пол Стивенсон
@zaharpopov, в вашем тесте это не квадратично, потому что вы повторяете один и тот же список снова и снова. Вы тестируете линейный угловой вариант.
Майк Грэм,
k = ([[1, 2], [4], [5, 6, 2], [1, 2], [3], [5, 2], [6], [8], [9]] +[[x] for x in range(1000)]) *5прекрасно покажет квадратичное поведение
Джон Ла Рой
17
>>> k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]
>>> k = sorted(k)
>>> k
[[1, 2], [1, 2], [3], [4], [4], [5, 6, 2]]
>>> dedup = [k[i] for i in range(len(k)) if i == 0 or k[i] != k[i-1]]
>>> dedup
[[1, 2], [3], [4], [5, 6, 2]]

Я не знаю, обязательно ли это быстрее, но вам не обязательно использовать кортежи и наборы.

danben
источник
Спасибо, данбен. это быстрее, чем переходить к кортежам, затем «устанавливать», а затем обратно к спискам?
zaharpopov 06
Вы можете легко это проверить - напишите оба метода дедупликации, сгенерируйте несколько случайных списков с помощью randomи установите время time.
danben 06
4

Все setсвязанные решения этой проблемы до сих пор требуют создания целого setперед итерацией.

Можно сделать это «ленивым» и в то же время сохранить порядок, перебирая список списков и добавляя к «увиденному» set . Затем выдает список, только если он не найден в этом трекере set.

Этот unique_everseenрецепт доступен в itertools документации . Он также доступен в сторонней toolzбиблиотеке:

from toolz import unique

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

# lazy iterator
res = map(list, unique(map(tuple, k)))

print(list(res))

[[1, 2], [4], [5, 6, 2], [3]]

Обратите внимание, что tupleпреобразование необходимо, потому что списки не хешируются.

JPP
источник
3

Даже ваш «длинный» список довольно короткий. Кроме того, вы выбрали их, чтобы они соответствовали фактическим данным? Производительность будет зависеть от того, как на самом деле выглядят эти данные. Например, у вас есть короткий список, который повторяется снова и снова, чтобы составить более длинный список. Это означает, что квадратичное решение линейно в ваших тестах, но не в действительности.

Для действительно больших списков лучше всего использовать код набора - он линейный (хотя и требует много места). Методы sort и groupby - O (n log n), а метод loop in, очевидно, является квадратичным, поэтому вы знаете, как они будут масштабироваться, когда n станет действительно большим. Если это реальный размер данных, которые вы анализируете, то кого это волнует? Он крошечный.

Между прочим, я вижу заметное ускорение, если я не сформирую промежуточный список для создания набора, то есть если я заменю

kt = [tuple(i) for i in k]
skt = set(kt)

с участием

skt = set(tuple(i) for i in k)

Реальное решение может зависеть от дополнительной информации: вы уверены, что список списков действительно является тем представлением, которое вам нужно?

Майк Грэм
источник
3

Список кортежей и {} можно использовать для удаления дубликатов

>>> [list(tupl) for tupl in {tuple(item) for item in k }]
[[1, 2], [5, 6, 2], [3], [4]]
>>> 
SuperNova
источник
1

Создайте словарь с кортежем в качестве ключа и распечатайте ключи.

  • создать словарь с кортежем в качестве ключа и индексом в качестве значения
  • распечатать список ключей словаря

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

dict_tuple = {tuple(item): index for index, item in enumerate(k)}

print [list(itm) for itm in dict_tuple.keys()]

# prints [[1, 2], [5, 6, 2], [3], [4]]
SuperNova
источник
1

Это должно работать.

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

k_cleaned = []
for ele in k:
    if set(ele) not in [set(x) for x in k_cleaned]:
        k_cleaned.append(ele)
print(k_cleaned)

# output: [[1, 2], [4], [5, 6, 2], [3]]
Зои Л
источник
0

Как ни странно, приведенные выше ответы удаляют «дубликаты», но что, если я хочу также удалить дублированное значение? Следующее должно быть полезно и не создает новый объект в памяти!

def dictRemoveDuplicates(self):
    a=[[1,'somevalue1'],[1,'somevalue2'],[2,'somevalue1'],[3,'somevalue4'],[5,'somevalue5'],[5,'somevalue1'],[5,'somevalue1'],[5,'somevalue8'],[6,'somevalue9'],[6,'somevalue0'],[6,'somevalue1'],[7,'somevalue7']]


print(a)
temp = 0
position = -1
for pageNo, item in a:
    position+=1
    if pageNo != temp:
        temp = pageNo
        continue
    else:
        a[position] = 0
        a[position - 1] = 0
a = [x for x in a if x != 0]         
print(a)

и o / p:

[[1, 'somevalue1'], [1, 'somevalue2'], [2, 'somevalue1'], [3, 'somevalue4'], [5, 'somevalue5'], [5, 'somevalue1'], [5, 'somevalue1'], [5, 'somevalue8'], [6, 'somevalue9'], [6, 'somevalue0'], [6, 'somevalue1'], [7, 'somevalue7']]
[[2, 'somevalue1'], [3, 'somevalue4'], [7, 'somevalue7']]
zorze
источник
-1

Другое, вероятно, более общее и простое решение - создать словарь с ключом строковой версии объектов и получить значения () в конце:

>>> dict([(unicode(a),a) for a in [["A", "A"], ["A", "A"], ["A", "B"]]]).values()
[['A', 'B'], ['A', 'A']]

Загвоздка в том, что это работает только для объектов, строковое представление которых является достаточно хорошим уникальным ключом (что верно для большинства собственных объектов).

jacmkno
источник