Teste se as listas compartilham itens em python


131

Quero verificar se algum dos itens de uma lista está presente em outra lista. Posso fazer isso simplesmente com o código abaixo, mas suspeito que possa haver uma função de biblioteca para fazer isso. Caso contrário, existe um método mais pitônico de alcançar o mesmo resultado.

In [78]: a = [1, 2, 3, 4, 5]

In [79]: b = [8, 7, 6]

In [80]: c = [8, 7, 6, 5]

In [81]: def lists_overlap(a, b):
   ....:     for i in a:
   ....:         if i in b:
   ....:             return True
   ....:     return False
   ....: 

In [82]: lists_overlap(a, b)
Out[82]: False

In [83]: lists_overlap(a, c)
Out[83]: True

In [84]: def lists_overlap2(a, b):
   ....:     return len(set(a).intersection(set(b))) > 0
   ....: 

As únicas otimizações em que consigo pensar estão caindo len(...) > 0porque bool(set([]))produz False. E, é claro, se você mantivesse suas listas como conjuntos, você economizaria sobrecarga na criação de conjuntos.
msw


1
Observe que você não pode diferenciar Truede 1e Falsepara 0. not set([1]).isdisjoint([True])fica True, o mesmo com outras soluções.
Dimali

Respostas:


313

Resposta curta : use not set(a).isdisjoint(b), geralmente é o mais rápido.

Existem quatro maneiras comuns de testar se há duas listas ae bcompartilhar itens. A primeira opção é converter ambos em conjuntos e verificar sua interseção, como tal:

bool(set(a) & set(b))

Como os conjuntos são armazenados usando uma tabela de hash no Python, é possível pesquisá-losO(1) (consulte aqui para obter mais informações sobre a complexidade dos operadores no Python). Teoricamente, isso é, O(n+m)em média, para ne mobjetos nas listas ae b. Mas 1) ele deve primeiro criar conjuntos das listas, o que pode levar um tempo não negligenciável, e 2) supõe que as colisões de hash sejam escassas entre seus dados.

A segunda maneira de fazer isso é usar uma expressão de gerador executando iteração nas listas, como:

any(i in a for i in b)

Isso permite pesquisar no local, para que nenhuma nova memória seja alocada para variáveis ​​intermediárias. Também se destaca na primeira descoberta. Mas o inoperador está sempre O(n)nas listas (veja aqui ).

Outra opção proposta é um híbrido para iterar através de uma das listas, converter a outra em um conjunto e testar a associação nesse conjunto, da seguinte forma:

a = set(a); any(i in a for i in b)

Uma quarta abordagem é aproveitar o isdisjoint()método dos conjuntos (congelados) (veja aqui ), por exemplo:

not set(a).isdisjoint(b)

Se os elementos pesquisados ​​estiverem perto do início de uma matriz (por exemplo, ela é classificada), a expressão do gerador é favorecida, pois o método de interseção de conjuntos precisa alocar nova memória para as variáveis ​​intermediárias:

from timeit import timeit
>>> timeit('bool(set(a) & set(b))', setup="a=list(range(1000));b=list(range(1000))", number=100000)
26.077727576019242
>>> timeit('any(i in a for i in b)', setup="a=list(range(1000));b=list(range(1000))", number=100000)
0.16220548999262974

Aqui está um gráfico do tempo de execução para este exemplo em função do tamanho da lista:

Tempo de execução do teste de compartilhamento de elemento quando compartilhado no início

Observe que os dois eixos são logarítmicos. Isso representa o melhor caso para a expressão do gerador. Como pode ser visto, o isdisjoint()método é melhor para tamanhos de lista muito pequenos, enquanto a expressão do gerador é melhor para tamanhos de lista maiores.

Por outro lado, como a pesquisa começa com o início da expressão híbrida e geradora, se o elemento compartilhado estiver sistematicamente no final da matriz (ou as duas listas não compartilharem nenhum valor), as abordagens de interseção separada e definida serão então muito mais rápido que a expressão do gerador e a abordagem híbrida.

