比较两个通用列表差异的最快方法

比较两个庞大(>50.000项)的最快(和最少资源密集型)的方法是什么,从而得到如下所示的两个列表:

  1. 在第一个列表中出现但在第二个列表中没有出现的项目
  2. 出现在第二个列表中但不在第一个列表中的项目

目前,我正在使用列表或IReadOnlyCollection,并在linq查询中解决这个问题:

var list1 = list.Where(i => !list2.Contains(i)).ToList();
var list2 = list2.Where(i => !list.Contains(i)).ToList();

但是这并不像我想的那样好。 有什么想法使这更快和更少的资源密集,因为我需要处理很多列表?< / p >

518165 次浏览

使用Except:

var firstNotSecond = list1.Except(list2).ToList();
var secondNotFirst = list2.Except(list1).ToList();

我怀疑有一些方法实际上会比这个略快,但即使是这个方法也会比你的O(N * M)方法快大大

如果你想把它们结合起来,你可以用上面的方法创建一个方法,然后创建一个return语句:

return !firstNotSecond.Any() && !secondNotFirst.Any();

需要注意的一点是,在问题中的原始代码和这里的解决方案之间存在的结果差异:任何只在一个列表中的重复元素将只在我的代码中报告一次,而它们将在原始代码中出现多次报告。

例如,对于[1, 2, 2, 2, 3][1]的列表,原始代码中的“元素在list1但不是list2”的结果将是[2, 2, 2, 3]。对于我的代码,它将只是[2, 3]。在许多情况下,这不是一个问题,但这是值得注意的。

使用Enumerable.Except会更有效:

var inListButNotInList2 = list.Except(list2);
var inList2ButNotInList = list2.Except(list);

该方法是通过使用延迟执行实现的。这意味着你可以这样写:

var first10 = inListButNotInList2.Take(10);

它也是有效的,因为它在内部使用Set<T>来比较对象。它的工作原理是首先从第二个序列中收集所有不同的值,然后将第一个序列的结果流式传输,检查它们是否之前没有出现过。

试试这个方法:

var difList = list1.Where(a => !list2.Any(a1 => a1.id == a.id))
.Union(list2.Where(a => !list1.Any(a1 => a1.id == a.id)));

不是针对这个问题,但是这里有一些代码来比较相等和不相等的列表!相同的对象:

public class EquatableList<T> : List<T>, IEquatable<EquatableList<T>> where    T : IEquatable<T>


/// <summary>
/// True, if this contains element with equal property-values
/// </summary>
/// <param name="element">element of Type T</param>
/// <returns>True, if this contains element</returns>
public new Boolean Contains(T element)
{
return this.Any(t => t.Equals(element));
}


/// <summary>
/// True, if list is equal to this
/// </summary>
/// <param name="list">list</param>
/// <returns>True, if instance equals list</returns>
public Boolean Equals(EquatableList<T> list)
{
if (list == null) return false;
return this.All(list.Contains) && list.All(this.Contains);
}

如果你想让结果是不分大小写,下面的方法可以工作:

List<string> list1 = new List<string> { "a.dll", "b1.dll" };
List<string> list2 = new List<string> { "A.dll", "b2.dll" };


var firstNotSecond = list1.Except(list2, StringComparer.OrdinalIgnoreCase).ToList();
var secondNotFirst = list2.Except(list1, StringComparer.OrdinalIgnoreCase).ToList();

firstNotSecond将包含b1.dll

secondNotFirst将包含b2.dll

我已经使用这段代码来比较两个有百万记录的列表。

这种方法不会花很多时间

    //Method to compare two list of string
private List<string> Contains(List<string> list1, List<string> list2)
{
List<string> result = new List<string>();


result.AddRange(list1.Except(list2, StringComparer.OrdinalIgnoreCase));
result.AddRange(list2.Except(list1, StringComparer.OrdinalIgnoreCase));


return result;
}

也许这很有趣,但这对我来说很管用:

string.Join("",List1) != string.Join("", List2)

这是你能找到的最好的解决办法

var list3 = list1.Where(l => list2.ToList().Contains(l));

如果只需要组合结果,这也可以工作:

