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

Comparing two collections for equality irrespective of the order of items in them

提问人:mbillard 提问时间:9/9/2008 最后编辑:Communitymbillard 更新时间:6/29/2022 访问量:115666

问:

我想比较两个集合(在 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#),但用您想要的任何语言给出答案都无关紧要。

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

.NET 集合 比较 相等

评论

1赞 Nuri YILMAZ 6/8/2012
算法怎么样?所有答案都通过比较某物、通用列表比较 linq 等相关。我们真的向某人承诺过,我们永远不会像老式程序员那样使用算法吗?
0赞 CAD bloke 3/24/2015
您不是在检查相等性,而是在检查等价性。这是吹毛求疵的,但这是一个重要的区别。很久以前。这是一个很好的Q+A。
0赞 ChaseMedallion 1/4/2016
您可能对这篇文章感兴趣,它讨论了下面描述的基于字典的方法的优化版本。大多数简单的字典方法的一个问题是它们不能正确处理空值,因为 .NET 的 Dictionary 类不允许 null 键。

答:

34赞 Daniel Jennings 9/9/2008 #1

创建一个字典“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),假设 Dictionary 使用 O(1) 查找。

评论

0赞 mbillard 9/9/2008
这几乎是我想要的。但是,即使我不使用整数,我也希望能够做到这一点。我想使用引用对象,但它们在字典中不能正确地充当键。
0赞 skolima 9/16/2008
Mono,如果您的物品没有可比性,您的问题就没有意义了。如果它们不能用作 Dictionary 中的键,则没有可用的解决方案。
1赞 erickson 10/1/2008
我认为 Mono 意味着键不可排序。但 Daniel 的解决方案显然是打算用哈希表而不是树来实现的,只要有等价测试和哈希函数,它就会起作用。
0赞 mbillard 1/7/2009
当然,对帮助投了赞成票,但没有被接受,因为它缺少一个重要的点(我在回答中介绍了这一点)。
1赞 Tyson Williams 2/26/2016
FWIW,您可以使用以下命令简化最后一个 foreach 循环和 return 语句:return dict.All(kvp => kvp.Value == 0);
0赞 James A. Rosen 9/9/2008 #2

埃里克森几乎是对的:既然你想匹配重复的数量,你就需要一个。在 Java 中,这看起来像这样:

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

我确定 C# 有一个内置的 Set 实现。我会先使用它;如果性能有问题,则始终可以使用不同的 Set 实现,但使用相同的 Set 接口。

109赞 Igor Ostrovsky 9/9/2008 #3

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

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

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

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

评论

1赞 Junior Mayhé 5/22/2010
您只需要添加一个 using System.Linq;第一个让它工作的人
0赞 Junior Mayhé 5/22/2010
如果此代码位于循环中,并且 Collection1 已更新,而 Collection2 保持不变,请注意,即使两个集合具有相同的对象,调试器也会为此“相等”变量显示 false。
6赞 Brett 5/29/2014
@Chaulky - 我相信 OrderBy 是需要的。请参见:dotnetfiddle.net/jA8iwE
1赞 StayOnTarget 12/12/2019
另一个被称为“上面”的答案是什么?可能 stackoverflow.com/a/50465/3195477
0赞 Reyhn 5/25/2023
请注意仔细选择比较。例如,如果两个元素相对相同(但对象不同),则元素可能根本不会重新排序。
17赞 mbillard 9/9/2008 #4

这是我(深受 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;
    }
}

评论

12赞 Ohad Schneider 4/2/2010
干得不错,但请注意: 1.与 Daniel Jennings 解决方案相比,这不是 O(N) 而是 O(N^2),因为 bar 集合的 foreach 循环内的 find 函数;2. 可以将方法推广为接受 IEnumerable<T>而不是 ICollection<T>而无需对代码进行进一步修改
0赞 Antonín Lejsek 12/21/2018
The keys of a dictionary are compared by reference, so we have to find the original key that is equivalent to the "item"- 这不是真的。该算法基于错误的假设,虽然有效,但效率非常低。
13赞 Joel Gauvreau 10/4/2008 #5

您可以使用 Hashset。查看 SetEquals 方法。

评论

3赞 Mark Cidade 10/4/2008
当然,使用 HashSet 假定没有重复项,但如果是这样,HashSet 是最好的方法
0赞 Sean Werkema 9/5/2023
正如现在内置在 Linq 中一样,该解决方案可以编写为非常紧凑、高效的单行代码:。这不支持重复,但它很容易成为最短的答案,它不需要外部库,甚至在摊销的 O(n) 时间内运行。ToHashSet()SetEquals()collection1.ToHashSet().SetEquals(collection2)
0赞 Sasha 10/12/2008 #6

