Maneira mais rápida de comparar duas listas genéricas para diferenças

214

Qual é o mais rápido (e menos intensivo em recursos) para comparar dois itens massivos (> 50.000) e, como resultado, possui duas listas como as abaixo:

  1. itens que aparecem na primeira lista, mas não na segunda
  2. itens que aparecem na segunda lista, mas não na primeira

Atualmente, estou trabalhando com a List ou IReadOnlyCollection e resolvo esse problema em uma consulta linq:

var list1 = list.Where(i => !list2.Contains(i)).ToList();
var list2 = list2.Where(i => !list.Contains(i)).ToList();

Mas isso não funciona tão bem quanto eu gostaria. Alguma idéia de tornar esse recurso mais rápido e menos intensivo, pois preciso processar muitas listas?

Frank
fonte

Respostas:

452

Use Except:

var firstNotSecond = list1.Except(list2).ToList();
var secondNotFirst = list2.Except(list1).ToList();

Eu suspeito que existem abordagens que seriam marginalmente mais rápidas do que isso, mas mesmo isso será muito mais rápido que a sua abordagem O (N * M).

Se você quiser combiná-los, poderá criar um método com o acima e, em seguida, uma declaração de retorno:

return !firstNotSecond.Any() && !secondNotFirst.Any();

Um ponto a salientar é que não é uma diferença nos resultados entre o código original na questão e a solução aqui: quaisquer elementos duplicados que são apenas em uma lista só será relatado uma vez com meu código, enquanto eles ser relatado como muitos vezes que ocorrem no código original.

Por exemplo, com listas de [1, 2, 2, 2, 3]e [1], o resultado "elementos na lista1, mas não na lista2" no código original seria [2, 2, 2, 3]. Com o meu código seria apenas [2, 3]. Em muitos casos, isso não será um problema, mas vale a pena estar ciente.

Jon Skeet
fonte
8
Este é realmente um enorme ganho de desempenho! Obrigado por esta resposta.
9763 Frank
2
Estou pensando em duas listas enormes, é útil classificar antes de comparar? ou dentro de Exceto o método de extensão, a lista passada já está classificada.
Larry
9
@ Larry: Não está classificado; cria um conjunto de hash.
quer
2
@PranavSingh: Ele funcionará para qualquer coisa que tenha a igualdade apropriada - por isso, se o seu tipo personalizado substitui Equals(object)e / ou implementa, IEquatable<T>ele deve estar bem.
Jon Skeet
2
@ k2ibegin: Ele usa o comparador de igualdade padrão, que usará uma IEquatable<T>implementação ou o object.Equals(object)método. Parece que você deve criar uma nova pergunta com um exemplo reproduzível mínimo - não podemos realmente diagnosticar coisas nos comentários.
precisa saber é o seguinte
40

Mais eficiente seria usar Enumerable.Except:

var inListButNotInList2 = list.Except(list2);
var inList2ButNotInList = list2.Except(list);

Este método é implementado usando a execução adiada. Isso significa que você pode escrever por exemplo:

var first10 = inListButNotInList2.Take(10);

Também é eficiente, pois usa internamente a Set<T>para comparar os objetos. Ele funciona coletando primeiro todos os valores distintos da segunda sequência e depois transmitindo os resultados da primeira, verificando se eles não foram vistos antes.

Tim Schmelter
fonte
1
Hmm. Não é bem diferido. Eu diria parcialmente adiado. Um completo Set<T>é construído a partir da segunda sequência (ou seja, é totalmente iterado e armazenado), e então os itens que podem ser adicionados a partir da primeira sequência são gerados.
gastador
2
@ spender, é como dizer que a execução de Whereé parcialmente adiada porque no list.Where(x => x.Id == 5)valor do número 5é armazenado no início, em vez de ser executado preguiçosamente.
GTC
27

Método Enumerable.SequenceEqual

Determina se duas seqüências são iguais de acordo com um comparador de igualdade. MS.Docs

Enumerable.SequenceEqual(list1, list2);

Isso funciona para todos os tipos de dados primitivos. Se você precisar usá-lo em objetos personalizados, precisará implementarIEqualityComparer

Define métodos para suportar a comparação de objetos para igualdade.