>>> timeit('any(i in a for i in b)', setup="a=list(range(1000));b=[x+998 for x in range(999,0,-1)]", number=1000))
13.739536046981812
>>> timeit('bool(set(a) & set(b))', setup="a=list(range(1000));b=[x+998 for x in range(999,0,-1)]", number=1000))
0.08102107048034668

Tempo de execução do teste de compartilhamento de elemento quando compartilhado no final

É interessante notar que a expressão do gerador é muito mais lenta para tamanhos de lista maiores. Isso é apenas para 1000 repetições, em vez de 100000 para a figura anterior. Essa configuração também se aproxima bem quando quando nenhum elemento é compartilhado e é o melhor caso para as abordagens de interseção separada e definida.

Aqui estão duas análises usando números aleatórios (em vez de manipular a configuração para favorecer uma técnica ou outra):

Tempo de execução do teste de compartilhamento de elementos para dados gerados aleatoriamente com alta chance de compartilhamento Tempo de execução do teste de compartilhamento de elementos para dados gerados aleatoriamente com alta chance de compartilhamento

Grande chance de compartilhamento: os elementos são retirados aleatoriamente [1, 2*len(a)]. Baixa chance de compartilhamento: os elementos são retirados aleatoriamente [1, 1000*len(a)].

Até agora, essa análise supunha que as duas listas eram do mesmo tamanho. No caso de duas listas de tamanhos diferentes, por exemplo, aé muito menor, isdisjoint()é sempre mais rápido:

Tempo de execução do teste de compartilhamento de elemento em duas listas de tamanhos diferentes quando compartilhadas no início Tempo de execução do teste de compartilhamento de elemento em duas listas de tamanhos diferentes quando compartilhadas no final

Verifique se a alista é menor, caso contrário, o desempenho diminui. Nesta experiência, o atamanho da lista foi definido como constante 5.

Em suma:

  • Se as listas são muito pequenas (<10 elementos), not set(a).isdisjoint(b)é sempre a mais rápida.
  • Se os elementos nas listas forem classificados ou tiverem uma estrutura regular da qual você possa tirar proveito, a expressão do gerador any(i in a for i in b)será a mais rápida em tamanhos de lista grandes;
  • Teste a interseção definida com not set(a).isdisjoint(b), que é sempre mais rápida que bool(set(a) & set(b)).
  • O híbrido "iterar através da lista, testar no set" a = set(a); any(i in a for i in b)é geralmente mais lento que outros métodos.
  • A expressão do gerador e o híbrido são muito mais lentos do que as outras duas abordagens quando se trata de listas sem compartilhar elementos.

Na maioria dos casos, o uso do isdisjoint()método é a melhor abordagem, pois a expressão do gerador levará muito mais tempo para ser executada, pois é muito ineficiente quando nenhum elemento é compartilhado.


8
Esses são alguns dados úteis, mostram que a análise do grande O não é o princípio e o fim de todo o raciocínio sobre tempo de execução.
22813 Steve Allison

e o pior cenário? anysai no primeiro valor não falso. Usando uma lista em que o único valor correspondente está no final, obtemos o seguinte: timeit('any(i in a for i in b)', setup="a=list(range(1000));b=[x+998 for x in range(999,-0,-1)]", number=1000) 13.739536046981812 timeit('bool(set(a) & set(b))', setup="a=list(range(1000));b=[x+998 for x in range(999,-0,-1)]", number=1000) 0.08102107048034668 ... e é apenas com 1000 iterações.
RobM 28/09/2015

2
Obrigado @RobM pela informação. Atualizei minha resposta para refletir isso e levar em conta as outras técnicas propostas neste tópico.
Soravux 31/01

Deve ser not set(a).isdisjoint(b)para testar se duas listas compartilham um membro. set(a).isdisjoint(b)retorna Truese as duas listas não compartilham um membro. A resposta deve ser editada?
Guillochon