这个问题有很多解决方案。 如果您不关心重复项,则不必对两者进行排序。首先,确保它们具有相同数量的项目。在那之后,对其中一个集合进行排序。然后,对排序集合中第二个集合中的每个项目进行 binsearch 。如果找不到给定项目,请停止并返回 false。 其复杂性: - 对第一个集合进行排序:NLog(N) - 从第二个到第一个搜索每个项目:N LOG(N) 因此,假设它们匹配并查找所有内容,您最终会得到 2*N*LOG(N)。这与对两者进行排序的复杂性类似。此外,如果存在差异,这为您提供了更早停止的好处。 但是,请记住,如果在进行此比较之前对两者都进行了排序,并且您尝试使用类似 qsort 之类的东西进行排序,则排序将更加昂贵。对此进行了优化。 另一种选择是使用位掩码索引,这对于您知道元素范围的小型集合非常有用。这将为您提供 O(n) 性能。 另一种选择是使用哈希并查找它。对于小型集合,通常最好进行排序或位掩码索引。哈希表的缺点是位置性较差,因此请记住这一点。 同样,只有在您不关心重复的情况下。如果要考虑重复项,请对两者进行排序。

7赞 Schmidty 6/19/2009 #7

编辑:当我提出这个问题时,我就意识到这实际上只适用于集合 - 它不能正确处理具有重复项目的集合。例如,从此算法的角度来看,{ 1, 1, 2 } 和 { 2, 2, 1 } 将被视为相等。但是,如果您的集合是集合(或者它们的相等性可以这样衡量),我希望您发现以下内容有用。

我使用的解决方案是:

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

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

我使用 Daniel 建议的“SetEqual”方法、Igor 建议的 OrderBy/SequenceEquals 方法和我的建议进行了健全性检查。结果如下,显示 Igor 的 O(N*LogN) 和我和 Daniel 的 O(N)。

我认为 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

评论

0赞 mbillard 6/19/2009
此代码的唯一问题是,它仅在比较值类型或将指针与引用类型进行比较时才有效。集合中可以有两个同一对象的不同实例,因此我需要能够指定如何比较每个实例。您可以将比较委托传递给 intersect 方法吗?
0赞 6/19/2009
当然,您可以传递比较器委托。但是,请注意上述关于我添加的集合的限制,这大大限制了其适用性。
0赞 Griffin 8/13/2016
Intersect 方法返回一个不同的集合。给定 a = {1,1,2} 和 b ={2,2,1},a.Intersect(b)。Count() != a.Count,这会导致表达式正确返回 false。{1,2}.计数 != {1,1,2}。计数 参见 link[/link] (请注意,在比较之前,两边是分开的。
5赞 Ohad Schneider 4/2/2010 #8

在没有重复和无顺序的情况下,可以使用以下 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() 实现。哈希码算法来自 Effective Java(通过 Jon Skeet)。

评论

0赞 nawfal 6/25/2015
Serializable for Comparer 类的意义何在?:o您也可以将输入更改为表示它适用于集合(即没有重复)。ISet<T>
0赞 Ohad Schneider 6/25/2015
@nawfal谢谢,不知道当我将其标记为可序列化时我在想什么......至于,这里的想法是将 视为一个集合(因为你一开始就有一个),尽管考虑到 0 年多来的 5 个赞成票,这可能不是最尖锐的想法:PISetIEnumerableIEnumerable
2赞 user329244 4/30/2010 #9

各种重复的帖子,但请查看我的比较集合的解决方案。这很简单:

无论顺序如何,这将执行相等性比较:

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

原帖在这里

129赞 Ohad Schneider 9/25/2010 #10

事实证明,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)
            hash ^= (val == null ? 42 : m_comparer.GetHashCode(val));

        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

评论

9赞 Ian Dallas 8/10/2012
我不是100%确定,但我认为您的答案违反了Microsoft针对逆向工程的使用条款。
1赞 James Roeiter 9/2/2013
你好奥哈德,请阅读以下该主题的长篇辩论,stackoverflow.com/questions/371328/...如果更改对象哈希码,当它在哈希集中时,它将中断哈希集的正确操作,并可能导致异常。规则如下:如果两个对象相等 - 它们必须具有相同的哈希码。如果两个对象具有相同的哈希码 - 它们不必相等。哈希码必须在对象的整个生命周期内保持不变!这就是为什么你推动 ICompareable 和 IEqualrity 的原因。
3赞 Ohad Schneider 9/2/2013
@JamesRoeiter 也许我的评论具有误导性。当字典遇到它已经包含的哈希码时,它会检查与 (您提供的哈希码或 ,您可以检查 Reflector 或引用源来验证这一点)的实际相等性。诚然,如果对象在此方法运行时发生变化(特别是它们的哈希码更改),则结果是意外的,但这只是意味着此方法在此上下文中不是线程安全的。EqualityComparerEqualityComparer.Default
2赞 Ohad Schneider 9/11/2013
@JamesRoeiter 假设 x 和 y 是我们要比较的两个对象。如果它们具有不同的哈希码,我们知道它们是不同的(因为相同的项目具有相同的哈希码),并且上述实现是正确的。如果它们具有相同的哈希码,则字典实现将使用指定的(或者如果没有指定)检查实际相等性,并且实现再次正确。EqualityComparerEqualityComparer.Default
1赞 Ohad Schneider 3/24/2015
@CADbloke由于接口的原因,必须对方法进行命名。您应该查看的是比较器本身的名称。在这种情况下,这是有道理的。EqualsIEqualityComparer<T>MultiSetComparer
4赞 Korayem 8/9/2011 #11