var set1 = new HashSet<T>(list1);
var set2 = new HashSet<T>(list2);
var areEqual = set1.SetEquals(set2);

其中T是列表元素的类型。

using System.Collections.Generic;
using System.Linq;


namespace YourProject.Extensions
{
public static class ListExtensions
{
public static bool SetwiseEquivalentTo<T>(this List<T> list, List<T> other)
where T: IEquatable<T>
{
if (list.Except(other).Any())
return false;
if (other.Except(list).Any())
return false;
return true;
}
}
}

有时候你只需要知道如果两个列表是不同的,而不需要知道这些差异是什么。在这种情况下,考虑将此扩展方法添加到项目中。注意,你列出的对象应该实现IEquatable!

用法:

public sealed class Car : IEquatable<Car>
{
public Price Price { get; }
public List<Component> Components { get; }


...
public override bool Equals(object obj)
=> obj is Car other && Equals(other);


public bool Equals(Car other)
=> Price == other.Price
&& Components.SetwiseEquivalentTo(other.Components);


public override int GetHashCode()
=> Components.Aggregate(
Price.GetHashCode(),
(code, next) => code ^ next.GetHashCode()); // Bitwise XOR
}

无论Component类是什么,这里为Car显示的方法应该几乎相同地实现。

注意我们如何编写GetHashCode是非常重要的。为了正确地实现IEquatableEqualsGetHashCode 必须以逻辑兼容的方式操作实例的属性。

两个具有相同内容的列表仍然是不同的对象,并且将产生不同的哈希码。由于我们希望这两个列表被同等对待,所以必须让GetHashCode为它们各自产生相同的值。我们可以通过将hashcode委托给列表中的每个元素,并使用标准的逐位异或来组合它们来实现这一点。XOR是顺序不可知的,所以如果列表排序不同并不重要。重要的是它们只包含相同的成员。

注意:这个奇怪的名字是为了暗示这个方法不考虑列表中元素的顺序。如果您确实关心列表中元素的顺序,则此方法不适合您!

我认为这是一个简单易行的方法来逐个元素比较两个列表

x=[1,2,3,5,4,8,7,11,12,45,96,25]
y=[2,4,5,6,8,7,88,9,6,55,44,23]


tmp = []




for i in range(len(x)) and range(len(y)):
if x[i]>y[i]:
tmp.append(1)
else:
tmp.append(0)
print(tmp)

< >强可点数的。SequenceEqual方法< / >强

根据相等比较器确定两个序列是否相等。 MS.Docs < / p >

Enumerable.SequenceEqual(list1, list2);

这适用于所有基本数据类型。如果你需要在自定义对象上使用它,你需要实现IEqualityComparer

定义方法以支持相等的对象比较。

< >强IEqualityComparer界面< / >强

定义方法以支持相等的对象比较。 MS.Docs for IEqualityComparer < / p >

虽然Jon Skeet的答案对于每天使用少量或中等数量的元素(最多数百万)的练习是一个很好的建议,但它不是最快的方法,也不是很有效的资源。一个明显的缺点是,要得到完整的差值需要对数据进行两次传递(如果同样感兴趣的是相等的元素,则甚至需要进行三次传递)。显然,这可以通过自定义的Except方法的重新实现来避免,但创建哈希集仍然需要大量内存,并且哈希的计算需要时间。

对于非常大的数据集(数十亿个元素),考虑特定的情况通常是值得的。这里有一些想法可能会给你一些启发: 如果元素可以比较(在实践中几乎总是这样),那么对列表进行排序并应用以下zip方法是值得考虑的
/// <returns>The elements of the specified (ascendingly) sorted enumerations that are
/// contained only in one of them, together with an indicator,
/// whether the element is contained in the reference enumeration (-1)
/// or in the difference enumeration (+1).</returns>
public static IEnumerable<Tuple<T, int>> FindDifferences<T>(IEnumerable<T> sortedReferenceObjects,
IEnumerable<T> sortedDifferenceObjects, IComparer<T> comparer)
{
var refs  = sortedReferenceObjects.GetEnumerator();
var diffs = sortedDifferenceObjects.GetEnumerator();
bool hasNext = refs.MoveNext() && diffs.MoveNext();
while (hasNext)
{
int comparison = comparer.Compare(refs.Current, diffs.Current);
if (comparison == 0)
{
// insert code that emits the current element if equal elements should be kept
hasNext = refs.MoveNext() && diffs.MoveNext();


}
else if (comparison < 0)
{
yield return Tuple.Create(refs.Current, -1);
hasNext = refs.MoveNext();
}
else
{
yield return Tuple.Create(diffs.Current, 1);
hasNext = diffs.MoveNext();
}
}
}