Interface IEqualityComparer

Define métodos para suportar a comparação de objetos para igualdade. MS.Docs para IEqualityComparer

miguelmpn
fonte
essa deve ser a resposta aceita. A questão não é sobre SETS, mas sobre LISTS, que podem conter duplicação de elementos.
Adrian Nasui 5/09/19
3
Não vejo como essa poderia ser a resposta, já que o resultado SequenceEqualé simples bool. O OP deseja duas listas de resultados - e descreve o que deseja em termos de operações definidas: "itens que aparecem na primeira lista, mas não na segunda". Não há indicação de que o pedido seja relevante, enquanto o SequenceEqual o considera relevante. Isso parece estar respondendo a uma pergunta totalmente diferente.
Jon Skeet
sim, correto, parece que eu respondi este muito rápido e não olhou para a segunda parte do pedido ... mesmo que os dois primeiros comentários ...
miguelmpn
9

Se você deseja que os resultados não diferenciam maiúsculas de minúsculas , o seguinte funcionará:

List<string> list1 = new List<string> { "a.dll", "b1.dll" };
List<string> list2 = new List<string> { "A.dll", "b2.dll" };

var firstNotSecond = list1.Except(list2, StringComparer.OrdinalIgnoreCase).ToList();
var secondNotFirst = list2.Except(list1, StringComparer.OrdinalIgnoreCase).ToList();

firstNotSecondconteria b1.dll

secondNotFirstconteria b2.dll

por exemplo
fonte
5

Não é para esse problema, mas aqui está um código para comparar listas iguais e não! objetos idênticos:

public class EquatableList<T> : List<T>, IEquatable<EquatableList<T>> where    T : IEquatable<T>

/// <summary>
/// True, if this contains element with equal property-values
/// </summary>
/// <param name="element">element of Type T</param>
/// <returns>True, if this contains element</returns>
public new Boolean Contains(T element)
{
    return this.Any(t => t.Equals(element));
}

/// <summary>
/// True, if list is equal to this
/// </summary>
/// <param name="list">list</param>
/// <returns>True, if instance equals list</returns>
public Boolean Equals(EquatableList<T> list)
{
    if (list == null) return false;
    return this.All(list.Contains) && list.All(this.Contains);
}
Eremita de Pio
fonte
1
É isso que você precisa para comparar tipos de dados personalizados. Então useExcept
Pranav Singh 8/16
Provavelmente você pode fazer melhor com tipos classificáveis. Isso é executado em O (n ^ 2), enquanto você pode fazer O (nlogn).
yuvalm2
3

tente desta maneira:

var difList = list1.Where(a => !list2.Any(a1 => a1.id == a.id))
            .Union(list2.Where(a => !list1.Any(a1 => a1.id == a.id)));
Ali Issa
fonte
13
Isso sofre um desempenho horrível, exigindo uma varredura da segunda lista para cada item da primeira. Não reduziu a votação porque funciona, mas é tão ruim quanto o código original.
gastador
3
using System.Collections.Generic;
using System.Linq;

namespace YourProject.Extensions
{
    public static class ListExtensions
    {
        public static bool SetwiseEquivalentTo<T>(this List<T> list, List<T> other)
            where T: IEquatable<T>
        {
            if (list.Except(other).Any())
                return false;
            if (other.Except(list).Any())
                return false;
            return true;
        }
    }
}

Às vezes, você só precisa saber se duas listas são diferentes, e não quais são essas diferenças. Nesse caso, considere adicionar esse método de extensão ao seu projeto. Observe que seus objetos listados devem implementar o IEquatable!

Uso:

public sealed class Car : IEquatable<Car>
{
    public Price Price { get; }
    public List<Component> Components { get; }

    ...
    public override bool Equals(object obj)
        => obj is Car other && Equals(other);

    public bool Equals(Car other)
        => Price == other.Price
            && Components.SetwiseEquivalentTo(other.Components);

    public override int GetHashCode()
        => Components.Aggregate(
            Price.GetHashCode(),
            (code, next) => code ^ next.GetHashCode()); // Bitwise XOR
}

Qualquer que seja a Componentclasse, os métodos mostrados aqui Cardevem ser implementados quase de forma idêntica.

