Porównanie dwóch kolekcji pod kątem równości niezależnie od kolejności elementów w nich


162

Chciałbym porównać dwie kolekcje (w C #), ale nie jestem pewien, jaki jest najlepszy sposób na wydajne wdrożenie tego.

Przeczytałem inny wątek o Enumerable.SequenceEqual , ale nie jest to dokładnie to, czego szukam.

W moim przypadku dwie kolekcje byłyby równe, gdyby obie zawierały te same elementy (bez względu na kolejność).

Przykład:

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

collection1 == collection2; // true

Zwykle robię pętlę przez każdy element jednej kolekcji i sprawdzam, czy istnieje w drugiej kolekcji, a następnie przechodzę w pętli przez każdy element z drugiej kolekcji i sprawdzam, czy istnieje w pierwszej kolekcji. (Zaczynam od porównania długości).

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

Jednak nie jest to całkowicie poprawne i prawdopodobnie nie jest to najbardziej efektywny sposób porównywania dwóch kolekcji pod kątem równości.

Przykład, o którym mogę pomyśleć, byłby zły:

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

Co byłoby równe mojej realizacji. Czy mam po prostu policzyć, ile razy każdy przedmiot został znaleziony i upewnić się, że liczby są równe w obu kolekcjach?


Przykłady są w jakimś języku C # (nazwijmy to pseudo-C #), ale udziel odpowiedzi w dowolnym języku, to nie ma znaczenia.

Uwaga: Użyłem liczb całkowitych w przykładach ze względu na prostotę, ale chcę również móc używać obiektów typu referencyjnego (nie zachowują się one poprawnie jak klucze, ponieważ porównywane jest tylko odniesienie do obiektu, a nie treść).


1
A co z algorytmem? Wszystkie odpowiedzi związane przez porównanie czegoś, ogólne listy porównują linq itp. Naprawdę obiecaliśmy komuś, że nigdy nie będziemy używać algorytmu jako staroświecki programista?
Nuri YILMAZ

Nie sprawdzasz równości, ale sprawdzasz równoważność. To trudne, ale ważne rozróżnienie. I dawno temu. To jest dobre Q + A.
Facet z CAD

Może Cię zainteresować ten post , w którym omówiono ulepszoną wersję metody słownikowej opisanej poniżej. Jednym z problemów z większością prostych podejść słownikowych jest to, że nie obsługują one poprawnie wartości null, ponieważ klasa Dictionary platformy .NET nie zezwala na klucze o wartości null.
ChaseMedallion

Odpowiedzi:


112

Okazuje się, że Microsoft uwzględnił to już w swojej strukturze testowej: CollectionAssert.AreEquivalent

Uwagi

Dwie kolekcje są równoważne, jeśli zawierają te same elementy w tej samej ilości, ale w dowolnej kolejności. Elementy są równe, jeśli ich wartości są równe, a nie, jeśli odnoszą się do tego samego obiektu.

Używając reflektora, zmodyfikowałem kod za AreEquivalent (), aby utworzyć odpowiednią funkcję porównującą równość. Jest bardziej kompletny niż istniejące odpowiedzi, ponieważ bierze pod uwagę wartości null, implementuje IEqualityComparer i ma pewną wydajność i kontrolę przypadków skrajnych. plus, to 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;
    }
}

Przykładowe użycie:

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

Lub jeśli chcesz bezpośrednio porównać dwie kolekcje:

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

Na koniec możesz użyć wybranej przez siebie porównywarki równości:

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

7
Nie jestem w 100% pewien, ale myślę, że twoja odpowiedź narusza warunki użytkowania firmy Microsoft przeciwko inżynierii wstecznej.
Ian Dallas

