Comment comparer efficacement deux listes non ordonnées (et non des ensembles) en Python?

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

a et b doivent être considérés comme égaux, car ils ont exactement les mêmes éléments, mais dans un ordre différent.

Le fait est que mes listes réelles seront constituées d'objets (mes instances de classe), pas d'entiers.

johndir
la source
7
Comment les objets sont-ils comparés?
Marcelo Cantos
2
quelle est la taille attendue des vraies listes? Les listes comparées seront-elles de tailles comparables ou très différentes? Vous attendez-vous à ce que la plupart des listes correspondent ou non?
Dmitry B.
On pourrait d'abord vérifier len()s.
greybeard

Réponses:

245

O (n) : La méthode Counter () est la meilleure (si vos objets sont hachables):

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

O (n log n) : La méthode sorted () est la meilleure suivante (si vos objets peuvent être commandés):

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

O (n * n) : Si les objets ne sont ni hachables, ni triables, vous pouvez utiliser l'égalité:

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
la source
1
Je vous remercie. J'ai converti chaque objet en une chaîne puis utilisé la méthode Counter ().
johndir
Hey @Raymond, j'ai récemment rencontré cette question lors d'une interview et j'ai utilisé sorted(), certes ne sachant rien Counter. L'intervieweur a insisté sur le fait qu'il y avait une méthode plus efficace et j'ai clairement dessiné un blanc. Après des tests approfondis en python 3 avec le timeitmodule, trié sort systématiquement plus rapidement sur les listes d'entiers. Sur les listes de 1k éléments, environ 1,5% plus lent et sur les listes courtes, 10 éléments, 7,5% plus lent. Pensées?
arctelix
4
Pour les listes courtes, l'analyse big-O n'est généralement pas pertinente car les délais sont dominés par des facteurs constants. Pour les listes plus longues, je soupçonne que quelque chose ne va pas avec votre analyse comparative. Pour 100 ints avec 5 répétitions chacun, j'obtiens: 127 usec pour trié et 42 pour compteur (environ 3x plus rapide). À 1000 pouces avec 5 répétitions, le compteur est 4x plus rapide. 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 En effet, nous obtenons des résultats différents. J'ai posté ma configuration dans une salle de chat sorted vs counter. Je suis très curieux de savoir ce qui se passe ici.
arctelix
4
Non merci. Je ne suis pas très intéressé par le débogage de faux scripts de chronométrage. Il se passe beaucoup de choses ici (code python pur vs code C, tri temporel appliqué aux données aléatoires vs données semi-ordonnées, détails d'implémentation différents selon les versions, combien de doublons sont dans les données, etc.)
Raymond Hettinger
16

Vous pouvez trier les deux:

sorted(a) == sorted(b)

Un tri par comptage pourrait également être plus efficace (mais il nécessite que l'objet soit hachable).

>>> 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
la source
Le compteur utilise le hachage, mais les objets ne sont pas indéchiffrables en soi. Vous avez juste à implémenter un sens __hash__, mais cela pourrait être impossible pour les collections.
Jochen Ritzel
2
trié ne fonctionnera pas non plus pour tout, par exemple les nombres complexessorted([0, 1j])
John La Rooy
1
sorted () ne fonctionne pas non plus avec les ensembles où les opérateurs de comparaison ont été remplacés pour les tests de sous-ensemble / sur-ensemble.
Raymond Hettinger
12

Si vous savez que les éléments sont toujours hachables, vous pouvez utiliser a Counter()qui est O (n)
Si vous savez que les éléments sont toujours triables, vous pouvez utiliser sorted()qui est O (n log n)

Dans le cas général, vous ne pouvez pas compter sur la capacité de trier ou d'avoir les éléments, vous avez donc besoin d'une solution de secours comme celle-ci, qui est malheureusement O (n ^ 2)

len(a)==len(b) and all(a.count(i)==b.count(i) for i in a)
John La Rooy
la source
5

La meilleure façon d'y parvenir est de trier les listes et de les comparer. (L'utilisation Counterne fonctionnera pas avec des objets qui ne sont pas hachables.) C'est simple pour les entiers:

sorted(a) == sorted(b)

Cela devient un peu plus délicat avec des objets arbitraires. Si vous vous souciez de l'identité des objets, c'est-à-dire si les mêmes objets sont dans les deux listes, vous pouvez utiliser la id()fonction comme clé de tri.

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

(En Python 2.x, vous n'avez pas réellement besoin du key=paramètre, car vous pouvez comparer n'importe quel objet à n'importe quel objet. L'ordre est arbitraire mais stable, donc cela fonctionne bien à cette fin; peu importe l'ordre dans lequel les objets sont dans, seulement que l'ordre est le même pour les deux listes. En Python 3, cependant, la comparaison d'objets de types différents est interdite dans de nombreuses circonstances - par exemple, vous ne pouvez pas comparer des chaînes à des entiers - donc si vous avez des objets de différents types, il est préférable d'utiliser explicitement l'ID de l'objet.)

Si vous souhaitez comparer les objets de la liste par valeur, par contre, vous devez d'abord définir ce que signifie «valeur» pour les objets. Ensuite, vous aurez besoin d'un moyen de fournir cela en tant que clé (et pour Python 3, en tant que type cohérent). Un moyen potentiel qui fonctionnerait pour de nombreux objets arbitraires est de trier par leur repr(). Bien sûr, cela pourrait perdre beaucoup de temps supplémentaire et de créer des repr()chaînes de mémoire pour les grandes listes, etc.

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

Si les objets sont tous de vos propres types, vous pouvez les définir __lt__()pour que l'objet sache se comparer aux autres. Ensuite, vous pouvez simplement les trier et ne pas vous soucier du key=paramètre. Bien sûr, vous pouvez également définir __hash__()et utiliser Counter, ce qui sera plus rapide.

kindall
la source
4

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

assertCountEqual (premier, deuxième, msg = Aucun)

Testez que la séquence contient d'abord les mêmes éléments que la seconde, quel que soit leur ordre. Si ce n'est pas le cas, un message d'erreur répertoriant les différences entre les séquences sera généré.

Les éléments en double ne sont pas ignorés lors de la comparaison des premier et second. Il vérifie si chaque élément a le même nombre dans les deux séquences. Équivaut à: assertEqual (Counter (list (first)), Counter (list (second))) mais fonctionne également avec des séquences d'objets non détachables.

Nouveau dans la version 3.2.

ou en 2.7: https://docs.python.org/2.7/library/unittest.html#unittest.TestCase.assertItemsEqual

cleder
la source
2
(Qu'est-ce que cela ajoute à la réponse de jarekwg ?)
greybeard
3

Si la liste contient des éléments qui ne sont pas hachables (comme une liste d'objets), vous pourrez peut-être utiliser la classe de compteur et la fonction id () comme:

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

J'espère que le morceau de code ci-dessous pourrait fonctionner dans votre cas: -

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

Cela garantira que tous les éléments des deux listes a& bsont identiques, qu'ils soient dans le même ordre ou non.

Pour une meilleure compréhension, reportez-vous à ma réponse à cette question

Pabitra Pati
la source
2

Si la comparaison doit être effectuée dans un contexte de test, utilisez assertCountEqual(a, b)( py>=3.2) et assertItemsEqual(a, b)( 2.7<=py<3.2).

Fonctionne également sur des séquences d'objets non détachables.

jarekwg
la source
1

Soit a, b listes

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

Pas besoin de les rendre hachables ou de les trier.

Umur Kontacı
la source
1
Oui, mais c'est O (n ** 2) comme l'ont indiqué plusieurs autres affiches, il ne devrait donc être utilisé que si les autres méthodes ne fonctionnent pas. Il suppose également des asupports pop(est mutable) et index(est une séquence). Raymond ne suppose ni l'un ni l'autre tandis que gnibbler n'assume qu'une séquence.
agf
0

L'utilisation du unittestmodule vous donne une approche claire et standard.

import unittest

test_object = unittest.TestCase()
test_object.assertCountEqual(a, b)
Meysam Sadeghi
la source