Видалення дублікатів зі списку списків


116

У мене є список списків на Python:

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

І я хочу видалити з нього повторювані елементи. Якби це був звичайний список, а не списки, які я міг би використовувати set. Але прикро, що список не є доступним і не може складати набір списків. Тільки кортежів. Тож я можу перетворити всі списки на кортежі, а потім використовувати set та назад до списків. Але це не швидко.

Як це можна зробити найбільш ефективним способом?

Результатом вищевказаного списку повинен бути:

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

Мене не хвилює збереження порядку.

Примітка: це питання схоже, але не зовсім те, що мені потрібно. Шукали ТАК, але не знайшли точного дубліката.


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

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)

"цикл в" (квадратичний метод) найшвидший з усіх для коротких списків. Для довгих списків це швидше, ніж всі, крім методу 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
Під "це не швидко", ви маєте на увазі, що ви приурочили це, і це недостатньо швидко для вашої заявки, або що ви думаєте, що це не швидко?
Торстен Марек

@Torsten, просто здається, що занадто багато копіювання, щоб бути розумним методом. вибачте, почуття кишки. скопіюйте списки в кортежі, потім у набір, потім поверніться до списку (знову скопіюйте кортежі у списки)
Захарпопов

@zaharpopov: це не так, як працює Python, нічого не буде скопійовано , лише нові контейнери для існуючих елементів (хоча для ints - це майже те саме)
Jochen Ritzel

3
1. Часи для методів, що використовують сортування, зменшуються, оскільки "k" відновлюється до відсортованого варіанту. 2. Останній метод швидший, оскільки спосіб генерування тестових даних залишає у вас щонайменше 4 чіткі елементи. Спробуйте що-н. як K = [[int (u) for u in str (random.randrange (1, 1000))] for _ in range (100)]
Torsten Marek

@Torsten: зафіксована подяка. але все-таки циклічний метод швидкий, навіть коли в списку 10 є лише один дублікат
Захарпов

Відповіді:


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часто пропонує найшвидші та найпотужніші рішення подібного роду проблем, і варто добре ознайомитися з ним! -)

Редагувати : як я згадую в коментарі, звичайні зусилля з оптимізації зосереджені на великих входах (підхід big-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

квадратичний підхід непоганий, але сортування та груповий підхід краще. І т.д.

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

Також варто подумати про те, щоб дотримуватися іншого представлення k- чому це в першу чергу повинен бути список списків, а не набір кортежів? Якщо завдання з видалення дублікатів є частим, а профілювання показує, що це вузьке місце продуктивності програми, весь час зберігає набір кортежів і отримує з нього список списків, лише якщо це потрібно, наприклад, може бути швидшим.


@ Алекс дякую за альтернативу. цей спосіб приблизно з тією ж швидкістю, що і у Danben, на кілька% швидший
Захарпов

@alex: дивним чином це повільніше, ніж наївний квадратичний метод для коротших списків (див. редагування питань)
zahapopov

@zaharpopov: це так лише у вашому спеціальному випадку, пор. мій коментар до питання.
Торстен Марек

@zaharpopov, якщо ви надаєте ймовірний розподіл довжин списку та підпису та шансів дублікатів, можливо (з величезними зусиллями) обчислити / виміряти розподіл ймовірностей виконання для будь-якого заданого коду та оптимізувати будь-який захід, який вам потрібен (медіана, середня, 90-а центиль, що завгодно). Навряд чи це колись робиться через дуже низьку рентабельність інвестицій: зазвичай, акцент робиться на набагато простішому випадку великих входів (підхід big-O), де нижчі алгоритми дійсно страшно пошкоджують продуктивність. І я не бачу, що ви все одно вказуєте на розподіл ймовірностей у своєму Q ;-).
Алекс Мартеллі

@zaharpov, радий, що тобі сподобалось!
Алекс Мартеллі

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, це не квадратично у вашому орієнтирі, оскільки ви повторюєте той самий список знову і знову. Ви орієнтуєтеся з лінійним кутовим корпусом.
Майк Грем

k = ([[1, 2], [4], [5, 6, 2], [1, 2], [3], [5, 2], [6], [8], [9]] +[[x] for x in range(1000)]) *5добре покаже квадратичну поведінку
John La Rooy

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. це швидше, ніж перетворитись на кортежі, потім "встановити", а потім повернутися до списків?
Захарпов

Ви можете легко перевірити це - запишіть обидва способи виведення даних, генеруйте деякі випадкові списки, використовуючи randomчас і за допомогою цього time.
danben

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конверсія необхідна, оскільки списки не є доступними.


3

Навіть ваш "довгий" список досить короткий. Ви також вибрали їх, щоб вони відповідали фактичним даним? Продуктивність залежить від того, як виглядають ці дані. Наприклад, у вас короткий список повторюється знову і знову, щоб зробити більш довгий список. Це означає, що квадратичне рішення лінійне у ваших орієнтирах, а не в реальності.

Для справді великих списків найкращий вибір встановленого коду - лінійний (хоча і простір). Методами сортування та групування є O (n log n), і цикл методу, очевидно, квадратичний, тому ви знаєте, як вони будуть масштабуватися, коли 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]]
>>> 

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

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

-1

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

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

Проблема полягає в тому, що це працює лише для об'єктів, представлення яких є строковим унікальним ключем (що справедливо для більшості нативних об'єктів).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.