代码之家  ›  专栏  ›  技术社区  ›  mbillard

比较两个集合是否相等,而不考虑其中项的顺序

  •  147
  • mbillard  · 技术社区  · 16 年前

    我想比较两个集合(在C中),但我不确定如何有效地实现这一点。

    我读过另一篇关于 Enumerable.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),但是用你想要的任何语言给出你的答案,这并不重要。

    注: 为了简单起见,我在示例中使用了整数,但我也希望能够使用引用类型的对象(它们作为键的行为不正确,因为只比较对象的引用,而不是内容)。

    18 回复  |  直到 6 年前
        1
  •  105
  •   Ohad Schneider    7 年前

    事实证明,微软已经在其测试框架中涵盖了这一点: CollectionAssert.AreEquivalent

    评论

    如果两个集合 相同的元素 数量,但以任何顺序。元素 如果它们的值相等, 如果它们引用同一对象,则不会。

    使用Reflector,我修改了areequivalent()后面的代码,以创建相应的相等比较器。它比现有的答案更完整,因为它考虑了空值,实现了IEqualityComparer,并有一些效率和边缘案例检查。另外,它是 微软 :)

    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
    
        2
  •  88
  •   Sani Huttunen    11 年前

    一个简单且相当有效的解决方案是对两个集合进行排序,然后比较它们是否相等:

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

    这个算法是O(n*logn),而上面的解是O(n^2)。

    如果集合具有某些属性,则可以实现更快的解决方案。例如,如果两个集合都是哈希集,则它们不能包含重复项。此外,检查哈希集是否包含某些元素的速度非常快。在这种情况下,类似于您的算法可能是最快的。

        3
  •  31
  •   Daniel Jennings    16 年前

    创建字典“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;
    
        }
    

    编辑:据我所知,这与最有效的算法的顺序相同。该算法是O(n),假设字典使用O(1)查找。

        4
  •  18
  •   mbillard    16 年前

    这是我的(受到d.jennings的严重影响)比较方法的一般实现(在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;
        }
    }
    
        5
  •  10
  •   Joel Gauvreau    16 年前

    你可以使用 Hashset . 看看 SetEquals 方法。

        6
  •  5
  •   Schmidty    15 年前

    编辑:我一提出这只适用于集合就意识到了——它不能正确地处理具有重复项的集合。例如1、1、2和2、2、1从该算法的角度来看是相等的。但是,如果您的集合是集合(或者可以用这种方式测量它们的相等性),我希望您发现下面的内容有用。

    我使用的解决方案是:

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

    Linq在封面下做字典的事情,所以这也是O(N)。(注意,如果集合大小不同,则为O(1)。

    我使用丹尼尔建议的“setequal”方法、igor建议的orderby/sequenceequals方法和我的建议进行了一次健全性检查。结果如下,显示igor为O(n*logn),Mine和Daniel为O(n)。

    我认为Linq Intersect代码的简单性使它成为首选的解决方案。

    __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
    
        7
  •  5
  •   Community CDub    7 年前

    在没有重复和顺序的情况下,可以使用以下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;
        }
    }
    

    Here 是我使用的tohashset()实现。这个 hash code algorithm 来自有效的Java(通过乔恩SKET)。

        8
  •  4
  •   palswim    8 年前
    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 命名空间。 According to Microsoft , SymmetricExceptWith 是一个 o(n+m) 操作,与 n 表示第一组中的元素数和 表示第二个元素的数目。如果需要的话,可以向该函数添加一个相等比较器。

        9
  •  3
  •   Korayem Praphul Katlana    13 年前

    为什么不使用.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

        10
  •  3
  •   Pier-Lionel Sgard    7 年前

    如果你使用 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));
        }
    }
    

    更新

    上存在可选参数 应该是 方法。

    collection1.ShouldBe(collection2, ignoreOrder: true); // true
    
        11
  •  2
  •   Community CDub    7 年前

    erickson 几乎是正确的:因为您希望在重复计数上匹配,所以您需要 Bag . 在爪哇,这看起来像:

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

    我相信C有一个内置的集合实现。我将首先使用它;如果性能有问题,您可以始终使用不同的集合实现,但使用相同的集合接口。

        12
  •  2
  •   Community CDub    7 年前

    一个重复的帖子,但是 check out my solution for comparing collections . 很简单:

    这将执行相等比较,而不考虑顺序:

    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
    

    原帖 here .

        13
  •  1
  •   Eric J.    13 年前

    这是我对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;
        }
    }
    
        14
  •  1
  •   N73k    7 年前

    这是一个解决方案,它比 this one .

    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]);
        }
    
        15
  •  0
  •   Sasha    16 年前

    这个问题有很多解决办法。 如果你不在乎复制品,你就不必两者都排序。首先,确保它们具有相同数量的项目。在那之后是一个收藏。然后binsearch排序集合中第二个集合中的每个项。如果找不到给定的项目,请停止并返回false。 这一点的复杂性: -排序第一个集合:n 日志(n) -从第二个搜索到第一个:n 日志(n) 所以你最终得到2*n*log(n),假设它们是匹配的,你可以查找所有东西。这类似于对两者进行排序的复杂性。如果有差异,这也可以让您提前停止。 但是,请记住,如果在进行比较之前对两者都进行了排序,并且尝试使用类似qsort的方法进行排序,那么排序将更加昂贵。对此有一些优化。 另一种选择是,对于您知道元素范围的小集合来说,使用位掩码索引是很好的选择。这将给你一个O(N)的表现。 另一种选择是使用哈希并查找它。对于小的集合,进行排序或位掩码索引通常要好得多。hashtable的缺点是位置更差,所以请记住这一点。 再说一遍,只有当你不在乎重复的时候。如果您想说明重复项,请对两者进行排序。

        16
  •  0
  •   Community CDub    7 年前

    在许多情况下,唯一合适的答案是igor ostrovsky中的一个,其他答案是基于对象散列代码的。 但是,当您为一个对象生成哈希代码时,您这样做只是基于对象的不可变字段(例如对象ID字段(对于数据库实体))。 Why is it important to override GetHashCode when Equals method is overridden?

    这意味着,如果比较两个集合,即使不同项的字段不相等,比较方法的结果也可能为真。 要深入比较集合,需要使用igor方法并实现IEquality。

    请阅读我和施奈德先生在他投票最多的帖子上的评论。

    詹姆斯

        17
  •  0
  •   Josh Gust    6 年前

    允许在 IEnumerable<T> (如果集合不可取或不可能)并且“忽略顺序”,您应该能够使用 .GroupBy() .

    我不是复杂性度量方面的专家,但我的基本理解是这应该是O(N)。我理解O(n^2)来自于在另一个O(n)操作中执行O(n)操作,如 ListA.Where(a => ListB.Contains(a)).ToList() . 对列表B中的每个项进行相等性评估。

    如我所说,我对复杂性的理解是有限的,所以如果我错了,请纠正我的看法。

    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;
        }
    
        18
  •  0
  •   Jo Ham Masood Khaari    6 年前

    This simple solution 迫使 IEnumerable 要实现的泛型类型 IComparable . 因为 OrderBy 的定义。

    如果您不想做这样的假设,但仍然想使用这个解决方案,您可以使用下面的代码:

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