1
Witaj Ohad, proszę przeczytaj następującą długą debatę w tym temacie, stackoverflow.com/questions/371328/… Jeśli zmienisz hashcode obiektu, gdy jest on w hashset, przerwie to właściwe działanie hashset i może spowodować wyjątek. Zasada jest następująca: Jeśli dwa obiekty są równe - muszą mieć ten sam kod skrótu. Jeśli dwa obiekty mają ten sam kod skrótu - nie jest konieczne, aby były równe. Hashcode musi pozostać taki sam przez cały okres istnienia obiektu! To dlatego wymuszasz porównanie I równości.
James Roeiter,

2
@JamesRoeiter Być może mój komentarz wprowadził w błąd. Gdy słownik napotka kod skrótu, który już zawiera, sprawdza rzeczywistą równość z EqualityComparer(podanym przez Ciebie lub EqualityComparer.Defaultmożesz sprawdzić Reflektor lub źródło odniesienia, aby to sprawdzić). To prawda, że ​​jeśli obiekty ulegną zmianie (a konkretnie zmiany kodu skrótu), gdy ta metoda jest uruchomiona, wyniki są nieoczekiwane, ale to po prostu oznacza, że ​​ta metoda nie jest bezpieczna wątkowo w tym kontekście.
Ohad Schneider

1
@JamesRoeiter Załóżmy, że x i y to dwa obiekty, które chcemy porównać. Jeśli mają różne hashcodes, wiemy, że są różne (ponieważ równe elementy mają równe hashcodes), a powyższa implementacja jest poprawna. Jeśli mają ten sam kod skrótu, implementacja słownika sprawdzi rzeczywistą równość przy użyciu określonego EqualityComparer(lub EqualityComparer.Defaultjeśli nie określono) i ponownie implementacja jest poprawna.
Ohad Schneider,

1
@CADbloke metoda musi być nazwana Equalsze względu na IEqualityComparer<T>interfejs. To, na co powinieneś spojrzeć, to nazwa samego elementu porównującego . W tym przypadku to MultiSetComparerma sens.
Ohad Schneider,

98

Prostym i dość wydajnym rozwiązaniem jest posortowanie obu kolekcji, a następnie porównanie ich pod kątem równości:

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

Ten algorytm to O (N * logN), podczas gdy powyższe rozwiązanie to O (N ^ 2).

Jeśli kolekcje mają określone właściwości, możesz wdrożyć szybsze rozwiązanie. Na przykład, jeśli obie kolekcje są zestawami skrótów, nie mogą zawierać duplikatów. Również sprawdzenie, czy zestaw hash zawiera jakiś element, jest bardzo szybkie. W takim przypadku algorytm podobny do twojego prawdopodobnie byłby najszybszy.


1
Wystarczy dodać using System.Linq; pierwszy, aby to zadziałało
Junior Mayhé

jeśli ten kod znajduje się w pętli i kolekcja1 zostanie zaktualizowana, a kolekcja2 pozostanie nietknięta, zauważ, że nawet jeśli obie kolekcje mają ten sam obiekt, debugger pokaże fałsz dla tej zmiennej „równej”.
Junior Mayhé

5
@Chaulky - Uważam, że OrderBy jest potrzebny. Zobacz: dotnetfiddle.net/jA8iwE
Brett

Jaka była inna odpowiedź, o której mowa jako „powyżej”? Prawdopodobnie stackoverflow.com/a/50465/3195477 ?
UuDdLrLrSs

32

Utwórz słownik „dict”, a następnie dla każdego członka w pierwszej kolekcji wykonaj dict [member] ++;

Następnie wykonaj pętlę nad drugą kolekcją w ten sam sposób, ale dla każdego elementu członkowskiego wykonaj polecenie [element członkowski] -.

Na koniec obejrzyj wszystkich członków słownika:

    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;

    }

Edycja: O ile wiem, jest to w tej samej kolejności, co najbardziej wydajny algorytm. Ten algorytm to O (N), przy założeniu, że Słownik używa wyszukiwań O (1).


To jest prawie to, czego chcę. Chciałbym jednak móc to zrobić, nawet jeśli nie używam liczb całkowitych. Chciałbym używać obiektów referencyjnych, ale nie zachowują się one poprawnie jak klucze w słownikach.
mbillard