例如,它可以以以下方式使用:

const int N = <Large number>;
const int omit1 = 231567;
const int omit2 = 589932;
IEnumerable<int> numberSequence1 = Enumerable.Range(0, N).Select(i => i < omit1 ? i : i + 1);
IEnumerable<int> numberSequence2 = Enumerable.Range(0, N).Select(i => i < omit2 ? i : i + 1);
var numberDiffs = FindDifferences(numberSequence1, numberSequence2, Comparer<int>.Default);

在我的计算机上对N = 1M进行基准测试,得到以下结果:

< span style=" font - family:宋体;">是< / th > < span style=" font - family:宋体;"> < / th >错误 < span style=" font - family:宋体;"> StdDev < / th > < span style=" font - family:宋体;"> < / th >比例 < span style=" font - family:宋体;"> Gen 0 < / th > < span style=" font - family:宋体;">创1 < / th > < span style=" font - family:宋体;"> < / th >第2代 < span style=" font - family:宋体;"> < / th >分配 . . . . < span style=" font - family:宋体;"道明> > 67110744 B < / . . < span style=" font - family:宋体;"> - < / td > < span style=" font - family:宋体;"> - < / td > < span style=" font - family:宋体;"> - < / td > < span style=" font - family:宋体;"道明> > 720 B < /
方法
DiffLinq 115.19 ms2800.00002800.00002800.0000
DiffZip 23.48 ms 0.018 ms0.015 ms0.20

对于N = 100M:

< span style=" font - family:宋体;">是< / th > < span style=" font - family:宋体;"> < / th >错误 < span style=" font - family:宋体;"> StdDev < / th > < span style=" font - family:宋体;"> < / th >比例 < span style=" font - family:宋体;"> Gen 0 < / th > < span style=" font - family:宋体;">创1 < / th > < span style=" font - family:宋体;"> < / th >第2代 < span style=" font - family:宋体;"> < / th >分配 . . . . . < span style=" font - family:宋体;"道明> > 8589937032 B < / . < span style=" font - family:宋体;"> - < / td > < span style=" font - family:宋体;"> - < / td > < span style=" font - family:宋体;"> - < / td > < span style=" font - family:宋体;"道明> > 720 B < /
方法
DiffLinq 12.46 s0.0379 s13000.000013000.000013000.0000
DiffZip 2.324 s0.0019 s 0.0018 s 0.19

请注意,这个示例当然得益于列表已经排序并且可以非常有效地比较整数的事实。但这正是关键所在:如果你确实有有利的环境,一定要好好利用它们。

一些进一步的评论:比较函数的速度显然与整体性能相关,因此优化它可能是有益的。这样做的灵活性是压缩方法的一个好处。此外,并行化对我来说似乎更可行,尽管这并不容易,而且可能不值得付出努力和开销。然而,有一种简单的方法可以将这个过程加快大约2倍,那就是将列表分别分成两部分(如果可以有效地做到这一点),并并行地比较各个部分,一个从前到后处理,另一个按相反的顺序处理。

我比较了3种不同的方法来比较不同的数据集。下面的测试创建了一个从0length - 1的所有数字的字符串集合,然后是另一个具有相同范围的集合,但是是偶数。然后我从第一个集合中挑出奇数。

使用Linq除外

public void TestExcept()
{
WriteLine($"Except {DateTime.Now}");
int length = 20000000;
var dateTime = DateTime.Now;
var array = new string[length];
for (int i = 0; i < length; i++)
{
array[i] = i.ToString();
}
Write("Populate set processing time: ");
WriteLine(DateTime.Now - dateTime);
var newArray = new string[length/2];
int j = 0;
for (int i = 0; i < length; i+=2)
{
newArray[j++] = i.ToString();
}
dateTime = DateTime.Now;
Write("Count of items: ");
WriteLine(array.Except(newArray).Count());
Write("Count processing time: ");
WriteLine(DateTime.Now - dateTime);
}