为什么不使用 .除了()

// 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赞 Cristian Diaconescu 1/31/2013
Except不适用于计算重复项目。对于集合 {1,2,2} 和 {1,1,2},它将返回 true。
0赞 Korayem 4/12/2018
@CristiDiaconescu你可以做一个“.Distinct()“删除任何重复项
0赞 Cristian Diaconescu 4/15/2018
OP 要求 .使用会使它们看起来平等。[1, 1, 2] != [1, 2, 2]Distinct
1赞 Eric J. 3/12/2012 #12

这是我的 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;
    }
}

评论

0赞 nawfal 11/9/2013
这表现如何,有什么想法吗?
0赞 Eric J. 11/9/2013
我只将其用于小型集合,因此没有考虑过 Big-O 的复杂性或进行基准测试。HaveMismatchedElements 本身是 O(M*N),因此对于大型集合,它可能表现不佳。
0赞 nawfal 6/28/2015
如果 s 是查询,那么调用不是一个好主意。奥哈德最初的答案是检查它们是否是更好的主意。IEnumerable<T>Count()ICollection<T>
0赞 James Roeiter 8/31/2013 #13

在许多情况下,唯一合适的答案是伊戈尔·奥斯特洛夫斯基(Igor Ostrovsky)的答案,其他答案基于对象哈希代码。 但是,当您为对象生成哈希代码时,您仅基于他的 IMMUTABLE 字段(例如对象 Id 字段(如果是数据库实体))来生成哈希代码,为什么在重写 Equals 方法时重写 GetHashCode 很重要?

这意味着,如果你比较两个集合,即使不同项目的字段不相等,比较方法的结果也可能是正确的。 要深度比较集合,需要使用 Igor 的方法并实现 IEqualirity。

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

詹姆斯

8赞 palswim 6/24/2016 #14
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 和命名空间。根据 Microsoft 的说法,是一个 O(n + m) 操作,其中 n 表示第一组中的元素数,m 表示第二组中的元素数。如有必要,您可以随时向此函数添加相等比较器。System.Collections.GenericSymmetricExceptWith

评论

0赞 Emmanuel DURIN 3/25/2021
有趣而罕见的事实。谢谢你的知识
0赞 Mick Byrne 8/16/2021
这里的最佳答案,简洁、正确、快速。应该被点赞。
1赞 N73k 5/30/2017 #15

这是一个解决方案,它比这个解决方案有所改进。

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]);
    }
8赞 Pier-Lionel Sgard 11/14/2017 #16

如果使用 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

评论

1赞 Pier-Lionel Sgard 11/14/2017
我刚刚在最新版本上发现 ShouldBe 方法上有一个参数。bool ignoreOrder
0赞 Lesair Valmont 11/24/2021
对 Shouldly 的精彩引用。
0赞 Josh Gust 8/10/2018 #17

允许重复(如果集合不可取\可能)和“忽略顺序”,您应该能够使用 .IEnumerable<T>.GroupBy()

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

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

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;
    }
2赞 Jo Ham 12/20/2018 #18

此简单解决方案强制 IEnumerable 的泛型类型实现 IComparable。因为 OrderBy 的定义。

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

bool equal = collection1.OrderBy(i => i?.GetHashCode())
   .SequenceEqual(collection2.OrderBy(i => i?.GetHashCode()));
0赞 crokusek 8/27/2019 #19

如果出于单元测试断言的目的进行比较,那么在进行比较之前,将一些效率抛在窗外并简单地将每个列表转换为字符串表示形式 (csv) 可能是有意义的。这样,默认的测试断言消息将显示错误消息中的差异。

用法:

using Microsoft.VisualStudio.TestTools.UnitTesting;

// define collection1, collection2, ...

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

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)));
}
1赞 xhafan 5/11/2021 #20

