比较两个集合是否相等,无论其中项目的顺序如何
-
09-06-2019 - |
题
我想比较两个集合(在 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#),但是用您希望的任何语言给出答案都没关系。
笔记: 为了简单起见,我在示例中使用了整数,但我也希望能够使用引用类型对象(它们作为键的行为不正确,因为只比较对象的引用,而不比较内容)。
解决方案
事实证明,微软已经在其测试框架中涵盖了这一点: CollectionAssert.AreEquivalent
评论
如果两个集合具有相同数量的相同元素,但按任何顺序具有相同的元素。如果其值相等,则元素是相等的,如果它们指的是同一对象。
使用反射器,我修改了 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
其他提示
一个简单且相当有效的解决方案是对两个集合进行排序,然后比较它们是否相等:
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;
}
编辑:据我所知,这与最有效的算法处于同一顺序。该算法的时间复杂度为 O(N),假设字典使用 O(1) 查找。
这是我(深受 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;
}
}
编辑:当我提出这实际上只适用于集合时,我意识到它不能正确处理具有重复项目的集合。例如,从该算法的角度来看,{ 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
在没有重复和没有顺序的情况下,可以使用以下 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)。
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
命名空间。 据微软称, SymmetricExceptWith
是一个 O(n+m) 操作,与 n 表示第一组中的元素数量, 米 表示第二个元素的数量。如果需要,您可以随时向此函数添加一个相等比较器。
为什么不使用 .Exception()
// 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);
如果你使用 应该, ,您可以将 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
类似的重复帖子,但是 查看我的比较集合的解决方案. 。这很简单:
这将执行相等比较,无论顺序如何:
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
原帖 这里.
这是我的 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]);
}
这个问题有很多解决方案。如果您不关心重复项,则不必对两者进行排序。首先确保它们具有相同数量的项目。之后对其中一个集合进行排序。然后对已排序集合中第二个集合中的每个项目进行二进制搜索。如果没有找到给定的项目,则停止并返回 false。这件事的复杂性:- 对第一个集合进行排序:氮log(n) - 从第二个项目搜索每个项目:氮log(n),因此您最终会有2*n*log(n),假设它们匹配并查找所有内容。这类似于对两者进行排序的复杂性。此外,如果出现差异,这还可以让您提前停止。但是,请记住,如果在进行比较之前对两者进行排序,并且尝试使用 qsort 之类的方法进行排序,则排序的成本会更高。对此有一些优化。另一种选择是使用位掩码索引,这对于您知道元素范围的小型集合非常有用。这将为您带来 O(n) 的性能。另一种选择是使用哈希并查找它。对于小型集合,通常进行排序或位掩码索引要好得多。哈希表的缺点是局部性较差,因此请记住这一点。再说一遍,只有当您不关心重复项时才会出现这种情况。如果您想考虑重复项,请对两者进行排序。
在许多情况下,唯一合适的答案是 Igor Ostrovsky 之一,其他答案基于对象哈希码。但是,当您为对象生成哈希码时,您只能基于其 IMMUTABLE 字段(例如对象 Id 字段(如果是数据库实体))来生成哈希码。为什么在重写 Equals 方法时重写 GetHashCode 很重要?
这意味着,如果比较两个集合,即使不同项目的字段不相等,compare 方法的结果也可能为真。要深度比较集合,您需要使用 Igor 的方法并实现 IEqualirity 。
请阅读我和 Schnider 先生对他得票最多的帖子的评论。
詹姆士
允许重复 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;
}
这个简单的解决方案 迫使 IEnumerable
要实现的泛型类型 IComparable
. 。因为OrderBy
的定义。
如果您不想做出这样的假设但仍想使用此解决方案,您可以使用以下代码:
bool equal = collection1.OrderBy(i => i?.GetHashCode())
.SequenceEqual(collection2.OrderBy(i => i?.GetHashCode()));