1
Obrigado pelo aviso, @Guillochon, está consertado.
Soravux 15/02

25
def lists_overlap3(a, b):
    return bool(set(a) & set(b))

Nota: o acima pressupõe que você deseja um booleano como resposta. Se tudo o que você precisa é de uma expressão para usar em uma ifinstrução, useif set(a) & set(b):


5
Este é o pior caso O (n + m). No entanto, o lado negativo é que ele cria um novo conjunto e não sai quando um elemento comum é encontrado cedo.
Matthew Flaschen

1
Estou curioso para saber por que isso é O(n + m). Meu palpite seria que os conjuntos são implementados usando tabelas de hash e, portanto, o inoperador pode trabalhar no O(1)tempo (exceto em casos degenerados). Isso está correto? Em caso afirmativo, considerando que as tabelas de hash têm desempenho de pesquisa de pior caso O(n), isso significa que, no pior caso, ele terá O(n * m)desempenho?
fmark

1
@mark: Teoricamente, você está certo. Praticamente, ninguém se importa; leia os comentários em Objects / dictobject.c na fonte CPython (os conjuntos são apenas dicts com apenas chaves, sem valores) e veja se você pode gerar uma lista de chaves que causarão o desempenho da pesquisa de O (n).
John Knin

Ok, obrigado por esclarecer, eu queria saber se havia alguma mágica acontecendo :). Embora eu concorde que praticamente não preciso me importar, é trivial gerar uma lista de chaves que causarão O(n)desempenho na pesquisa;), consulte pastebin.com/Kn3kAW7u Apenas para lafs.
fmark

2
Sim, eu sei. Além disso, acabei de ler a fonte para a qual você me indicou, que documenta ainda mais mágica no caso de funções hash não aleatórias (como a embutida). Supus que fosse necessário aleatoriedade, como a Java, que resulta em monstruosidades como esta stackoverflow.com/questions/2634690/… . Eu preciso me lembrar que Python não é Java (graças a Deus!).
fmark

10
def lists_overlap(a, b):
  sb = set(b)
  return any(el in sb for el in a)

Isso é assintoticamente ideal (pior caso O (n + m)) e pode ser melhor que a abordagem de interseção devido ao anycurto-circuito do circuito.

Por exemplo:

lists_overlap([3,4,5], [1,2,3])

retornará True assim que chegar 3 in sb

EDIT: Outra variação (com agradecimentos a Dave Kirby):

def lists_overlap(a, b):
  sb = set(b)
  return any(itertools.imap(sb.__contains__, a))

Isso depende imapdo iterador, que é implementado em C, em vez de uma compreensão do gerador. Ele também usa sb.__contains__como a função de mapeamento. Não sei quanta diferença de desempenho isso faz. Ainda irá causar um curto-circuito.


1
Os loops na abordagem de interseção estão todos no código C; há um loop em sua abordagem que inclui código Python. O grande desconhecido é se uma interseção vazia é provável ou improvável.
John Machin

2
Você também pode usar o any(itertools.imap(sb.__contains__, a))que deve ser mais rápido ainda, pois evita o uso de uma função lambda.
Dave Kirby

Obrigado, @Dave. :) Concordo que remover o lambda é uma vitória.
Matthew Flaschen

4

Você também pode usar anycom compreensão de lista:

any([item in a for item in b])

6
Você poderia, mas o tempo é O (n * m), enquanto o tempo para a abordagem de interseção definida é O (n + m). Você também poderia fazê-lo SEM entender a lista (perder a []) e ela seria executada mais rapidamente e consumiria menos memória, mas o tempo ainda seria O (n * m).
John Machin

1
Embora sua grande análise de O seja verdadeira, suspeito que, para pequenos valores de nm, o tempo que leva para construir as hashtables subjacentes entre em jogo. Big O ignora o tempo necessário para calcular os hashes.
Anthony Conyers

