Сравнение двух коллекций на равенство независимо от порядка элементов в них

Я хотел бы сравнить две коллекции (в C#), но я не уверен, что лучший способ это реализовать эффективно.

Я прочитал другую тему о перечисли.SequenceEqual, но это не совсем то, что я ищу.

в моем случае две коллекции будут равны, если они оба содержат одни и те же элементы (независимо от порядка).

пример:

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

collection1 == collection2; // true

что я обычно делаю, это цикл через каждый элемент одной коллекции и посмотрите, существует ли он в другой коллекции, затем выполните цикл через каждый элемент другой коллекции и посмотрите, существует ли он в первой коллекции. (Я начинаю с сравнения длин).

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

однако это не совсем правильно, и это, вероятно, не самый эффективный способ сравнить две коллекции для равенства.

пример, который я могу придумать, был бы неправильным:

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

что было бы равно с моей реализацией. Должен ли я просто считать? количество раз, когда каждый элемент найден и убедитесь, что количество равно в обеих коллекциях?


примеры находятся на каком-то C# (назовем его псевдо-C#), но дайте свой ответ на любом языке, который вы хотите, это не имеет значения.

Примечание: я использовал целые числа в примерах для простоты, но я хочу также использовать объекты ссылочного типа (они не ведут себя правильно как ключи, потому что сравнивается только ссылка объекта, а не содержание.)

17 ответов


оказывается, Microsoft уже имеет это в своей тестовой структуре:CollectionAssert.AreEquivalent

Примечания

две коллекции эквивалентны, если они иметь одни и те же элементы в одном и том же количество, но в любом порядке. Элементы равны, если их значения равны, нет, если они относятся к одному и тому же объекту.

используя reflector, я изменил код за AreEquivalent (), чтобы создать соответствующий компаратор равенства. Он более полон, чем существующие ответы, поскольку он учитывает нули, реализует IEqualityComparer и имеет некоторые проверки эффективности и крайнего случая. плюс, это 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;
    }
}

пример использования:

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

или если вы просто хотите сравнить две коллекции напрямую:

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

наконец, вы можете использовать свой компаратор равенства по вашему выбору:

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

простым и довольно эффективным решением является сортировка обеих коллекций, а затем сравнение их для равенства:

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

этот алгоритм O(N*logN), в то время как ваше решение выше O (N^2).

если коллекции имеют определенные свойства, вы можете реализовать более быстрое решение. Например, если обе коллекции являются хэш-наборами, они не могут содержать дубликатов. Кроме того, проверка того, содержит ли хэш-набор какой-либо элемент, очень быстрая. В этом случае алгоритм похожий на ваш, вероятно, будет самым быстрым.


создайте словарь "dict", а затем для каждого члена в первой коллекции сделайте dict[member]++;

затем повторите цикл над второй коллекцией таким же образом, но для каждого члена сделайте dict[member]--.

в конце, цикл над всеми членами в словаре:

    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;

    }

Edit: насколько я могу судить, это в том же порядке, что и самый эффективный алгоритм. Этот алгоритм является O(N), предполагая, что словарь использует O (1) поисков.


Это моя (под сильным влиянием Д. Дженнингса) общая реализация метода сравнения (в 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;
    }
}

вы могли бы использовать поиска HashSet. Посмотри SetEquals метод.


EDIT: я понял, как только я поставил, что это действительно работает только для наборов-он не будет должным образом иметь дело с коллекциями, которые имеют повторяющиеся элементы. Например, { 1, 1, 2 } и { 2, 2, 1 } Будем считать равными с точки зрения этого алгоритма. Если ваши коллекции являются наборами (или их равенство может быть измерено таким образом), однако, я надеюсь, что вы найдете ниже полезным.

решение, которое я использую:

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

Linq делает словарную вещь под обложками, поэтому это также O (N). (Обратите внимание, что это O (1), Если коллекции не имеют одинакового размера).

Я сделал проверку здравомыслия, используя метод "SetEqual", предложенный Даниэлем, метод OrderBy/SequenceEquals, предложенный Игорем, и мое предложение. Ниже приведены результаты, показывающие O(N*LogN) для Igor и O (N) для mine и Daniel's.

Я думаю, что простота кода пересечения Linq делает его предпочтительным решением.

__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

