Como comparar eficientemente duas listas não ordenadas (não conjuntos) em Python?

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

a & b deve ser considerado igual, porque eles têm exatamente os mesmos elementos, apenas em ordem diferente.

O problema é que minhas listas reais consistem em objetos (minhas instâncias de classe), não em números inteiros.

johndir
fonte
7
Como os objetos são comparados?
Marcelo Cantos
2
qual é o tamanho esperado das listas reais? As listas comparadas serão de tamanhos comparáveis ​​ou muito diferentes? Você espera que a maioria das listas corresponda ou não?
Dmitry B.
Pode-se verificar len()s primeiro.
greybeard

Respostas:

245

O (n) : O método Counter () é melhor (se seus objetos são hasháveis):

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

O (n log n) : O método classificado () é o próximo melhor (se seus objetos podem ser pedidos):

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

O (n * n) : se os objetos não forem hasháveis ​​nem ordenáveis, você poderá usar a igualdade:

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
fonte
1
Obrigado. Eu converti cada objeto em uma string e usei o método Counter ().
Johndir
Hey @ Raymond, eu recentemente encontrei essa pergunta em uma entrevista e usei sorted(), reconhecidamente não sabendo Counter. O entrevistador insistiu que havia um método mais eficiente e claramente eu deixei um espaço em branco. Após testes extensivos no python 3 com o timeitmódulo, a classificação é lançada consistentemente mais rapidamente nas listas de números inteiros. Nas listas de 1 mil itens, cerca de 1,5% mais devagar e nas listas curtas, 10 itens, 7,5% mais lentos. Pensamentos?
Arctelix #
4
Para listas curtas, a análise big-O geralmente é irrelevante porque os tempos são dominados por fatores constantes. Para as listas mais longas, suspeito que algo esteja errado com o seu benchmarking. Para 100 ints com 5 repetições cada, recebo: 127 usec para classificação e 42 para Counter (cerca de 3x mais rápido). Com 1.000 ints com 5 repetições, o contador é 4x mais rápido. 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 De fato, estamos obtendo resultados diferentes. Postei minha configuração em uma sala de chat sorted vs counter.. Estou muito curioso para saber o que está acontecendo aqui.
Arctelix #
4
Não, obrigado. Não tenho muito interesse em depurar scripts de tempo espúrios. Há muita coisa acontecendo aqui (python puro versus código C, timsort sendo aplicado a dados aleatórios vs dados semi-ordenados, diferentes detalhes de implementação entre versões, quantas duplicatas existem nos dados etc.)
Raymond Hettinger
16

Você pode classificar os dois:

sorted(a) == sorted(b)

Uma classificação de contagem também pode ser mais eficiente (mas exige que o objeto seja lavável).

>>> 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
fonte
O contador usa hash, mas os objetos não são laváveis ​​por si só. Você só precisa implementar um sensato __hash__, mas isso pode ser impossível para coleções.
Jochen Ritzel
2
ordenada não vai funcionar para tudo, quer, por exemplo, números complexossorted([0, 1j])
John La Rooy
1
Sorted () também não funciona com conjuntos nos quais os operadores de comparação foram substituídos para testes de subconjunto / superconjunto.
Raymond Hettinger
12

Se você sabe que os itens são sempre laváveis, você pode usar um Counter()que é O (n)
Se você sabe que os itens são sempre ordenáveis, você pode usar o sorted()que é O (n log n)

No caso geral, você não pode confiar em poder classificar ou possui os elementos; portanto, você precisa de um fallback como esse, que infelizmente é O (n ^ 2)

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

A melhor maneira de fazer isso é ordenando as listas e comparando-as. (O uso Counternão funcionará com objetos que não são laváveis.) Isso é simples para números inteiros:

sorted(a) == sorted(b)

Fica um pouco mais complicado com objetos arbitrários. Se você se importa com a identidade do objeto, ou seja, se os mesmos objetos estão nas duas listas, você pode usar a id()função como chave de classificação.

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

(No Python 2.x, você realmente não precisa do key=parâmetro, porque é possível comparar qualquer objeto a qualquer objeto. A ordem é arbitrária, mas estável, portanto funciona bem para esse propósito; não importa em que ordem os objetos estejam. No entanto, no Python 3, comparar objetos de tipos diferentes não é permitido em muitas circunstâncias - por exemplo, você não pode comparar cadeias de caracteres com números inteiros - portanto, se você tiver objetos de vários tipos, melhor usar explicitamente o ID do objeto.)

Se você deseja comparar os objetos da lista por valor, por outro lado, primeiro precisa definir o que "valor" significa para os objetos. Então você precisará de alguma maneira de fornecer isso como uma chave (e para o Python 3, como um tipo consistente). Uma maneira potencial que funcionaria para muitos objetos arbitrários é classificar por eles repr(). Obviamente, isso poderia desperdiçar muito tempo extra e repr()seqüências de construção de memória para listas grandes e assim por diante.

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

Se os objetos são todos do seu próprio tipo, você pode defini __lt__()-los para que o objeto saiba como se comparar a outros. Então você pode apenas classificá-los e não se preocupar com o key=parâmetro. Claro que você também pode definir __hash__()e usar Counter, o que será mais rápido.

kindall
fonte
4

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

assertCountEqual (primeiro, segundo, msg = nenhum)

Teste essa sequência primeiro contém os mesmos elementos que o segundo, independentemente de sua ordem. Quando não o fizerem, uma mensagem de erro listando as diferenças entre as seqüências será gerada.

Elementos duplicados não são ignorados ao comparar o primeiro e o segundo. Ele verifica se cada elemento tem a mesma contagem nas duas seqüências. Equivalente a: assertEqual (Contador (lista (primeiro)), Contador (lista (segundo))), mas também trabalha com seqüências de objetos laváveis.

Novo na versão 3.2.

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

cleder
fonte
2
(O que isso acrescenta à resposta de jarekwg ?) #
Greybeard
3

Se a lista contiver itens que não são laváveis ​​(como uma lista de objetos), você poderá usar a Classe de Contador e a função id (), como:

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

Espero que o trecho de código abaixo possa funcionar no seu caso: -

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

Isso irá garantir que todos os elementos em ambas as listas ae bsão os mesmos, independentemente de se eles estão em mesma ordem ou não.

Para uma melhor compreensão, consulte a minha resposta nesta pergunta

Pabitra Pati
fonte
2

Se a comparação for realizada em um contexto de teste, use assertCountEqual(a, b)( py>=3.2) e assertItemsEqual(a, b)( 2.7<=py<3.2).

Também funciona em seqüências de objetos unhashable.

jarekwg
fonte
1

Deixe as listas 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

Não há necessidade de torná-los laváveis ​​ou classificá-los.

Umur Kontacı
fonte
1
Sim, mas este é O (n ** 2), como vários outros pôsteres declararam, portanto, só deve ser usado se os outros métodos não funcionarem. Ele também assume asuportes pop(é mutável) e index(é uma sequência). O de Raymond não assume nenhum, enquanto o gnibbler assume apenas uma sequência.
agf
0

O uso do unittestmódulo oferece uma abordagem limpa e padrão.

import unittest

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