Як ефективно порівняти два не упорядковані списки (а не набори) в Python?


141
a = [1, 2, 3, 1, 2, 3]
b = [3, 2, 1, 3, 2, 1]

a & b слід вважати рівними, оскільки вони мають абсолютно однакові елементи, лише в іншому порядку.

Справа в тому, що мої фактичні списки будуть складатися з об’єктів (мої екземпляри класу), а не цілих чисел.


7
Як порівнюються об'єкти?
— Marcelo Cantos

2
який очікуваний розмір реальних списків? Чи будуть списки, що порівнюються, бути порівнянних розмірів чи дуже різними? Чи очікуєте, що більшість списків збігаються чи ні?
— Дмитро Б.

Можна перевірити len()спершу.
— сіра борода

Відповіді:


245

O (n) : Найкращим є метод Counter () (якщо ваші об'єкти є доступними):

def compare(s, t):
    return Counter(s) == Counter(t)

O (n log n) : Найкращим є наступний метод сортування () (якщо ваші об'єкти впорядковані):

def compare(s, t):
    return sorted(s) == sorted(t)

O (n * n) : Якщо об'єкти не є ні доступними, ні упорядкованими, ви можете використовувати рівність:

def compare(s, t):
    t = list(t)   # make a mutable copy
    try:
        for elem in s:
            t.remove(elem)
    except ValueError:
        return False
    return not t

1
Дякую. Я перетворив кожен об'єкт у рядок, потім застосував метод Counter ().
— johndir

Ей, @ Реймонд, я нещодавно на інтерв'ю стикався з цим питанням, і використовував sorted(), правда, не знаючи про це Counter. Інтерв'юер наполягав, що існує більш ефективний метод, і я чітко намалював його. Після розширеного тестування в python 3 з timeitмодулем сортування послідовно виходить швидше за списками цілих чисел. У списках - 1 к. Предметів, приблизно на 1,5% повільніше, а в коротких списках - 10 предметів, на 7,5% повільніше. Думки?
— arctelix

4
Для коротких списків аналіз big-O, як правило, не має значення, оскільки в таймінгах переважають постійні фактори. Що стосується більш довгих списків, я підозрюю, що з вашим бенчмаркінгом щось не так. За 100 вкладок з 5 повтореннями я отримую: 127 Usec для відсортованого та 42 для Counter (приблизно в 3 рази швидше). На 1000 входів з 5 повтореннями, лічильник в 4 рази швидший. python3.6 -m timeit -s 'from collections import Counter' -s 'from random import shuffle' -s 't=list(range(100)) * 5' -s 'shuffle(t)' -s 'u=t[:]' -s 'shuffle(u)' 'Counter(t)==Counter(u)'
— Реймонд Хеттінгер

@Raymond Дійсно ми отримуємо різні результати. Я розмістив налаштування в чаті sorted vs counter. Мені дуже цікаво, що тут відбувається.
— arctelix

4
Ні, дякую. Я не маю особливого інтересу до налагодження помилкових сценаріїв синхронізації. Тут багато чого відбувається (чистий python vs C код, тимчасовий додаток до рандомізованих даних проти напівзамовних даних, різні деталі реалізації в різних версіях, скільки дублікатів у даних тощо)
— Реймонд Хеттінгер

16

Ви можете сортувати обидва:

sorted(a) == sorted(b)

Підрахунку роду також може бути більш ефективним (але це вимагає , щоб об'єкт бути hashable).

>>> from collections import Counter
>>> a = [1, 2, 3, 1, 2, 3]
>>> b = [3, 2, 1, 3, 2, 1]
>>> print (Counter(a) == Counter(b))
True

Лічильник використовує хешування, але об'єкти самі по собі не є незмінними. Вам просто потрібно реалізувати розумне __hash__, але це може бути неможливим для колекцій.
— Йохен Рітцель

2
впорядковані також не будуть працювати для всього, наприклад, складні номериsorted([0, 1j])
— Джон Ла Рой,

1
sorted () також не працює з наборами, де оператори порівняння були замінені для тестів підмножини / суперсети.
— Реймонд Хеттінгер

12

Якщо ви знаєте, що елементи завжди доступні для використання, ви можете використовувати a, Counter()який є O (n)
Якщо ви знаєте, що елементи завжди можна сортувати, ви можете використовувати, sorted()який є O (n log n)

У загальному випадку ви не можете розраховувати на можливість сортування або на елементах, тому вам потрібен такий запасний варіант, який, на жаль, O (n ^ 2)

len(a)==len(b) and all(a.count(i)==b.count(i) for i in a)

5

Найкращий спосіб зробити це - сортування списків та порівняння їх. (Використання Counterне буде працювати з об'єктами, які не підлягають перегляду.) Це зрозуміло для цілих чисел:

sorted(a) == sorted(b)

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

sorted(a, key=id) == sorted(b, key==id)

(У Python 2.x насправді це не потрібно key= параметр, тому що ви можете порівнювати будь-який об’єкт з будь-яким об'єктом. Порядок впорядкування є довільним, але стабільним, тому він цілком працює для цієї мети; не має значення, в якому порядку розташовані об'єкти лише в тому, що впорядкування є однаковим для обох списків. Однак у Python 3 порівняння об'єктів різних типів заборонено за багатьох обставин - наприклад, ви не можете порівнювати рядки з цілими числами - тому якщо у вас будуть об'єкти різних типів, найкраще явно використовувати ідентифікатор об’єкта.)

Якщо ви хочете порівняти об’єкти у списку за значенням, з іншого боку, спочатку потрібно визначити, що означає "значення" для об'єктів. Тоді вам знадобиться якийсь спосіб забезпечити це як ключем (і для Python 3, як послідовний тип). Один з потенційних способів, який би працював для багатьох довільних об'єктів, - це сортування за їхніми ознаками repr(). Звичайно, це може витратити багато зайвого часу та repr()рядків для створення пам'яті для великих списків тощо.

sorted(a, key=repr) == sorted(b, key==repr)

Якщо об'єкти всі ваші власні типи, ви можете визначити __lt__()їх, щоб об’єкт знав, як порівнювати себе з іншими. Тоді ви можете просто сортувати їх і не турбуватися про key=параметр. Звичайно, ви також можете визначити __hash__()та використовувати Counter, що буде швидше.


4

https://docs.python.org/3.5/library/unittest.html#unittest.TestCase.assertCountEqual

assertCountEqual (перший, другий, msg = Немає)

Перевірте, що спочатку послідовність містить ті ж елементи, що і друга, незалежно від їх порядку. Якщо цього не відбувається, буде створено повідомлення про помилку, в якому перелічені відмінності між послідовностями.

Дублюючі елементи не ігноруються при порівнянні першого і другого. Він перевіряє, чи має кожен елемент однаковий підрахунок в обох послідовностях. Еквівалентно: assertEqual (Counter (список (перший)), Counter (список (другий))), але працює і з послідовностями нечутливих об'єктів.

Нове у версії 3.2.

або в 2.7: https://docs.python.org/2.7/library/unittest.html#unittest.TestCase.assertItemsEqual


2
(Що це додає до відповіді jarekwg ?)
— сіра борода

3

Якщо список містить елементи, які не доступні для перегляду (наприклад, список об'єктів), можливо, ви зможете використовувати Counter Class та функцію id (), такі як:

from collections import Counter
...
if Counter(map(id,a)) == Counter(map(id,b)):
    print("Lists a and b contain the same objects")

2

Я сподіваюся, що наведений нижче код може працювати у вашому випадку: -

if ((len(a) == len(b)) and
   (all(i in a for i in b))):
    print 'True'
else:
    print 'False'

Це забезпечить, щоб усі елементи в обох списках a& bбули однаковими, незалежно від того, в одному порядку вони чи ні.

Для кращого розуміння зверніться до моєї відповіді в цьому питанні



1

Нехай списки a, b

def ass_equal(a,b):
try:
    map(lambda x: a.pop(a.index(x)), b) # try to remove all the elements of b from a, on fail, throw exception
    if len(a) == 0: # if a is empty, means that b has removed them all
        return True 
except:
    return False # b failed to remove some items from a

Не потрібно робити їх перемішуваними або сортувати.


1
Так, але це O (n ** 2), як зазначено в кількох інших плакатах, тому їх слід використовувати лише в тому випадку, якщо інші методи не працюють. Він також передбачає aпідтримку pop(є змінною) і index(є послідовністю). Реймонд не передбачає жодного, тоді як гніблер передбачає лише послідовність.
— agf

0

Використання unittestмодуля дає чіткий та стандартний підхід.

import unittest

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