Mono, twoje pytanie jest dyskusyjne, jeśli twoje przedmioty nie są porównywalne. Jeśli nie można ich użyć jako kluczy w Słowniku, nie ma rozwiązania.
skolima

1
Myślę, że Mono oznaczało, że klucze nie są sortowalne. Ale rozwiązanie Daniela jest wyraźnie przeznaczone do implementacji za pomocą tablicy haszującej, a nie drzewa, i będzie działać tak długo, jak długo istnieje test równoważności i funkcja skrótu.
erickson

Głosowano oczywiście za pomocą, ale nie przyjęto go, ponieważ brakuje w nim ważnego punktu (który omawiam w mojej odpowiedzi).
mbillard

1
FWIW, możesz uprościć swoją ostatnią pętlę foreach i zwrócić instrukcję za pomocą tego:return dict.All(kvp => kvp.Value == 0);
Tyson Williams

18

Oto moja (na którą duży wpływ wywarł D.Jennings) generyczna implementacja metody porównania (w 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
Dobra robota, ale Uwaga: 1. W przeciwieństwie do rozwiązania Daniela Jenningsa, to nie jest O (N), ale raczej O (N ^ 2), z powodu funkcji find wewnątrz pętli foreach w kolekcji słupków; 2. Możesz uogólnić metodę, aby zaakceptować IEnumerable <T> zamiast ICollection <T> bez dalszych modyfikacji kodu
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"- to nie jest prawda. Algorytm opiera się na błędnych założeniach i chociaż działa, jest strasznie nieefektywny.
Antonín Lejsek


7

Jeśli używasz Shouldly , możesz użyć ShouldAllBe z Contains.

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

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

Na koniec możesz napisać rozszerzenie.

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

AKTUALIZACJA

Opcjonalny parametr istnieje w metodzie ShouldBe .

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

1
Właśnie znalazłem w najnowszej wersji , że jest parametr bool ignoreOrderdotyczący metody ShouldBe .
Pier-Lionel Sgard

5

EDYCJA: Zdałem sobie sprawę, gdy tylko stwierdziłem, że to naprawdę działa tylko w przypadku zestawów - nie będzie poprawnie radzić sobie z kolekcjami, które mają zduplikowane elementy. Na przykład {1, 1, 2} i {2, 2, 1} będą uważane za równe z punktu widzenia tego algorytmu. Jeśli jednak Twoje kolekcje są zestawami (lub ich równość można zmierzyć w ten sposób), mam nadzieję, że poniższe informacje okażą się przydatne.

Rozwiązanie, którego używam, to:

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

Linq robi to ze słownika pod okładkami, więc to też jest O (N). (Uwaga: to O (1), jeśli kolekcje nie są tego samego rozmiaru).

Zrobiłem test poczytalności, używając metody „SetEqual” sugerowanej przez Daniela, metody OrderBy / SequenceEquals sugerowanej przez Igora oraz mojej sugestii. Wyniki są poniżej, pokazując O (N * LogN) dla Igora i O (N) dla mojego i Daniela.

Myślę, że prostota kodu przecięcia Linq sprawia, że ​​jest to preferowane rozwiązanie.

__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

Jedynym problemem związanym z tym kodem jest to, że działa on tylko podczas porównywania typów wartości lub porównywania wskaźników z typami odwołań. Mogę mieć w kolekcjach dwie różne instancje tego samego obiektu, więc muszę mieć możliwość określenia sposobu porównywania każdego z nich. Czy możesz przekazać delegata porównania do metody intersect?
mbillard

Jasne, możesz przekazać delegata funkcji porównującej. Ale zwróć uwagę na powyższe ograniczenie dotyczące zestawów, które dodałem, które nakłada znaczące ograniczenie na jego zastosowanie.

Metoda Intersect zwraca odrębną kolekcję. Biorąc pod uwagę a = {1,1,2} i b = {2,2,1}, a.Intersect (b) .Count ()! = A.Count, co powoduje, że wyrażenie poprawnie zwraca fałsz. {1,2} .Count! = {1,1,2} .Count Zobacz link [/ link] (Zauważ, że obie strony są oddzielne przed porównaniem.)
Griffin

5

W przypadku braku powtórzeń i kolejności można użyć następującego EqualityComparer, aby zezwolić na kolekcje jako klucze słownikowe:

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;
    }
}