É muito importante observar como escrevemos GetHashCode. Para implementar corretamente IEquatable, Equalse GetHashCode deve operar nas propriedades da instância de uma maneira logicamente compatível.

Duas listas com o mesmo conteúdo ainda são objetos diferentes e produzirão códigos de hash diferentes. Como queremos que essas duas listas sejam tratadas como iguais, devemos GetHashCodeproduzir o mesmo valor para cada uma delas. Podemos fazer isso delegando o código de hash a todos os elementos da lista e usando o XOR bit a bit padrão para combiná-los todos. O XOR é independente de ordem, portanto, não importa se as listas são classificadas de maneira diferente. É importante apenas que eles contenham apenas membros equivalentes.

Nota: o nome estranho é implicar o fato de que o método não considera a ordem dos elementos na lista. Se você se importa com a ordem dos elementos na lista, esse método não é para você!

Devon Parsons
fonte
1

Eu usei esse código para comparar duas listas com milhões de registros.

Este método não levará muito tempo

    //Method to compare two list of string
    private List<string> Contains(List<string> list1, List<string> list2)
    {
        List<string> result = new List<string>();

        result.AddRange(list1.Except(list2, StringComparer.OrdinalIgnoreCase));
        result.AddRange(list2.Except(list1, StringComparer.OrdinalIgnoreCase));

        return result;
    }
Sathish
fonte
0

Se apenas um resultado combinado for necessário, isso também funcionará:

var set1 = new HashSet<T>(list1);
var set2 = new HashSet<T>(list2);
var areEqual = set1.SetEquals(set2);

onde T é o tipo de elemento de listas.

Ali Khaleghi Karsalari
fonte
-1

Pode ser engraçado, mas funciona para mim

string.Join ("", List1)! = string.Join ("", List2)

Jibz
fonte
como está escrito aqui, nem funcionaria para List <string> ou List <int>, como por exemplo as duas listas 11; 2; 3 e 1; 12; 3 seriam idênticas, pois você não une as strings com alguns separador exclusivo que não é um item possível na lista. Além disso, concatenar strings para uma lista com muitos itens provavelmente é um fator que prejudica o desempenho.
precisa saber é o seguinte
@SwissCoder: Você está errado, este não é um assassino performacne para string. Se você tiver duas listas com 50.000 strings (cada uma com comprimento 3), esse algoritmo precisará de 3 ms na minha máquina. O respondere aceito precisa de 7. Acho que o truque é que Jibz precisa apenas de uma comparação de string. Claro que ele tem que adicionar um separador único.
user1027167
@ user1027167: Não estou falando sobre a comparação direta de strings (pois essa também não é a questão). Chamar o método .ToString () de todos os objetos em uma Lista com 50.000 objetos pode criar uma cadeia enorme, dependendo de como ela é implementada. Eu não acho que esse é o caminho a seguir. Além disso, também é arriscado confiar que um caractere ou sequência seja "único", o código não seria realmente reutilizável assim.
SwissCoder 22/02
Ok, isso é verdade. O interlocutor pediu o caminho mais rápido, sem fornecer o tipo de dados de suas listas. Provavelmente, essa resposta é a maneira mais rápida para o caso de uso do questionador.
user1027167
-3

Eu acho que essa é uma maneira simples e fácil de comparar duas listas elemento por elemento

x=[1,2,3,5,4,8,7,11,12,45,96,25]
y=[2,4,5,6,8,7,88,9,6,55,44,23]

tmp = []


for i in range(len(x)) and range(len(y)):
    if x[i]>y[i]:
        tmp.append(1)
    else:
        tmp.append(0)
print(tmp)
user10915707
fonte
3
Esta é uma pergunta C # e você não forneceu código C #.
Wai Ha Lee
1
Talvez você possa excluir esta resposta e movê-la para (por exemplo) Como comparar duas listas em python e retornar correspondências ?
Wai Ha Lee
-4

Esta é a melhor solução que você encontrará

var list3 = list1.Where(l => list2.ToList().Contains(l));
Fajoui El Mahdi
fonte
1
Isso é realmente muito ruim, porque cria um novo List<T>para cada elemento list1. Além disso, o resultado é chamado list3quando não é um List<T>.
Wai Ha Lee