в случае отсутствия повторов и порядка для разрешения коллекций в качестве ключей словаря можно использовать следующий EqualityComparer:

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

здесь - это реализация ToHashSet (), которую я использовал. The хэш-кода алгоритм происходит от эффективной Java (через Джона Скита).


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

решение требует .NET 3.5 и System.Collections.Generic пространство имен. согласно Microsoft, SymmetricExceptWith это O (n + m) операции с n представляющее количество элементов в первом сете и m представление количества элементов во втором. При необходимости в эту функцию всегда можно добавить компаратор равенства.


почему бы не использовать .За исключением()

// 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


дубликат сообщения, но проверить мое решение для сравнения коллекций. Это довольно просто:

Это будет выполнять сравнение равенства независимо от порядка:

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

Это проверит, были ли добавлены / удалены элементы:

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

это увидит, какие элементы в словаре изменились:

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

оригинальное сообщение здесь.


Если вы используете Shouldly, вы можете использовать ShouldAllBe с Contains.

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

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

и, наконец, вы можете написать расширение.

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

обновление

необязательный параметр существует на ShouldBe метод.

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

Эриксон почти правильно: так как вы хотите соответствовать по количеству дубликатов, вы хотите мешок. В Java, это выглядит примерно так:

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

Я уверен, что C# имеет встроенную реализацию набора. Если производительность является проблемой, вы всегда можете использовать другую реализацию набора, но использовать тот же интерфейс набора.


вот мой вариант метода расширения ответа ohadsc, если он кому-то полезен

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

вот это решение, которое является улучшением по сравнению с этот.

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

существует множество решений этой проблемы. Если вы не заботитесь о дубликатах, вам не нужно сортировать оба. Сначала убедитесь, что у них одинаковое количество предметов. После этого-одна из коллекций. Затем binsearch каждый элемент из второй коллекции в отсортированной коллекции. Если вы не найдете данный элемент, остановите и верните false. Сложность этого: - сортировка первой коллекции: NLog(N) - поиск каждого элемента со второго на первый: N LOG(N) так что в итоге ты ... с 2*N * LOG (N) предполагая, что они совпадают, и вы просматриваете все. Это похоже на сложность сортировки обоих. Также это дает вам преимущество остановиться раньше, если есть разница. Однако имейте в виду, что если оба сортируются до того, как вы войдете в это сравнение, и вы попытаетесь Сортировать по использованию чего-то вроде qsort, сортировка будет дороже. Для этого есть оптимизация. Другой вариант, который отлично подходит для небольших коллекций, где вы знаете диапазон элементов использовать индекс битовой маски. Это даст вам o (n) производительность. Другой альтернативой является использование хэша и поиск его. Для небольших коллекций обычно намного лучше выполнять сортировку или индекс битовой маски. У Hashtable есть недостаток худшей локальности, поэтому имейте это в виду. Опять же, это только если вы не заботитесь о дубликатах. Если вы хотите учитывать дубликаты, перейдите к сортировке обоих.


во многих случаях единственным подходящим ответом является один из Игоря Островского, другие ответы основаны на хэш-коде объектов. Но когда вы генерируете хэш-код для объекта, вы делаете это только на основе его неизменяемых полей - таких как поле идентификатора объекта (в случае сущности базы данных) - почему важно переопределить GetHashCode при переопределении метода Equals?

Это означает , что при сравнении двух коллекций результат может быть верным для метода compare даже хотя поля различных элементов не равны . Для глубокого сравнения коллекций необходимо использовать метод Игоря и реализовать IEqualirity .

пожалуйста, прочитайте комментарии меня и mr.Schnider's on his most voted post.

Джеймс


С учетом дубликатов в IEnumerable<T> (если наборы нежелательны\возможны) и "игнорирование порядка" вы должны иметь возможность использовать .GroupBy().

Я не эксперт по измерениям сложности, но мое рудиментарное понимание заключается в том, что это должно быть O(n). Я понимаю O (n^2) как исходящий от выполнения операции O(n) внутри другой операции O(n), такой как ListA.Where(a => ListB.Contains(a)).ToList(). Каждый элемент в ListB оценивается на равенство с каждым элементом в ListA.

как я уже сказал, мой понимание сложности ограничено, поэтому исправьте меня, если я ошибаюсь.

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