输出

Except 2021-08-14 11:43:03 AM
Populate set processing time: 00:00:03.7230479
2021-08-14 11:43:09 AM
Count of items: 10000000
Count processing time: 00:00:02.9720879

使用HashSet。添加

public void TestHashSet()
{
WriteLine($"HashSet {DateTime.Now}");
int length = 20000000;
var dateTime = DateTime.Now;
var hashSet = new HashSet<string>();
for (int i = 0; i < length; i++)
{
hashSet.Add(i.ToString());
}
Write("Populate set processing time: ");
WriteLine(DateTime.Now - dateTime);
var newHashSet = new HashSet<string>();
for (int i = 0; i < length; i+=2)
{
newHashSet.Add(i.ToString());
}
dateTime = DateTime.Now;
Write("Count of items: ");
// HashSet Add returns true if item is added successfully (not previously existing)
WriteLine(hashSet.Where(s => newHashSet.Add(s)).Count());
Write("Count processing time: ");
WriteLine(DateTime.Now - dateTime);
}

输出

HashSet 2021-08-14 11:42:43 AM
Populate set processing time: 00:00:05.6000625
Count of items: 10000000
Count processing time: 00:00:01.7703057

特殊HashSet测试:

public void TestLoadingHashSet()
{
int length = 20000000;
var array = new string[length];
for (int i = 0; i < length; i++)
{
array[i] = i.ToString();
}
var dateTime = DateTime.Now;
var hashSet = new HashSet<string>(array);
Write("Time to load hashset: ");
WriteLine(DateTime.Now - dateTime);
}
> TestLoadingHashSet()
Time to load hashset: 00:00:01.1918160

使用.Contains

public void TestContains()
{
WriteLine($"Contains {DateTime.Now}");
int length = 20000000;
var dateTime = DateTime.Now;
var array = new string[length];
for (int i = 0; i < length; i++)
{
array[i] = i.ToString();
}
Write("Populate set processing time: ");
WriteLine(DateTime.Now - dateTime);
var newArray = new string[length/2];
int j = 0;
for (int i = 0; i < length; i+=2)
{
newArray[j++] = i.ToString();
}
dateTime = DateTime.Now;
WriteLine(dateTime);
Write("Count of items: ");
WriteLine(array.Where(a => !newArray.Contains(a)).Count());
Write("Count processing time: ");
WriteLine(DateTime.Now - dateTime);
}

输出

Contains 2021-08-14 11:19:44 AM
Populate set processing time: 00:00:03.1046998
2021-08-14 11:19:49 AM
Count of items: Hosting process exited with exit code 1.
(Didnt complete. Killed it after 14 minutes)

结论:

  • Linq Except在我的设备上运行大约比使用HashSets慢1秒(n=20,000,000)。
  • 使用WhereContains运行了很长时间

哈希集的总结:

  • 独特的数据
  • 确保为类类型重写GetHashCode(正确地)
  • 如果您复制数据集,可能需要高达2倍的内存,这取决于实现
  • HashSet优化,用于使用IEnumerable构造函数克隆其他哈希集,但将其他集合转换为哈希集要慢一些(参见上面的特殊测试)

一行:

var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<int> { 1, 2, 3, 4 };
if (list1.Except(list2).Count() + list2.Except(list1).Count() == 0)
Console.WriteLine("same sets");

第一种方法:

if (list1 != null && list2 != null && list1.Select(x => list2.SingleOrDefault(y => y.propertyToCompare == x.propertyToCompare && y.anotherPropertyToCompare == x.anotherPropertyToCompare) != null).All(x => true))
return true;

第二种方法,如果你对重复值没有问题:

if (list1 != null && list2 != null && list1.Select(x => list2.Any(y => y.propertyToCompare == x.propertyToCompare && y.anotherPropertyToCompare == x.anotherPropertyToCompare)).All(x => true))
return true;