Confronto tra due raccolte per uguaglianza indipendentemente dall'ordine degli oggetti in esse contenuti


162

Vorrei confrontare due raccolte (in C #), ma non sono sicuro del modo migliore per implementarlo in modo efficiente.

Ho letto l'altro thread su Enumerable.SequenceEqual , ma non è esattamente quello che sto cercando.

Nel mio caso, due raccolte sarebbero uguali se contengano entrambe gli stessi articoli (indipendentemente dall'ordine).

Esempio:

collection1 = {1, 2, 3, 4};
collection2 = {2, 4, 1, 3};

collection1 == collection2; // true

Quello che faccio di solito è scorrere attraverso ogni elemento di una raccolta e vedere se esiste nell'altra raccolta, quindi scorrere attraverso ogni elemento dell'altra raccolta e vedere se esiste nella prima raccolta. (Comincio confrontando le lunghezze).

if (collection1.Count != collection2.Count)
    return false; // the collections are not equal

foreach (Item item in collection1)
{
    if (!collection2.Contains(item))
        return false; // the collections are not equal
}

foreach (Item item in collection2)
{
    if (!collection1.Contains(item))
        return false; // the collections are not equal
}

return true; // the collections are equal

Tuttavia, questo non è del tutto corretto e probabilmente non è il modo più efficiente per confrontare due raccolte per l'uguaglianza.

Un esempio che posso pensare che sarebbe sbagliato è:

collection1 = {1, 2, 3, 3, 4}
collection2 = {1, 2, 2, 3, 4}

Il che sarebbe uguale alla mia implementazione. Devo solo contare il numero di volte in cui viene trovato ciascun elemento e accertarmi che i conteggi siano uguali in entrambe le raccolte?


Gli esempi sono in una sorta di C # (chiamiamolo pseudo-C #), ma diamo la tua risposta in qualunque lingua tu voglia, non importa.

Nota: ho usato numeri interi negli esempi per semplicità, ma voglio essere in grado di usare anche oggetti di tipo riferimento (non si comportano correttamente come chiavi perché viene confrontato solo il riferimento dell'oggetto, non il contenuto).


1
Che ne dici di algoritmo? Tutte le risposte correlate da confronta qualcosa, liste generiche confrontano linq ecc. Davvero abbiamo promesso a qualcuno che non useremo mai l'algoritmo come programmatore vecchio stile?
Nuri YILMAZ,

Non stai verificando l'uguaglianza, stai controllando l'equivalenza. È nitido ma una distinzione importante. E molto tempo fa. Questo è un buon Q + A.
Bloke CAD,

Potresti essere interessato a questo post , che discute una versione sintonizzata del metodo basato sul dizionario descritto di seguito. Un problema con la maggior parte dei semplici approcci di dizionario è che non gestiscono correttamente i null perché la classe Dictionary di .NET non consente le chiavi null.
ChaseMedallion,

Risposte:


112

Si scopre che Microsoft ha già questo coperto nel suo framework di test: CollectionAssert.AreEquivalent

Osservazioni

Due collezioni sono equivalenti se hanno gli stessi elementi nella stessa quantità, ma in qualsiasi ordine. Gli elementi sono uguali se i loro valori sono uguali, non se si riferiscono allo stesso oggetto.

Usando reflector, ho modificato il codice dietro AreEquivalent () per creare un comparatore di uguaglianza corrispondente. È più completo delle risposte esistenti, poiché tiene conto dei valori null, implementa IEqualityComparer e presenta alcuni controlli di efficienza e casi limite. inoltre, è Microsoft :)

public class MultiSetComparer<T> : IEqualityComparer<IEnumerable<T>>
{
    private readonly IEqualityComparer<T> m_comparer;
    public MultiSetComparer(IEqualityComparer<T> comparer = null)
    {
        m_comparer = comparer ?? EqualityComparer<T>.Default;
    }

    public bool Equals(IEnumerable<T> first, IEnumerable<T> second)
    {
        if (first == null)
            return second == null;

        if (second == null)
            return false;

        if (ReferenceEquals(first, second))
            return true;

        if (first is ICollection<T> firstCollection && second is ICollection<T> secondCollection)
        {
            if (firstCollection.Count != secondCollection.Count)
                return false;

            if (firstCollection.Count == 0)
                return true;
        }

        return !HaveMismatchedElement(first, second);
    }

    private bool HaveMismatchedElement(IEnumerable<T> first, IEnumerable<T> second)
    {
        int firstNullCount;
        int secondNullCount;

        var firstElementCounts = GetElementCounts(first, out firstNullCount);
        var secondElementCounts = GetElementCounts(second, out secondNullCount);

        if (firstNullCount != secondNullCount || firstElementCounts.Count != secondElementCounts.Count)
            return true;

        foreach (var kvp in firstElementCounts)
        {
            var firstElementCount = kvp.Value;
            int secondElementCount;
            secondElementCounts.TryGetValue(kvp.Key, out secondElementCount);

            if (firstElementCount != secondElementCount)
                return true;
        }

        return false;
    }

    private Dictionary<T, int> GetElementCounts(IEnumerable<T> enumerable, out int nullCount)
    {
        var dictionary = new Dictionary<T, int>(m_comparer);
        nullCount = 0;

        foreach (T element in enumerable)
        {
            if (element == null)
            {
                nullCount++;
            }
            else
            {
                int num;
                dictionary.TryGetValue(element, out num);
                num++;
                dictionary[element] = num;
            }
        }

        return dictionary;
    }

    public int GetHashCode(IEnumerable<T> enumerable)
    {
        if (enumerable == null) throw new ArgumentNullException(nameof(enumerable));

        int hash = 17;

        foreach (T val in enumerable.OrderBy(x => x))
            hash = hash * 23 + (val?.GetHashCode() ?? 42);

        return hash;
    }
}

Esempio di utilizzo:

var set = new HashSet<IEnumerable<int>>(new[] {new[]{1,2,3}}, new MultiSetComparer<int>());
Console.WriteLine(set.Contains(new [] {3,2,1})); //true
Console.WriteLine(set.Contains(new [] {1, 2, 3, 3})); //false

O se vuoi solo confrontare due raccolte direttamente:

var comp = new MultiSetComparer<string>();
Console.WriteLine(comp.Equals(new[] {"a","b","c"}, new[] {"a","c","b"})); //true
Console.WriteLine(comp.Equals(new[] {"a","b","c"}, new[] {"a","b"})); //false

Infine, puoi usare il tuo comparatore di uguaglianza di tua scelta:

var strcomp = new MultiSetComparer<string>(StringComparer.OrdinalIgnoreCase);
Console.WriteLine(strcomp.Equals(new[] {"a", "b"}, new []{"B", "A"})); //true

7
Non sono sicuro al 100% ma penso che la tua risposta violi i termini di utilizzo di Microsoft contro il reverse engineering.
Ian Dallas,

1
Ciao Ohad, per favore leggi il seguente lungo dibattito nell'argomento, stackoverflow.com/questions/371328/… Se cambi oggetto hashcode, mentre in un hashset interromperà l'azione corretta dell'hashset e potrebbe causare un'eccezione. La regola è la seguente: se due oggetti sono uguali, devono avere lo stesso codice hash. Se due oggetti hanno lo stesso codice hash, non è necessario che siano uguali. L'hashcode deve rimanere lo stesso per l'intera vita dell'oggetto! Ecco perché impelment ICompareable e IEqualrity.
James Roeiter,

2
@JamesRoeiter Forse il mio commento è stato fuorviante. Quando un dizionario rileva un hashcode che già contiene, verifica l' effettiva uguaglianza con un EqualityComparer(o quello che hai fornito o EqualityComparer.Default, puoi verificare Reflector o l'origine di riferimento per verificarlo). È vero, se gli oggetti cambiano (e in particolare il loro hashcode cambia) mentre questo metodo è in esecuzione, i risultati sono inattesi, ma ciò significa che questo metodo non è sicuro per i thread in questo contesto.
Ohad Schneider,

1
@JamesRoeiter Supponiamo che xey siano due oggetti che vogliamo confrontare. Se hanno codici hash diversi, sappiamo che sono diversi (perché gli elementi uguali hanno hashcode uguali) e l'implementazione sopra è corretta. Se hanno lo stesso hashcode, l'implementazione del dizionario verificherà l' effettiva uguaglianza usando lo specificato EqualityComparer(o EqualityComparer.Defaultse non ne è stato specificato nessuno) e l'implementazione è corretta.
Ohad Schneider,

1
@CADbloke il metodo deve essere chiamato a Equalscausa IEqualityComparer<T>dell'interfaccia. Quello che dovresti guardare è il nome del comparatore stesso . In questo caso è ciò MultiSetComparerche ha senso.
Ohad Schneider,

98

Una soluzione semplice e abbastanza efficiente è quella di ordinare entrambe le raccolte e poi confrontarle per l'uguaglianza:

bool equal = collection1.OrderBy(i => i).SequenceEqual(
                 collection2.OrderBy(i => i));

Questo algoritmo è O (N * logN), mentre la soluzione sopra è O (N ^ 2).

Se le raccolte hanno determinate proprietà, potresti essere in grado di implementare una soluzione più veloce. Ad esempio, se entrambe le raccolte sono set di hash, non possono contenere duplicati. Inoltre, verificare se un set di hash contiene alcuni elementi è molto veloce. In tal caso, un algoritmo simile al tuo sarebbe probabilmente il più veloce.


1
Devi solo aggiungere un usando System.Linq; primo a farlo funzionare
Junior Mayhé,

se questo codice è all'interno di un ciclo e collection1 viene aggiornato e collection2 rimane intatto, notare anche quando entrambe le raccolte hanno lo stesso oggetto, il debugger mostrerebbe false per questa variabile "uguale".
Junior Mayhé,

5
@Chaulky - Credo che OrderBy sia necessario. Vedi: dotnetfiddle.net/jA8iwE
Brett,

Qual è stata l'altra risposta definita "sopra"? Forse stackoverflow.com/a/50465/3195477 ?
UuDdLrLrSs

32

Creare un dizionario "dict" e quindi per ogni membro della prima raccolta, do dict [member] ++;

Quindi, scorrere la seconda raccolta allo stesso modo, ma per ogni membro dettare [membro] -.

Alla fine, passa in rassegna tutti i membri del dizionario:

    private bool SetEqual (List<int> left, List<int> right) {

        if (left.Count != right.Count)
            return false;

        Dictionary<int, int> dict = new Dictionary<int, int>();

        foreach (int member in left) {
            if (dict.ContainsKey(member) == false)
                dict[member] = 1;
            else
                dict[member]++;
        }

        foreach (int member in right) {
            if (dict.ContainsKey(member) == false)
                return false;
            else
                dict[member]--;
        }

        foreach (KeyValuePair<int, int> kvp in dict) {
            if (kvp.Value != 0)
                return false;
        }

        return true;

    }

Modifica: per quanto ne so, è nello stesso ordine dell'algoritmo più efficiente. Questo algoritmo è O (N), supponendo che il Dizionario usi le ricerche O (1).


Questo è quasi quello che voglio. Tuttavia, mi piacerebbe essere in grado di farlo anche se non utilizzo numeri interi. Vorrei usare oggetti di riferimento, ma non si comportano correttamente come chiavi nei dizionari.
mbillard,

Mono, la tua domanda è discutibile se i tuoi articoli non sono comparabili. Se non possono essere utilizzati come chiavi nel dizionario, non è disponibile alcuna soluzione.
skolima,

1
Penso che Mono significasse che i tasti non sono ordinabili. Ma la soluzione di Daniel è chiaramente intesa per essere implementata con una tabella hash, non con un albero, e funzionerà fintanto che c'è un test di equivalenza e una funzione hash.
Erickson,

Ovviamente valutato per l'aiuto, ma non accettato poiché manca un punto importante (che tratterò nella mia risposta).
mbillard,

1
FWIW, puoi semplificare il tuo ultimo ciclo foreach e la dichiarazione di ritorno con questo:return dict.All(kvp => kvp.Value == 0);
Tyson Williams,

18

Questa è la mia (genericamente influenzata da D.Jennings) implementazione generica del metodo di confronto (in C #):

/// <summary>
/// Represents a service used to compare two collections for equality.
/// </summary>
/// <typeparam name="T">The type of the items in the collections.</typeparam>
public class CollectionComparer<T>
{
    /// <summary>
    /// Compares the content of two collections for equality.
    /// </summary>
    /// <param name="foo">The first collection.</param>
    /// <param name="bar">The second collection.</param>
    /// <returns>True if both collections have the same content, false otherwise.</returns>
    public bool Execute(ICollection<T> foo, ICollection<T> bar)
    {
        // Declare a dictionary to count the occurence of the items in the collection
        Dictionary<T, int> itemCounts = new Dictionary<T,int>();

        // Increase the count for each occurence of the item in the first collection
        foreach (T item in foo)
        {
            if (itemCounts.ContainsKey(item))
            {
                itemCounts[item]++;
            }
            else
            {
                itemCounts[item] = 1;
            }
        }

        // Wrap the keys in a searchable list
        List<T> keys = new List<T>(itemCounts.Keys);

        // Decrease the count for each occurence of the item in the second collection
        foreach (T item in bar)
        {
            // Try to find a key for the item
            // The keys of a dictionary are compared by reference, so we have to
            // find the original key that is equivalent to the "item"
            // You may want to override ".Equals" to define what it means for
            // two "T" objects to be equal
            T key = keys.Find(
                delegate(T listKey)
                {
                    return listKey.Equals(item);
                });

            // Check if a key was found
            if(key != null)
            {
                itemCounts[key]--;
            }
            else
            {
                // There was no occurence of this item in the first collection, thus the collections are not equal
                return false;
            }
        }

        // The count of each item should be 0 if the contents of the collections are equal
        foreach (int value in itemCounts.Values)
        {
            if (value != 0)
            {
                return false;
            }
        }

        // The collections are equal
        return true;
    }
}

12
Bel lavoro, ma Nota: 1. Contrariamente alla soluzione di Daniel Jennings, questo non è O (N) ma piuttosto O (N ^ 2), a causa della funzione find all'interno del ciclo foreach sulla collezione bar; 2. Puoi generalizzare il metodo per accettare IEnumerable <T> invece di ICollection <T> senza ulteriori modifiche al codice
Ohad Schneider

The keys of a dictionary are compared by reference, so we have to find the original key that is equivalent to the "item"- questo non è vero. L'algoritmo si basa su presupposti errati e, mentre funziona, è terribilmente inefficiente.
Antonín Lejsek,


7

Se usi Shouldly , puoi usare ShouldAllBe con Contains.

collection1 = {1, 2, 3, 4};
collection2 = {2, 4, 1, 3};

collection1.ShouldAllBe(item=>collection2.Contains(item)); // true

E infine, puoi scrivere un'estensione.

public static class ShouldlyIEnumerableExtensions
{
    public static void ShouldEquivalentTo<T>(this IEnumerable<T> list, IEnumerable<T> equivalent)
    {
        list.ShouldAllBe(l => equivalent.Contains(l));
    }
}

AGGIORNARE

Esiste un parametro facoltativo sul metodo ShouldBe .

collection1.ShouldBe(collection2, ignoreOrder: true); // true

1
Ho appena scoperto sull'ultima versione che esiste un parametro bool ignoreOrdersul metodo ShouldBe .
Pier-Lionel Sgard,

5

EDIT: Mi sono reso conto non appena ho posato che questo funziona davvero solo per i set - non tratterà correttamente delle collezioni che hanno elementi duplicati. Ad esempio {1, 1, 2} e {2, 2, 1} saranno considerati uguali dalla prospettiva di questo algoritmo. Se le tue raccolte sono insiemi (o la loro uguaglianza può essere misurata in questo modo), tuttavia, spero che tu possa trovare utile il seguito.

La soluzione che utilizzo è:

return c1.Count == c2.Count && c1.Intersect(c2).Count() == c1.Count;

Linq fa la cosa del dizionario sotto le copertine, quindi anche questa è O (N). (Nota, è O (1) se le raccolte non hanno le stesse dimensioni).

Ho fatto un controllo di integrità usando il metodo "SetEqual" suggerito da Daniel, il metodo OrderBy / SequenceEquals suggerito da Igor e il mio suggerimento. I risultati sono di seguito, mostrando O (N * LogN) per Igor e O (N) per i miei e quelli di Daniel.

Penso che la semplicità del codice intersecare Linq lo renda la soluzione preferibile.

__Test Latency(ms)__
N, SetEquals, OrderBy, Intersect    
1024, 0, 0, 0    
2048, 0, 0, 0    
4096, 31.2468, 0, 0    
8192, 62.4936, 0, 0    
16384, 156.234, 15.6234, 0    
32768, 312.468, 15.6234, 46.8702    
65536, 640.5594, 46.8702, 31.2468    
131072, 1312.3656, 93.7404, 203.1042    
262144, 3765.2394, 187.4808, 187.4808    
524288, 5718.1644, 374.9616, 406.2084    
1048576, 11420.7054, 734.2998, 718.6764    
2097152, 35090.1564, 1515.4698, 1484.223

L'unico problema con questo codice è che funziona solo quando si confrontano i tipi di valore o si confrontano i puntatori con i tipi di riferimento. Potrei avere due diverse istanze dello stesso oggetto nelle raccolte, quindi devo essere in grado di specificare come confrontare ciascuna. Puoi passare un delegato di confronto al metodo intersect?
mbillard,

Certo, puoi passare un delegato comparatore. Tuttavia, nota la limitazione di cui sopra relativa agli insiemi che ho aggiunto, che pone un limite significativo alla sua applicabilità.

Il metodo Intersect restituisce una raccolta distinta. Dato a = {1,1,2} eb = {2,2,1}, a.Intersect (b) .Count ()! = A.Count, che fa sì che la tua espressione restituisca correttamente false. {1,2} .Count! = {1,1,2} .Count Vedi link [/ link] (Nota che entrambe le parti sono distinte prima del confronto.)
Griffin

5

In caso di nessuna ripetizione e nessun ordine, è possibile utilizzare il seguente EqualityComparer per consentire le raccolte come chiavi del dizionario:

public class SetComparer<T> : IEqualityComparer<IEnumerable<T>> 
where T:IComparable<T>
{
    public bool Equals(IEnumerable<T> first, IEnumerable<T> second)
    {
        if (first == second)
            return true;
        if ((first == null) || (second == null))
            return false;
        return first.ToHashSet().SetEquals(second);
    }

    public int GetHashCode(IEnumerable<T> enumerable)
    {
        int hash = 17;

        foreach (T val in enumerable.OrderBy(x => x))
            hash = hash * 23 + val.GetHashCode();

        return hash;
    }
}

Ecco l'implementazione ToHashSet () che ho usato. L' algoritmo del codice hash proviene da Effective Java (tramite Jon Skeet).


Qual è il punto di serializzazione per la classe Comparer? : o Inoltre è possibile modificare l'ingresso in ISet<T>per esprimere che è pensato per i set (cioè senza duplicati).
nawfal,

@nawfal grazie, non so cosa stavo pensando quando l'ho contrassegnato Serializable ... Per quanto riguarda ISetl'idea, qui era di trattare il IEnumerableset come (perché hai avuto un IEnumerableinizio), anche se considerando gli 0 voti in oltre 5 anni che potrebbero non essere stati l'idea più acuta: P
Ohad Schneider,

4
static bool SetsContainSameElements<T>(IEnumerable<T> set1, IEnumerable<T> set2) {
    var setXOR = new HashSet<T>(set1);
    setXOR.SymmetricExceptWith(set2);
    return (setXOR.Count == 0);
}

La soluzione richiede .NET 3.5 e lo System.Collections.Genericspazio dei nomi. Secondo Microsoft , SymmetricExceptWithè un'operazione O (n + m) , con n che rappresenta il numero di elementi nel primo set e m che rappresenta il numero di elementi nel secondo. È sempre possibile aggiungere un comparatore di uguaglianza a questa funzione, se necessario.


3

Perché non usare .Except ()

// Create the IEnumerable data sources.
string[] names1 = System.IO.File.ReadAllLines(@"../../../names1.txt");
string[] names2 = System.IO.File.ReadAllLines(@"../../../names2.txt");
// Create the query. Note that method syntax must be used here.
IEnumerable<string> differenceQuery =   names1.Except(names2);
// Execute the query.
Console.WriteLine("The following lines are in names1.txt but not names2.txt");
foreach (string s in differenceQuery)
     Console.WriteLine(s);

http://msdn.microsoft.com/en-us/library/bb397894.aspx


2
Exceptnon funzionerà per il conteggio di oggetti duplicati. Restituirà vero per i set {1,2,2} e {1,1,2}.
Cristian Diaconescu,

@CristiDiaconescu potresti fare prima un ".Distinct ()" per rimuovere eventuali duplicati
Korayem

L'OP chiede [1, 1, 2] != [1, 2, 2]. L'uso Distinctli renderebbe uguali.
Cristian Diaconescu,

2

Un post duplicato di sorta, ma controlla la mia soluzione per confrontare le raccolte . È abbastanza semplice:

Ciò eseguirà un confronto di uguaglianza indipendentemente dall'ordine:

var list1 = new[] { "Bill", "Bob", "Sally" };
var list2 = new[] { "Bob", "Bill", "Sally" };
bool isequal = list1.Compare(list2).IsSame;

Questo verificherà se gli articoli sono stati aggiunti / rimossi:

var list1 = new[] { "Billy", "Bob" };
var list2 = new[] { "Bob", "Sally" };
var diff = list1.Compare(list2);
var onlyinlist1 = diff.Removed; //Billy
var onlyinlist2 = diff.Added;   //Sally
var inbothlists = diff.Equal;   //Bob

Questo vedrà quali elementi nel dizionario sono cambiati:

var original = new Dictionary<int, string>() { { 1, "a" }, { 2, "b" } };
var changed = new Dictionary<int, string>() { { 1, "aaa" }, { 2, "b" } };
var diff = original.Compare(changed, (x, y) => x.Value == y.Value, (x, y) => x.Value == y.Value);
foreach (var item in diff.Different)
  Console.Write("{0} changed to {1}", item.Key.Value, item.Value.Value);
//Will output: a changed to aaa

Post originale qui .


1

erickson ha quasi ragione: poiché vuoi abbinare per numero di duplicati, vuoi una borsa . In Java, questo assomiglia a:

(new HashBag(collection1)).equals(new HashBag(collection2))

Sono sicuro che C # ha un'implementazione Set integrata. Lo userei per primo; se le prestazioni sono un problema, è sempre possibile utilizzare un'implementazione Set diversa, ma utilizzare la stessa interfaccia Set.


1

Ecco la mia variante del metodo di estensione della risposta di Ohadsc, nel caso sia utile a qualcuno

static public class EnumerableExtensions 
{
    static public bool IsEquivalentTo<T>(this IEnumerable<T> first, IEnumerable<T> second)
    {
        if ((first == null) != (second == null))
            return false;

        if (!object.ReferenceEquals(first, second) && (first != null))
        {
            if (first.Count() != second.Count())
                return false;

            if ((first.Count() != 0) && HaveMismatchedElement<T>(first, second))
                return false;
        }

        return true;
    }

    private static bool HaveMismatchedElement<T>(IEnumerable<T> first, IEnumerable<T> second)
    {
        int firstCount;
        int secondCount;

        var firstElementCounts = GetElementCounts<T>(first, out firstCount);
        var secondElementCounts = GetElementCounts<T>(second, out secondCount);

        if (firstCount != secondCount)
            return true;

        foreach (var kvp in firstElementCounts)
        {
            firstCount = kvp.Value;
            secondElementCounts.TryGetValue(kvp.Key, out secondCount);

            if (firstCount != secondCount)
                return true;
        }

        return false;
    }

    private static Dictionary<T, int> GetElementCounts<T>(IEnumerable<T> enumerable, out int nullCount)
    {
        var dictionary = new Dictionary<T, int>();
        nullCount = 0;

        foreach (T element in enumerable)
        {
            if (element == null)
            {
                nullCount++;
            }
            else
            {
                int num;
                dictionary.TryGetValue(element, out num);
                num++;
                dictionary[element] = num;
            }
        }

        return dictionary;
    }

    static private int GetHashCode<T>(IEnumerable<T> enumerable)
    {
        int hash = 17;

        foreach (T val in enumerable.OrderBy(x => x))
            hash = hash * 23 + val.GetHashCode();

        return hash;
    }
}

Quanto bene funziona, qualche idea?
nawfal,

Lo uso solo per piccole collezioni, quindi non ho pensato alla complessità di Big-O o fatto benchmarking. HaveMismatchedElements è solo O (M * N), quindi potrebbe non funzionare bene per raccolte di grandi dimensioni.
Eric J.

Se IEnumerable<T>s sono domande, chiamare Count()non è una buona idea. L'approccio della risposta originale di Ohad per verificare se sono ICollection<T>l'idea migliore.
nawfal,

1

Ecco una soluzione che è un miglioramento rispetto a questa .

public static bool HasSameElementsAs<T>(
        this IEnumerable<T> first, 
        IEnumerable<T> second, 
        IEqualityComparer<T> comparer = null)
    {
        var firstMap = first
            .GroupBy(x => x, comparer)
            .ToDictionary(x => x.Key, x => x.Count(), comparer);

        var secondMap = second
            .GroupBy(x => x, comparer)
            .ToDictionary(x => x.Key, x => x.Count(), comparer);

        if (firstMap.Keys.Count != secondMap.Keys.Count)
            return false;

        if (firstMap.Keys.Any(k1 => !secondMap.ContainsKey(k1)))
            return false;

        return firstMap.Keys.All(x => firstMap[x] == secondMap[x]);
    }

0

Esistono molte soluzioni a questo problema. Se non ti interessano i duplicati, non devi ordinare entrambi. Per prima cosa assicurati che abbiano lo stesso numero di articoli. Dopodiché ordina una delle collezioni. Quindi cerca ogni elemento della seconda raccolta nella raccolta ordinata. Se non trovi un determinato oggetto, fermati e restituisci false. La complessità di questo: - ordinare la prima raccolta: N Log (N) - la ricerca di ogni elemento dal secondo al primo: NLOG (N) quindi finisci con 2 * N * LOG (N) supponendo che corrispondano e cerchi tutto. Questo è simile alla complessità dell'ordinamento di entrambi. Anche questo ti dà il vantaggio di fermarti prima se c'è una differenza. Tuttavia, tieni presente che se entrambi vengono ordinati prima di entrare in questo confronto e provi l'ordinamento utilizzando qualcosa come un qsort, l'ordinamento sarà più costoso. Ci sono ottimizzazioni per questo. Un'altra alternativa, che è ottima per le piccole raccolte in cui si conosce la gamma degli elementi è quella di utilizzare un indice maschera di bit. Questo ti darà una prestazione O (n). Un'altra alternativa è usare un hash e cercarlo. Per le piccole raccolte di solito è molto meglio eseguire l'ordinamento o l'indice della maschera di bit. L'hashtable ha lo svantaggio di una località peggiore, quindi tienilo a mente. Ancora una volta, questo è solo se non non importa dei duplicati. Se si desidera tenere conto dei duplicati, procedere con l'ordinamento di entrambi.


0

In molti casi l'unica risposta adatta è quella di Igor Ostrovsky, altre risposte si basano sul codice hash degli oggetti. Ma quando generi un codice hash per un oggetto lo fai solo in base ai suoi campi IMMUTABILI - come il campo ID oggetto (nel caso di un'entità database) - Perché è importante sovrascrivere GetHashCode quando il metodo Equals viene ignorato?

Ciò significa che se si confrontano due raccolte, il risultato potrebbe essere vero per il metodo di confronto anche se i campi dei diversi elementi non sono uguali. Per confrontare a fondo le raccolte, è necessario utilizzare il metodo Igor e implementare IEqualirity.

Per favore, leggi i commenti di me e Mr.Schnider sul suo post più votato.

Giacomo


0

Consentire duplicati in IEnumerable<T>(se gli insiemi non sono desiderabili \ possibile) e "ignorare l'ordine" dovresti essere in grado di usare a .GroupBy().

Non sono un esperto delle misurazioni della complessità, ma la mia comprensione rudimentale è che questo dovrebbe essere O (n). Capisco O (n ^ 2) come proveniente dall'esecuzione di un'operazione O (n) all'interno di un'altra operazione O (n) comeListA.Where(a => ListB.Contains(a)).ToList() . Ogni elemento in ListB viene valutato per l'uguaglianza rispetto a ciascun elemento in ListA.

Come ho detto, la mia comprensione della complessità è limitata, quindi correggetemi se sbaglio.

public static bool IsSameAs<T, TKey>(this IEnumerable<T> source, IEnumerable<T> target, Expression<Func<T, TKey>> keySelectorExpression)
    {
        // check the object
        if (source == null && target == null) return true;
        if (source == null || target == null) return false;

        var sourceList = source.ToList();
        var targetList = target.ToList();

        // check the list count :: { 1,1,1 } != { 1,1,1,1 }
        if (sourceList.Count != targetList.Count) return false;

        var keySelector = keySelectorExpression.Compile();
        var groupedSourceList = sourceList.GroupBy(keySelector).ToList();
        var groupedTargetList = targetList.GroupBy(keySelector).ToList();

        // check that the number of grouptings match :: { 1,1,2,3,4 } != { 1,1,2,3,4,5 }
        var groupCountIsSame = groupedSourceList.Count == groupedTargetList.Count;
        if (!groupCountIsSame) return false;

        // check that the count of each group in source has the same count in target :: for values { 1,1,2,3,4 } & { 1,1,1,2,3,4 }
        // key:count
        // { 1:2, 2:1, 3:1, 4:1 } != { 1:3, 2:1, 3:1, 4:1 }
        var countsMissmatch = groupedSourceList.Any(sourceGroup =>
                                                        {
                                                            var targetGroup = groupedTargetList.Single(y => y.Key.Equals(sourceGroup.Key));
                                                            return sourceGroup.Count() != targetGroup.Count();
                                                        });
        return !countsMissmatch;
    }

0

Questa semplice soluzione impone l' IEnumerableimplementazione del tipo generico IComparable. Per via della OrderBydefinizione.

Se non vuoi fare un simile presupposto ma vuoi comunque usare questa soluzione, puoi usare il seguente codice:

bool equal = collection1.OrderBy(i => i?.GetHashCode())
   .SequenceEqual(collection2.OrderBy(i => i?.GetHashCode()));

0

Se si esegue il confronto ai fini delle asserzioni di unit test, può essere logico gettare un po 'di efficienza fuori dalla finestra e semplicemente convertire ciascun elenco in una rappresentazione di stringa (CSV) prima di eseguire il confronto. In questo modo, il messaggio di asserzione di prova predefinito mostrerà le differenze all'interno del messaggio di errore.

Uso:

using Microsoft.VisualStudio.TestTools.UnitTesting;

// define collection1, collection2, ...

Assert.Equal(collection1.OrderBy(c=>c).ToCsv(), collection2.OrderBy(c=>c).ToCsv());

Metodo di estensione dell'helper:

public static string ToCsv<T>(
    this IEnumerable<T> values,
    Func<T, string> selector,
    string joinSeparator = ",")
{
    if (selector == null)
    {
        if (typeof(T) == typeof(Int16) ||
            typeof(T) == typeof(Int32) ||
            typeof(T) == typeof(Int64))
        {
            selector = (v) => Convert.ToInt64(v).ToStringInvariant();
        }
        else if (typeof(T) == typeof(decimal))
        {
            selector = (v) => Convert.ToDecimal(v).ToStringInvariant();
        }
        else if (typeof(T) == typeof(float) ||
                typeof(T) == typeof(double))
        {
            selector = (v) => Convert.ToDouble(v).ToString(CultureInfo.InvariantCulture);
        }
        else
        {
            selector = (v) => v.ToString();
        }
    }

    return String.Join(joinSeparator, values.Select(v => selector(v)));
}
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.