我有课程:
class SomeClass
{
public string Name{get;set;}
public int SomeInt{get;set;}
}
class SomeComparison: IEqualityComparer<SomeClass>
{
public bool Equals(SomeClass s, SomeClass d)
{
return s.Name == d.Name;
}
public int GetHashCode(SomeClass a)
{
return (a.Name.GetHashCode() * 251);
}
}
我还有两个名为List<SomeClass>
和list1
list2
之前我曾经:
var q = (from a in list1
from b in list2
where a.Name != b.Name
select a).ToList();
并且执行大约需要1分钟。现在我有:
var q = list1.Except(list2,new SomeComparison()).ToList();
这需要不到1秒!
我想了解Except方法的作用。该方法是否为每个列表创建哈希表,然后执行相同的比较?如果我要进行大量的比较,我应该创建一个Hashtable吗?
现在我有两个HashSet<SomeClass>
名为hashSet1
和hashSet2
当我这样做时:
var q = (from a in hashSet1
form b in hashSet2
where a.Name != b.Name
select a).ToList();
仍然需要很长时间......我做错了什么?
答案 0 :(得分:19)
你的猜测很接近 - Linq to Objects Except
扩展方法在内部对传入的第二个序列使用HashSet<T>
- 允许它在迭代过程中查找O(1)中的元素第一个序列过滤掉第二个序列中包含的元素,因此整体努力是O(n + m),其中n和m是输入序列的长度 - 这是你可以希望做的最好的,因为你必须至少看一次每个元素。
有关如何实施此问题的评论,我推荐Jon Skeet的EduLinq系列,这是Except
实施的一部分以及full chapter的链接:
private static IEnumerable<TSource> ExceptImpl<TSource>(
IEnumerable<TSource> first,
IEnumerable<TSource> second,
IEqualityComparer<TSource> comparer)
{
HashSet<TSource> bannedElements = new HashSet<TSource>(second, comparer);
foreach (TSource item in first)
{
if (bannedElements.Add(item))
{
yield return item;
}
}
}
另一方面,您的第一个实现会将第一个列表中的每个元素与第二个列表中的每个元素进行比较 - 它正在执行cross product。这将需要n m操作,因此它将在O(n m)中运行 - 当n和m变大时,这变得非常快速地变慢。 (此解决方案也是错误的,因为它会创建重复的元素。)
答案 1 :(得分:2)
这两个代码示例不会产生相同的结果。
您的旧代码会创建两个列表中的Cartesian Product。
这意味着它会多次返回list1中的每个元素a
- 对于list2中不等于b
的每个元素a
一次。
使用“大”列表,这需要很长时间。
答案 2 :(得分:2)
from a in list1 from b in list2
会创建list1.Count * list2.Count
个元素,与list1.Except(list2)
不同!
如果list1
包含元素{ a, b, c, d }
和list2
元素{ a, b, c }
,那么您的第一个查询将产生以下对:
(a,a), (a,b), (a,c), (b,a), (b,b), (b,c), (c,a), (c,b), (c,c), (d,a), (d,b), (d,c)
因为您排除了相同的项目,结果将是
(a,a), (a,b), (a,c), (b,a),(b,b), (b,c), (c,a), (c,b),(c,c), (d,a), (d,b), (d,c)
因为你只选择了对中的第一个元素,你将得到
{ a, a, b, b, c, c, d, d, d }
第二个查询将产生{ a, b, c, d }
减去{ a, b, c }
,即{ d }
。
如果Exclude
中没有使用哈希表,则会导致使用O(m*n)
执行嵌套循环。使用哈希表,查询大致与O(n)
一起执行(忽略填充哈希表的成本)。
答案 3 :(得分:0)
这就是我的想法。
IEnumerable<T> Except<T>(IEnumerable<T> a,IEnumerable<T> b)
{
return a.Where(x => !b.Contains(x)).Distinct();
}
答案 4 :(得分:0)
在我看来,这会更有效率
private static IEnumerable<TSource> ExceptImpl<TSource>(
IEnumerable<TSource> first,
IEnumerable<TSource> second,
IEqualityComparer<TSource> comparer)
{
HashSet<TSource> bannedElements = new HashSet<TSource>(second, comparer);
foreach (TSource item in first)
{
if (!bannedElements.Contains(item))
{
yield return item;
}
}
}
包含是O(1)
添加是如果Count小于内部数组的容量,则此方法是O(1)操作。如果必须调整HashSet对象的大小,则此方法将成为O(n)操作,其中n为Count。