我刚刚发现Except()
会从第一个列表中删除所有在第二个列表中的元素,但它也使返回结果中的所有元素变得独特。
我使用的简单方法是Where(v => !secondList.Contains(v))
有人可以解释一下这种行为的原因,并如果可能的话,指导我去哪里查找相关文档吗?
当你这样做时,仍然会使用我正在使用的简单方法是
Where(v => !secondList.Contains(v))
secondList
进行 Distinct。var firstStrings = new [] { "1", null, null, null, "3", "3" };
var secondStrings = new [] { "1", "1", "1", null, null, "4" };
var resultStrings = firstStrings.Where(v => !secondStrings.Contains(v)); // 3, 3
我创建了一个扩展方法,使得不需要去重。使用示例:
var result2Strings = firstStrings.ExceptAll(secondStrings).ToList(); // null, 3, 3
这是它的功能:
这是源代码:
public static IEnumerable<TSource> ExceptAll<TSource>(
this IEnumerable<TSource> first,
IEnumerable<TSource> second)
{
// Do not call reuse the overload method because that is a slower imlementation
if (first == null) { throw new ArgumentNullException("first"); }
if (second == null) { throw new ArgumentNullException("second"); }
var secondList = second.ToList();
return first.Where(s => !secondList.Remove(s));
}
public static IEnumerable<TSource> ExceptAll<TSource>(
this IEnumerable<TSource> first,
IEnumerable<TSource> second,
IEqualityComparer<TSource> comparer)
{
if (first == null) { throw new ArgumentNullException("first"); }
if (second == null) { throw new ArgumentNullException("second"); }
var comparerUsed = comparer ?? EqualityComparer<TSource>.Default;
var secondList = second.ToList();
foreach (var item in first)
{
if (secondList.Contains(item, comparerUsed))
{
secondList.Remove(item);
}
else
{
yield return item;
}
}
}
编辑:根据DigEmAll的评论,有更快的实现方式
public static IEnumerable<TSource> ExceptAll<TSource>(
this IEnumerable<TSource> first,
IEnumerable<TSource> second,
IEqualityComparer<TSource> comparer = null)
{
if (first == null) { throw new ArgumentNullException(nameof(first)); }
if (second == null) { throw new ArgumentNullException(nameof(second)); }
var secondCounts = new Dictionary<TSource, int>(comparer ?? EqualityComparer<TSource>.Default);
int count;
int nullCount = 0;
// Count the values from second
foreach (var item in second)
{
if (item == null)
{
nullCount++;
}
else
{
if (secondCounts.TryGetValue(item, out count))
{
secondCounts[item] = count + 1;
}
else
{
secondCounts.Add(item, 1);
}
}
}
// Yield the values from first
foreach (var item in first)
{
if (item == null)
{
nullCount--;
if (nullCount < 0)
{
yield return item;
}
}
else
{
if (secondCounts.TryGetValue(item, out count))
{
if (count == 0)
{
secondCounts.Remove(item);
yield return item;
}
else
{
secondCounts[item] = count - 1;
}
}
else
{
yield return item;
}
}
}
}
更多信息请参见我的博客(还有Intersect和Union的变体)
Contains
和Remove
都是O(n)操作,并且你正在循环中执行它们。 - MagnusExcept
实现,他不能使用哈希集合,因为这样会消除第二个列表中的重复项。不过仍然有可能大大提高复杂度,例如通过构建一个字典,记录每个项目的出现次数,并减少其出现次数而不是从列表中删除该项目... - digEmAllExcept
方法的作用及原因。简而言之,您正在回答与所问无关的问题。 - Servy给定 A = [1, 2, 2, 3, 3, 3]
和 B = [3]
。
A.Except(B);
返回 [1, 2]
,正如 Greg Beech 在 他的回答 中所解释的。A.ExceptAll(B);
来自 Alex Siepman 的回答,返回 [1, 2, 2, 3, 3]
(我认为名称不够清晰)。A.Where(v => !B.Contains(v))
是 OP 的解决方法,返回 [1, 2, 2]
我认为 OP 的解决方法是期望的行为,并且这个还没有得到处理。
OP工作方式的主要问题在于List<T>.Contains(T)
是O(n)
,而Where
也是O(n)
,使得解决方案在时间上为O(n²)
(对于等效大小的A和B),而在内存上为O(1)
。我们可以通过使用哈希集将其优化为O(n)
时间复杂度和O(n)
空间复杂度:// I accept any better name for this method
public static IEnumerable<TSource> ExceptFrom<TSource>(
IEnumerable<TSource> first,
IEnumerable<TSource> second,
IEqualityComparer<TSource> comparer)
{
if (first == null)
throw new ArgumentNullException(nameof(first));
if (second == null)
throw new ArgumentNullException(nameof(second));
var secondSet = second as HashSet<TSource> ?? // this trick ignore the comparer
second.ToHashSet(comparer ?? EqualityComparer<TSource>.Default);
// Contains is O(1) for HashSet.
return first.Where(v => !secondSet.Contains(v));
}
IEnumerable<T>
是一个序列而不是一个集合,所以将一个通用的扩展方法用于序列并具有集合语义有些奇怪。但话又说回来,我想从IEnumerable<T>
中拥有一个具有序列语义的Except
方法和从ISet<T>
中拥有一个具有集合语义的Except
方法甚至更糟糕,因为ISet<T>
继承自IEnumerable<T>
,因此语义将取决于编译器如何绑定扩展方法。 - Greg BeechExcept
的输出按任何特定顺序排列,因为文档没有说明它会按顺序排列,并且暗示它不会这样做。 - Greg Beech