Oto implementacja ToHashSet (), której użyłem. Algorytm kod hash pochodzi z Effective Java (w drodze Jon Skeet).


Jaki jest cel klasy Serializable for Comparer? : o Możesz także zmienić wejście, aby ISet<T>wyrazić, że jest przeznaczone dla zestawów (tj. bez duplikatów).
nawfal

@nawfal dzięki, nie wiem, o czym myślałem, kiedy oznaczyłem go jako serializowalny ... A propos, ISettutaj chodziło o potraktowanie IEnumerablezestawu jako zestawu (bo masz IEnumerablena początek), choć biorąc pod uwagę 0 głosów za ponad 5 lat to chyba nie był najlepszy pomysł: 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);
}

Rozwiązanie wymaga platformy .NET 3.5 i System.Collections.Genericprzestrzeni nazw. Według Microsoft , SymmetricExceptWithto O (n + m) operacji, z n oznaczająca liczbę elementów w pierwszym zbiorze i m , oznaczającą liczbę elementów na sekundę. W razie potrzeby zawsze można dodać funkcję porównującą równość do tej funkcji.


3

Dlaczego nie użyć .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
Exceptnie będzie działać przy liczeniu zduplikowanych elementów. Zwróci prawdę dla zestawów {1,2,2} i {1,1,2}.
Cristian Diaconescu

@CristiDiaconescu możesz najpierw zrobić „.Distinct ()”, aby usunąć wszelkie duplikaty
Korayem

OP prosi [1, 1, 2] != [1, 2, 2]. Używanie Distinctsprawi, że będą wyglądać równo.
Cristian Diaconescu

2

Swojego rodzaju zduplikowany post, ale sprawdź moje rozwiązanie do porównywania kolekcji . To całkiem proste:

Spowoduje to wykonanie porównania równości niezależnie od kolejności:

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

Spowoduje to sprawdzenie, czy elementy zostały dodane / usunięte:

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

Spowoduje to zmianę pozycji w słowniku:

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

Oryginalny post tutaj .


1

erickson ma prawie rację: ponieważ chcesz dopasować liczbę duplikatów, potrzebujesz torby . W Javie wygląda to mniej więcej tak:

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

Jestem pewien, że C # ma wbudowaną implementację zestawu. Najpierw użyłbym tego; jeśli wydajność jest problemem, zawsze możesz użyć innej implementacji zestawu, ale użyć tego samego interfejsu zestawu.


1

Oto mój wariant metody rozszerzenia odpowiedzi Ohadsc, na wypadek, gdyby był dla kogoś przydatny

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;
    }
}

Jak dobrze to działa, jakieś pomysły?
nawfal

Używam tego tylko do małych kolekcji, więc nie myślałem o złożoności Big-O ani nie robiłem testów porównawczych. Sam parametr HaveMismatchedElements ma wartość O (M * N), więc może nie działać dobrze w przypadku dużych kolekcji.
Eric J.

Jeśli IEnumerable<T>są zapytaniami, dzwonienie Count()nie jest dobrym pomysłem. Oryginalna odpowiedź Ohada polegająca na sprawdzaniu, czy tak jest, ICollection<T>jest lepszym pomysłem.
nawfal

1

Oto rozwiązanie, które jest ulepszeniem w stosunku do tego .

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