根据这个重复问题的答案,以及答案下面的评论,以及@brian-genisio的答案,我想出了这些:

        public static bool AreEquivalentIgnoringDuplicates<T>(this IEnumerable<T> items, IEnumerable<T> otherItems)
        {
            var itemList = items.ToList();
            var otherItemList = otherItems.ToList();
            var except = itemList.Except(otherItemList);
            return itemList.Count == otherItemList.Count && except.IsEmpty();
        }

        public static bool AreEquivalent<T>(this IEnumerable<T> items, IEnumerable<T> otherItems)
        {
            var itemList = items.ToList();
            var otherItemList = otherItems.ToList();
            var except = itemList.Except(otherItemList);
            return itemList.Distinct().Count() == otherItemList.Count && except.IsEmpty();
        }

针对这两项的测试:

        [Test]
        public void collection_with_duplicates_are_equivalent()
        {
            var a = new[] {1, 5, 5};
            var b = new[] {1, 1, 5};

            a.AreEquivalentIgnoringDuplicates(b).ShouldBe(true); 
        }

        [Test]
        public void collection_with_duplicates_are_not_equivalent()
        {
            var a = new[] {1, 5, 5};
            var b = new[] {1, 1, 5};

            a.AreEquivalent(b).ShouldBe(false); 
        }
0赞 Adam Simon 6/29/2022 #21

这是我对这个问题的抨击。它基于这种策略,但也从公认的答案中借鉴了一些想法。

public static class EnumerableExtensions
{
    public static bool SequenceEqualUnordered<TSource>(this IEnumerable<TSource> source, IEnumerable<TSource> second)
    {
        return SequenceEqualUnordered(source, second, EqualityComparer<TSource>.Default);
    }
   
    public static bool SequenceEqualUnordered<TSource>(this IEnumerable<TSource> source, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));

        if (second == null)
            throw new ArgumentNullException(nameof(second));

        if (source.TryGetCount(out int firstCount) && second.TryGetCount(out int secondCount))
        {
            if (firstCount != secondCount)
                return false;

            if (firstCount == 0)
                return true;
        }

        IEqualityComparer<ValueTuple<TSource>> wrapperComparer = comparer != null ? new WrappedItemComparer<TSource>(comparer) : null;

        Dictionary<ValueTuple<TSource>, int> counters;
        ValueTuple<TSource> key;
        int counter;

        using (IEnumerator<TSource> enumerator = source.GetEnumerator())
        {
            if (!enumerator.MoveNext())
                return !second.Any();

            counters = new Dictionary<ValueTuple<TSource>, int>(wrapperComparer);

            do
            {
                key = new ValueTuple<TSource>(enumerator.Current);

                if (counters.TryGetValue(key, out counter))
                    counters[key] = counter + 1;
                else
                    counters.Add(key, 1);
            }
            while (enumerator.MoveNext());
        }

        foreach (TSource item in second)
        {
            key = new ValueTuple<TSource>(item);

            if (counters.TryGetValue(key, out counter))
            {
                if (counter <= 0)
                    return false;

                counters[key] = counter - 1;
            }
            else
                return false;
        }

        return counters.Values.All(cnt => cnt == 0);
    }

    private static bool TryGetCount<TSource>(this IEnumerable<TSource> source, out int count)
    {
        switch (source)
        {
            case ICollection<TSource> collection:
                count = collection.Count;
                return true;
            case IReadOnlyCollection<TSource> readOnlyCollection:
                count = readOnlyCollection.Count;
                return true;
            case ICollection nonGenericCollection:
                count = nonGenericCollection.Count;
                return true;
            default:
                count = default;
                return false;
        }
    }

    private sealed class WrappedItemComparer<TSource> : IEqualityComparer<ValueTuple<TSource>>
    {
        private readonly IEqualityComparer<TSource> _comparer;

        public WrappedItemComparer(IEqualityComparer<TSource> comparer)
        {
            _comparer = comparer;
        }

        public bool Equals(ValueTuple<TSource> x, ValueTuple<TSource> y) => _comparer.Equals(x.Item1, y.Item1);

        public int GetHashCode(ValueTuple<TSource> obj) => _comparer.GetHashCode(obj.Item1);
    }
}

MS 解决方案的改进:

  • 不走捷径,因为它有点值得商榷。例如,考虑一个自定义项,其实现如下: .ReferenceEquals(first, second)IEnumerable<T>public IEnumerator<T> GetEnumerator() => Enumerable.Repeat(default(T), new Random().Next(10)).GetEnumerator()
  • 当两个可枚举对象都是集合时,采用可能的快捷方式,但不仅检查其他集合接口,还检查其他集合接口。ICollection<T>
  • 正确处理 null 值。将 null 值与其他(非 null)值分开计数看起来也不是 100% 的故障安全。考虑一个自定义相等比较器,它以非标准方式处理 null 值。

此解决方案也在我的实用工具 NuGet 包中提供。