Come confrontare in modo efficiente due elenchi non ordinati (non set) in Python?


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

a & b devono essere considerati uguali, perché hanno esattamente gli stessi elementi, solo in ordine diverso.

Il fatto è che i miei elenchi effettivi saranno costituiti da oggetti (le mie istanze di classe), non da numeri interi.


7
Come vengono confrontati gli oggetti?
Marcelo Cantos,

2
qual è la dimensione prevista delle liste reali? Le liste confrontate avranno dimensioni comparabili o molto diverse? Ti aspetti che la maggior parte degli elenchi corrisponda o no?
Dmitry B.

Si potrebbe verificare len()prima s.
Greybeard,

Risposte:


245

O (n) : il metodo Counter () è il migliore (se i tuoi oggetti sono cancellabili):

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

O (n log n) : il metodo sort () è il migliore (se i tuoi oggetti sono ordinabili):

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

O (n * n) : se gli oggetti non sono né hash, né ordinabili, puoi usare l'uguaglianza:

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
Grazie. Ho convertito ogni oggetto in una stringa, quindi ho usato il metodo Counter ().
johndir,

Ehi @Raymond, di recente ho incontrato questa domanda in un'intervista e l'ho usato sorted(), devo ammettere che non lo sapevo Counter. L'intervistatore ha insistito sul fatto che esistesse un metodo più efficiente e chiaramente ho disegnato un vuoto. Dopo test approfonditi in python 3 con il timeitmodulo, l'ordinamento risulta costantemente più veloce sugli elenchi di numeri interi. Sugli elenchi di 1k articoli, circa l'1,5% più lento e sugli elenchi brevi, 10 articoli, il 7,5% più lento. Pensieri?
arctelix,

4
Per gli elenchi brevi, l'analisi big-O è generalmente irrilevante perché i tempi sono dominati da fattori costanti. Per gli elenchi più lunghi, sospetto che qualcosa non vada nel tuo benchmarking. Per 100 pollici con 5 ripetizioni ciascuno, ottengo: 127 usec per l'ordinamento e 42 per il contatore (circa 3 volte più veloce). A 1.000 ints con 5 ripetizioni, Counter è 4 volte più veloce. 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 In effetti stiamo ottenendo risultati diversi. Ho pubblicato la mia configurazione in una chat room sorted vs counter.. Sono molto curioso di sapere cosa sta succedendo qui.
arctelix,

4
No grazie. Non mi interessa molto il debug degli script di timing spuri. C'è molto da fare qui (codice Python vs C puro, timsort applicato a dati randomizzati vs dati semi-ordinati, diversi dettagli di implementazione tra le versioni, quanti duplicati ci sono nei dati, ecc.)
Raymond Hettinger,

16

Puoi ordinare entrambi:

sorted(a) == sorted(b)

UN ordinamento di conteggio potrebbe anche essere più efficiente (ma richiede che l'oggetto sia hash).

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

Il contatore usa l'hashing, ma gli oggetti non sono di per sé impercettibili. Devi solo implementare un ragionevole __hash__, ma potrebbe essere impossibile per le collezioni.
Jochen Ritzel,

2
ordinati non funzionerà neanche per tutto, ad esempio numeri complessisorted([0, 1j])
John La Rooy,

1
sort () inoltre non funziona con set in cui gli operatori di confronto sono stati sovrascritti per i test del sottoinsieme / superset.
Raymond Hettinger,

12

Se sai che gli oggetti sono sempre hash, puoi usare un Counter()che è O (n)
Se sai che gli oggetti sono sempre ordinabili, puoi usare sorted()che è O (n log n)

Nel caso generale non puoi fare affidamento sulla possibilità di ordinare o avere gli elementi, quindi hai bisogno di un fallback come questo, che purtroppo è O (n ^ 2)

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

5

Il modo migliore per farlo è ordinare gli elenchi e confrontarli. (L'uso Counternon funzionerà con oggetti non hash.) Questo è semplice per gli interi:

sorted(a) == sorted(b)

Diventa un po 'più complicato con oggetti arbitrari. Se ti interessa l'identità dell'oggetto, ovvero se gli stessi oggetti sono presenti in entrambi gli elenchi, puoi utilizzare la id()funzione come chiave di ordinamento.

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

(In Python 2.x non è effettivamente necessario key= parametro, perché puoi confrontare qualsiasi oggetto con qualsiasi oggetto. L'ordinamento è arbitrario ma stabile, quindi funziona bene per questo scopo; non importa quale ordine siano gli oggetti in, solo che l'ordinamento è lo stesso per entrambi gli elenchi. In Python 3, tuttavia, il confronto di oggetti di tipi diversi è vietato in molte circostanze - ad esempio, non è possibile confrontare stringhe con numeri interi - quindi se si avranno oggetti di vari tipi, meglio usare esplicitamente l'ID dell'oggetto.)

Se si desidera confrontare gli oggetti nell'elenco per valore, d'altra parte, è necessario innanzitutto definire cosa significa "valore" per gli oggetti. Quindi avrai bisogno di un modo per fornirlo come chiave (e per Python 3, come tipo coerente). Un modo potenziale che funzionerebbe per molti oggetti arbitrari è quello di ordinare in base al loro repr(). Ovviamente, ciò potrebbe far perdere molto tempo in più e repr()stringhe di costruzione della memoria per elenchi di grandi dimensioni e così via.

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

Se gli oggetti sono tutti i tuoi tipi, puoi definirli __lt__()in modo che l'oggetto sappia come confrontarsi con gli altri. Quindi puoi semplicemente ordinarli e non preoccuparti del key=parametro. Ovviamente potresti anche definire __hash__()e usareCounter , che sarà più veloce.


4

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

assertCountEqual (primo, secondo, msg = nessuno)

Prova prima che la sequenza contenga gli stessi elementi del secondo, indipendentemente dal loro ordine. In caso contrario, verrà generato un messaggio di errore che elenca le differenze tra le sequenze.

Gli elementi duplicati non vengono ignorati quando si confrontano il primo e il secondo. Verifica se ogni elemento ha lo stesso conteggio in entrambe le sequenze. Equivalente a: assertEqual (Counter (list (first)), Counter (list (second))) ma funziona anche con sequenze di oggetti non lavabili.

Novità nella versione 3.2.

o in 2.7: https://docs.python.org/2.7/library/unittest.html#unittest.TestCase.assertItemsEqual


2
(Cosa aggiunge questo alla risposta di jarekwg ?)
barbarossa

3

Se l'elenco contiene elementi che non sono cancellabili (come un elenco di oggetti), potresti essere in grado di utilizzare la classe Counter e la funzione id () come:

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

2

Spero che il codice seguente possa funzionare nel tuo caso: -

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

Ciò garantirà che tutti gli elementi in entrambi gli elenchi ae bsiano uguali, indipendentemente dal fatto che siano nello stesso ordine o meno.

Per una migliore comprensione, fare riferimento alla mia risposta in questa domanda



1

Consenti alle liste 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

Non c'è bisogno di renderli hashing o ordinarli.


1
Sì, ma questo è O (n ** 2) come indicato da molti altri poster, quindi dovrebbe essere usato solo se gli altri metodi non funzionano. Presuppone anche i asupporti pop(è mutabile) e index(è una sequenza). Raymond non ne assume né mentre Gnibbler assume solo una sequenza.
AGF

0

L'uso del unittestmodulo offre un approccio pulito e standard.

import unittest

test_object = unittest.TestCase()
test_object.assertCountEqual(a, b)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.