Istnieje wiele rozwiązań tego problemu. Jeśli nie dbasz o duplikaty, nie musisz sortować obu. Najpierw upewnij się, że mają taką samą liczbę elementów. Potem jedna z kolekcji. Następnie binsearch każdy element z drugiej kolekcji w posortowanej kolekcji. Jeśli nie znajdziesz danej pozycji, zatrzymaj się i zwróć false. Złożoność tego: - sortowanie pierwszego zbioru: N Log (N) - przeszukiwanie każdego elementu od drugiego do pierwszego: NLOG (N), więc otrzymujesz 2 * N * LOG (N), zakładając, że pasują i sprawdzasz wszystko. Jest to podobne do złożoności sortowania obu. Daje to również korzyść, jeśli istnieje różnica, jeśli zatrzymasz się wcześniej. Należy jednak pamiętać, że jeśli oba zostaną posortowane przed przejściem do tego porównania i spróbujesz posortować przy użyciu czegoś takiego jak qsort, sortowanie będzie droższe. Istnieją optymalizacje dla tego. Inną alternatywą, która jest świetna w przypadku małych kolekcji, w których znasz zakres elementów, jest użycie indeksu maski bitowej. To da ci wydajność O (n). Inną alternatywą jest użycie hasha i sprawdzenie go. W przypadku małych kolekcji zwykle znacznie lepiej jest posortować lub indeksować maskę bitową. Hashtable mają wadę gorszej lokalizacji, więc miej to na uwadze. Ponownie, to tylko wtedy, gdy nie dbam o duplikaty. Jeśli chcesz uwzględnić duplikaty, posortuj oba.


0

W wielu przypadkach jedyną właściwą odpowiedzią jest odpowiedź Igora Ostrowskiego, inne odpowiedzi opierają się na kodzie skrótu obiektów. Ale kiedy generujesz kod skrótu dla obiektu, robisz to tylko na podstawie jego IMMUTABLE pól - takich jak pole ID obiektu (w przypadku encji bazy danych) - dlaczego ważne jest, aby przesłonić GetHashCode, gdy metoda Equals jest nadpisywana?

Oznacza to, że jeśli porównasz dwie kolekcje, wynik może być prawdziwy dla metody porównania, nawet jeśli pola różnych elementów nie są równe. Aby dokładnie porównać kolekcje, musisz użyć metody Igora i zaimplementować IEqualirity.

Przeczytaj komentarze moje i pana Schnidera do jego postu, na który najczęściej głosowano.

James


0

Pozwalając na duplikaty w IEnumerable<T>(jeśli zestawy nie są pożądane \ możliwe) i "ignorując kolejność", powinieneś móc użyć pliku .GroupBy().

Nie jestem ekspertem w pomiarach złożoności, ale moje podstawowe zrozumienie jest takie, że powinno to być O (n). Rozumiem, że O (n ^ 2) pochodzi z wykonania operacji O (n) wewnątrz innej operacji O (n), takiej jak ListA.Where(a => ListB.Contains(a)).ToList(). Każdy element na LiścieB jest oceniany pod kątem równości względem każdego elementu na LiścieA.

Jak powiedziałem, moje rozumienie złożoności jest ograniczone, więc popraw mnie, jeśli się mylę.

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

To proste rozwiązanie wymusza IEnumerablezaimplementowanie typu ogólnego IComparable. Z powodu OrderBydefinicji.

Jeśli nie chcesz robić takiego założenia, ale nadal chcesz skorzystać z tego rozwiązania, możesz skorzystać z następującego fragmentu kodu:

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

0

Porównując na potrzeby twierdzeń testów jednostkowych, sensowne może być wyrzucenie trochę wydajności przez okno i po prostu przekonwertowanie każdej listy na reprezentację łańcuchową (csv) przed wykonaniem porównania. W ten sposób domyślny komunikat Assertion testu będzie wyświetlał różnice w komunikacie o błędzie.

Stosowanie:

using Microsoft.VisualStudio.TestTools.UnitTesting;

// define collection1, collection2, ...

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

Pomocnicza metoda rozszerzenia:

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)));
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.