2
Construir uma "hashtable" é amortizado O (n).
John Machin

1
Eu entendo isso, mas a constante que você está jogando fora é muito grande. Não importa para valores grandes de n, mas para valores pequenos.
Anthony Conyers

3

No python 2.6 ou posterior, você pode fazer:

return not frozenset(a).isdisjoint(frozenset(b))

1
Parece que não é necessário fornecer um conjunto ou frozenset como o primeiro argumento. Eu tentei com uma string e funcionou (ou seja: qualquer iterável serve).
Aktau

2

Você pode usar qualquer expressão interna da função / gerador de wa:

def list_overlap(a,b): 
     return any(i for i in a if i in b)

Como John e Lie apontaram, isso fornece resultados incorretos quando, para cada i compartilhado pelas duas listas, bool (i) == False. Deveria ser:

return any(i in b for i in a)

1
Comentário ampliado de Lie Ryan: dará resultado errado para qualquer item x que esteja no cruzamento onde bool(x)está False. No exemplo de Lie Ryan, x é 0. Somente a correção é a any(True for i in a if i in b)que é melhor escrita como a já vista any(i in b for i in a).
John Machin

1
Correção: vai dar resultado errado quando todos os itens xna interseção são tais que bool(x)é False.
John Machin

1

Essa pergunta é bastante antiga, mas notei que, enquanto as pessoas discutiam conjuntos versus listas, ninguém pensava em usá-las juntas. Seguindo o exemplo de Soravux,

Pior caso para listas:

>>> timeit('bool(set(a) & set(b))',  setup="a=list(range(10000)); b=[x+9999 for x in range(10000)]", number=100000)
100.91506409645081
>>> timeit('any(i in a for i in b)', setup="a=list(range(10000)); b=[x+9999 for x in range(10000)]", number=100000)
19.746716022491455
>>> timeit('any(i in a for i in b)', setup="a= set(range(10000)); b=[x+9999 for x in range(10000)]", number=100000)
0.092626094818115234

E o melhor caso para listas:

>>> timeit('bool(set(a) & set(b))',  setup="a=list(range(10000)); b=list(range(10000))", number=100000)
154.69790101051331
>>> timeit('any(i in a for i in b)', setup="a=list(range(10000)); b=list(range(10000))", number=100000)
0.082653045654296875
>>> timeit('any(i in a for i in b)', setup="a= set(range(10000)); b=list(range(10000))", number=100000)
0.08434605598449707

Portanto, ainda mais rápido do que a iteração nas duas listas é iterativo na lista para ver se ele está em um conjunto, o que faz sentido, pois verificar se um número está em um conjunto leva tempo constante, enquanto que a verificação pela iteração na lista leva um tempo proporcional ao tamanho de a lista.

Assim, minha conclusão é que iteramos através de uma lista e verificamos se está em um conjunto .


1
Usar o isdisjoint()método em um conjunto (congelado), conforme indicado por @Toughy, é ainda melhor: timeit('any(i in a for i in b)', setup="a= set(range(10000)); b=[x+9999 for x in range(10000)]", number=100000)=> 0.00913715362548828
Aktau

1

se você não se importa com o que o elemento sobreposto pode ser, basta verificar a lenlista combinada e a lista combinada como um conjunto. Se houver elementos sobrepostos, o conjunto será mais curto:

len(set(a+b+c))==len(a+b+c) retorna True, se não houver sobreposição.


Se o primeiro valor se sobrepuser, ele ainda converterá a lista inteira em um conjunto, não importa o tamanho.
Peter Wood

1

Vou lançar outro com um estilo de programação funcional:

any(map(lambda x: x in a, b))

Explicação:

map(lambda x: x in a, b)

retorna uma lista de booleanos em que elementos bsão encontrados a. Essa lista é então passada para any, que simplesmente retorna Truese houver algum elemento True.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.