Jak efektywnie porównać dwie nieuporządkowane listy (nie zestawy) w Pythonie?

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

a i b należy uważać za równe, ponieważ mają dokładnie te same elementy, tylko w różnej kolejności.

Rzecz w tym, że moje rzeczywiste listy będą składać się z obiektów (instancji moich klas), a nie liczb całkowitych.

johndir
źródło
7
Jak porównuje się obiekty?
Marcelo Cantos
2
jaki jest oczekiwany rozmiar prawdziwych list? Czy porównywane listy będą miały porównywalne rozmiary, czy też będą bardzo różne? Czy spodziewasz się, że większość list będzie pasować, czy nie?
Dmitry B.,
Można len()najpierw sprawdzić .
siwobrody

Odpowiedzi:

245

O (n) : Metoda Counter () jest najlepsza (jeśli twoje obiekty są haszowane):

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

O (n log n) : Metoda sortowana () jest następna w kolejności (jeśli obiekty można uporządkować):

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

O (n * n) : Jeśli obiekty nie są ani hashowalne, ani uporządkowane, możesz użyć równości:

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
Raymond Hettinger
źródło
1
Dziękuję Ci. Przekonwertowałem każdy obiekt na łańcuch, a następnie użyłem metody Counter ().
johndir
Hej @Raymond, niedawno spotkałem się z tym pytaniem w wywiadzie i użyłem sorted(), wprawdzie nie wiedząc o tym Counter. Ankieter nalegał, że jest bardziej skuteczna metoda i najwyraźniej wyciągnąłem lukę. Po obszernych testach w Pythonie 3 z timeitmodułem posortowane konsekwentnie pojawiają się szybciej na listach liczb całkowitych. Na listach 1k pozycji, około 1,5% wolniej, a na krótkich listach 10 pozycji, 7,5% wolniej. Myśli?
arctelix
4
W przypadku krótkich list, analiza big-O jest zwykle nieistotna, ponieważ czasy są zdominowane przez stałe czynniki. W przypadku dłuższych list podejrzewam, że coś jest nie tak z twoim benchmarkingiem. Dla 100 int z 5 powtórzeniami każdy otrzymuję: 127 usek dla sortowania i 42 dla Counter (około 3x szybciej). Przy 1000 int z 5 powtórzeniami Counter jest 4x szybszy. 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 Hettinger
@Raymond Rzeczywiście otrzymujemy różne wyniki. Wysłałem moją konfigurację do pokoju rozmów sorted vs counter. Jestem bardzo ciekawy, co się tutaj dzieje.
arctelix
4
Nie, dziękuję. Nie interesuje mnie debugowanie fałszywych skryptów czasu. Wiele się tu dzieje (czysty kod Python kontra C, sortowanie czasu stosowane do danych losowych w porównaniu z danymi częściowo uporządkowanymi, różne szczegóły implementacji w różnych wersjach, liczba duplikatów danych itp.)
Raymond Hettinger
16

Możesz sortować zarówno:

sorted(a) == sorted(b)

