Vergleich zweier Sammlungen auf Gleichheit unabhängig von der Reihenfolge der darin enthaltenen Elemente

162

Ich möchte zwei Sammlungen (in C #) vergleichen, bin mir aber nicht sicher, wie ich dies am besten effizient implementieren kann.

Ich habe den anderen Thread über Enumerable.SequenceEqual gelesen , aber es ist nicht genau das, wonach ich suche.

In meinem Fall wären zwei Sammlungen gleich, wenn beide dieselben Artikel enthalten (unabhängig von der Reihenfolge).

Beispiel:

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

collection1 == collection2; // true

Was ich normalerweise mache, ist, jedes Element einer Sammlung zu durchlaufen und zu prüfen, ob es in der anderen Sammlung vorhanden ist, dann jedes Element der anderen Sammlung zu durchlaufen und zu prüfen, ob es in der ersten Sammlung vorhanden ist. (Ich beginne mit dem Vergleich der Längen).

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

Dies ist jedoch nicht ganz richtig und wahrscheinlich nicht die effizienteste Methode, um zwei Sammlungen auf Gleichheit zu vergleichen.

Ein Beispiel, das ich mir vorstellen kann, wäre falsch:

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

Welches wäre gleich mit meiner Implementierung. Sollte ich nur zählen, wie oft jeder Artikel gefunden wurde, und sicherstellen, dass die Anzahl in beiden Sammlungen gleich ist?


Die Beispiele sind in einer Art C # (nennen wir es Pseudo-C #), aber geben Sie Ihre Antwort in einer beliebigen Sprache, es spielt keine Rolle.

Hinweis: Der Einfachheit halber habe ich in den Beispielen Ganzzahlen verwendet, möchte aber auch Objekte vom Referenztyp verwenden können (sie verhalten sich nicht korrekt als Schlüssel, da nur die Referenz des Objekts verglichen wird, nicht der Inhalt).

mbillard
quelle
1
Wie wäre es mit Algorithmus? Alle Antworten beziehen sich auf den Vergleich von etwas, generische Listen auf den Vergleich von Linq usw. Haben wir wirklich jemandem versprochen, dass wir als altmodischer Programmierer niemals einen Algorithmus verwenden werden?
Nuri YILMAZ
Sie prüfen nicht auf Gleichheit, sondern auf Gleichwertigkeit. Es ist pingelig, aber ein wichtiger Unterschied. Und vor langer Zeit. Dies ist eine gute Frage + A.
CAD Kerl
Dieser Beitrag könnte Sie interessieren, in dem eine optimierte Version der unten beschriebenen wörterbuchbasierten Methode erläutert wird. Ein Problem bei den meisten einfachen Wörterbuchansätzen besteht darin, dass sie Nullen nicht richtig verarbeiten, da die Dictionary-Klasse von .NET keine Nullschlüssel zulässt.
ChaseMedallion

Antworten:

112

Es stellt sich heraus, dass Microsoft dies bereits in seinem Testframework behandelt hat: CollectionAssert.AreEquivalent

Bemerkungen

Zwei Sammlungen sind gleichwertig, wenn sie dieselben Elemente in derselben Menge, jedoch in beliebiger Reihenfolge enthalten. Elemente sind gleich, wenn ihre Werte gleich sind, nicht wenn sie sich auf dasselbe Objekt beziehen.

Mit Reflektor habe ich den Code hinter AreEquivalent () geändert, um einen entsprechenden Gleichheitsvergleicher zu erstellen. Es ist vollständiger als vorhandene Antworten, da es Nullen berücksichtigt, IEqualityComparer implementiert und einige Effizienz- und Randfallprüfungen aufweist. Außerdem ist es 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;
    }
}

Beispielnutzung:

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

Oder wenn Sie nur zwei Sammlungen direkt vergleichen möchten:

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

Schließlich können Sie einen Gleichstellungsvergleicher Ihrer Wahl verwenden:

var strcomp = new MultiSetComparer<string>(StringComparer.OrdinalIgnoreCase);
Console.WriteLine(strcomp.Equals(new[] {"a", "b"}, new []{"B", "A"})); //true
Ohad Schneider
quelle
7
Ich bin nicht 100% sicher, aber ich denke, Ihre Antwort verstößt gegen die Nutzungsbedingungen von Microsoft gegen Reverse Engineering.
Ian Dallas
1
Hallo Ohad, bitte lesen Sie die folgende lange Debatte im Thema stackoverflow.com/questions/371328/…. Wenn Sie den Objekt-Hashcode ändern, während er sich in einem Hashset befindet, wird er mit der richtigen Hashset-Aktion unterbrochen und kann eine Ausnahme verursachen. Die Regel lautet wie folgt: Wenn zwei Objekte gleich sind, müssen sie denselben Hashcode haben. Wenn zwei Objekte denselben Hashcode haben, ist es kein Muss, dass sie gleich sind. Der Hashcode muss während der gesamten Lebensdauer des Objekts gleich bleiben! Deshalb treiben Sie ICompareable und IEqualrity an.
James Roeiter
2
@ JamesRoeiter Vielleicht war mein Kommentar irreführend. Wenn ein Wörterbuch auf einen bereits enthaltenen Hashcode stößt, prüft es die tatsächliche Gleichheit mit einem EqualityComparer(entweder dem von Ihnen angegebenen oder EqualityComparer.DefaultSie können Reflector oder die Referenzquelle überprüfen, um dies zu überprüfen). Wenn sich Objekte während der Ausführung dieser Methode ändern (und insbesondere ihr Hashcode ändert), sind die Ergebnisse zwar unerwartet, dies bedeutet jedoch nur, dass diese Methode in diesem Kontext nicht threadsicher ist.
Ohad Schneider
1
@JamesRoeiter Angenommen, x und y sind zwei Objekte, die wir vergleichen möchten. Wenn sie unterschiedliche Hashcodes haben, wissen wir, dass sie unterschiedlich sind (weil gleiche Elemente gleiche Hashcodes haben), und die obige Implementierung ist korrekt. Wenn sie denselben Hashcode haben, überprüft die Wörterbuchimplementierung anhand des angegebenen (oder wenn keiner angegeben wurde) die tatsächliche Gleichheit , und die Implementierung ist erneut korrekt. EqualityComparerEqualityComparer.Default
Ohad Schneider
1
@CADbloke Die Methode muss Equalswegen der IEqualityComparer<T>Schnittstelle benannt werden. Was Sie sich ansehen sollten, ist der Name des Vergleichers selbst . In diesem Fall MultiSetComparermacht es Sinn.
Ohad Schneider