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

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

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

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

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

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

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" (квадратичный метод), самый быстрый из всех для коротких списков. Для длинных списков это быстрее, чем все, кроме метода groupby. Это имеет смысл?

Для краткого списка (в коде) 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

Ответ 1

>>> 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 часто предлагает самые быстрые и мощные решения для подобных проблем и хорошо стоит знакомство с вами! -)

Изменить: как я упоминаю в комментарии, обычные усилия по оптимизации сосредоточены на больших входах (подход большой-O), потому что это намного проще, что дает хорошую отдачу от усилий. Но иногда (по существу, для "трагически важных узких мест" в глубоких внутренних циклах кода, которые подталкивают границы пределов производительности), может потребоваться более подробно рассказать о распределении вероятностей, определяя, какие показатели эффективности следует оптимизировать (возможно, верхняя граница или 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

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

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

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

Ответ 2

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

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

Ответ 3

Выполнение этого вручную, создание нового списка 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 для каждого элемента.

Ответ 4

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

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

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

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

с

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

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

Ответ 5

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

>>> [list(tupl) for tupl in {tuple(item) for item in k }]
[[1, 2], [5, 6, 2], [3], [4]]
>>> 

Ответ 6

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

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

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

Ответ 7

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

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

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