Sortowanie przez zliczanie może być również bardziej wydajny (ale wymaga obiektu przeznaczonego do 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
Mark Byers
źródło
Licznik używa hashowania, ale obiekty nie są nierozłączne jako takie. Wystarczy wdrożyć rozsądne __hash__, ale w przypadku kolekcji może to być niemożliwe.
Jochen Ritzel
2
posortowane też nie zadziałają na wszystko, np. liczby zespolonesorted([0, 1j])
John La Rooy,
1
sortowana () również nie działa z zestawami, w których operatory porównania zostały nadpisane w testach podzbiorów / superzbiorów.
Raymond Hettinger
12

Jeśli wiesz, że elementy są zawsze haszowane, możesz użyć a, Counter()które jest O (n)
Jeśli wiesz, że elementy są zawsze możliwe do sortowania, możesz użyć, sorted()które jest O (n log n)

W ogólnym przypadku nie możesz polegać na możliwości sortowania lub na posiadaniu elementów, więc potrzebujesz takiego rozwiązania awaryjnego, które niestety jest O (n ^ 2)

len(a)==len(b) and all(a.count(i)==b.count(i) for i in a)
John La Rooy
źródło
5

Najlepszym sposobem na to jest sortowanie list i porównywanie ich. (Użycie Counternie zadziała z obiektami, które nie są hashowane). Jest to proste w przypadku liczb całkowitych:

sorted(a) == sorted(b)

Trochę trudniej jest z przypadkowymi obiektami. Jeśli zależy Ci na tożsamości obiektu, tj. Czy te same obiekty znajdują się na obu listach, możesz użyć id()funkcji jako klucza sortowania.

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

(W Pythonie 2.x nie potrzebujesz tego key=parametru, ponieważ możesz porównać dowolny obiekt z dowolnym obiektem. Kolejność jest dowolna, ale stabilna, więc działa dobrze w tym celu; nie ma znaczenia, w jakiej kolejności są obiekty in, tylko że kolejność jest taka sama dla obu list. Jednak w Pythonie 3 porównywanie obiektów różnych typów jest w wielu przypadkach niedozwolone - na przykład nie możesz porównywać łańcuchów z liczbami całkowitymi - więc jeśli będziesz mieć obiekty różnych typów, najlepiej jawnie użyć identyfikatora obiektu).

Z drugiej strony, jeśli chcesz porównać obiekty na liście według wartości , najpierw musisz zdefiniować, co oznacza „wartość” dla obiektów. Wtedy będziesz potrzebował jakiegoś sposobu, aby podać go jako klucz (a dla Pythona 3 jako spójny typ). Jednym z potencjalnych sposobów działania w przypadku wielu dowolnych obiektów jest sortowanie według ich repr(). Oczywiście może to zmarnować dużo dodatkowego czasu i repr()ciągów budujących pamięć dla dużych list i tak dalej.

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

Jeśli wszystkie obiekty są twoimi własnymi typami, możesz __lt__()na nich zdefiniować, aby obiekt wiedział, jak porównywać się z innymi. Następnie możesz je po prostu posortować i nie martwić się o key=parametr. Oczywiście możesz także zdefiniować __hash__()i użyć Counter, co będzie szybsze.

kindall
źródło
4

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

assertCountEqual (pierwszy, drugi, msg = brak)

Przetestuj, że sekwencja najpierw zawiera te same elementy, co druga, niezależnie od ich kolejności. Jeśli tak nie jest, zostanie wygenerowany komunikat o błędzie zawierający różnice między sekwencjami.

Powielone elementy nie są ignorowane podczas porównywania pierwszego i drugiego. Sprawdza, czy każdy element ma taką samą liczbę w obu sekwencjach. Równoważne z: assertEqual (Counter (lista (pierwsza)), Counter (lista (druga))), ale działa również z sekwencjami obiektów, których nie można zhashować.

Nowość w wersji 3.2.

lub w wersji 2.7: https://docs.python.org/2.7/library/unittest.html#unittest.TestCase.assertItemsEqual

cleder
źródło
2
(Co to dodaje do odpowiedzi Jarekwga ?)
Greybeard
3

Jeśli lista zawiera elementy, których nie można haszować (na przykład listę obiektów), możesz użyć klasy Counter i funkcji id (), takiej jak:

from collections import Counter
...
if Counter(map(id,a)) == Counter(map(id,b)):
    print("Lists a and b contain the same objects")
Mars
źródło
2

Mam nadzieję, że poniższy fragment kodu może działać w twoim przypadku: -

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

Zapewni to, że wszystkie elementy na obu listach aib są takie same, niezależnie od tego, czy są one w tej samej kolejności, czy nie.

Dla lepszego zrozumienia zapoznaj się z moją odpowiedzią na to pytanie

Pabitra Pati
źródło
2

Jeśli porównanie ma być przeprowadzone w kontekście testowym, użyj assertCountEqual(a, b)( py>=3.2) i assertItemsEqual(a, b)(2.7<=py<3.2 ).

Działa również na sekwencjach obiektów, których nie można zhashować.

jarekwg
źródło
1

Niech a, b wymienia

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

Nie ma potrzeby ich haszowania ani sortowania.

Umur Kontacı
źródło
1
Tak, ale to jest O (n ** 2), jak wskazało kilka innych plakatów, więc powinno być używane tylko wtedy, gdy inne metody nie działają. Zakłada również, że aobsługuje pop(jest zmienny) i index(jest sekwencją). Raymond's nie zakłada żadnego, podczas gdy Gnibbler zakłada tylko sekwencję.
agf
0

Korzystanie z unittestmodułu zapewnia przejrzyste i standardowe podejście.

import unittest

test_object = unittest.TestCase()
test_object.assertCountEqual(a, b)
Meysam Sadeghi
źródło