【问题标题】:Quickest way to compare two generic lists for differences比较两个通用列表差异的最快方法
【发布时间】:2012-10-09 08:29:50
【问题描述】:

比较两个大型(>50.000 个项目)的最快(和最少的资源密集型)是什么,因此有两个列表,如下所示:

  1. 显示在第一个列表中但不在第二个列表中的项目
  2. 显示在第二个列表中但不在第一个列表中的项目

目前我正在使用 List 或 IReadOnlyCollection 并在 linq 查询中解决此问题:

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

但这并没有我想要的那么好。 由于我需要处理大量列表,因此有什么想法可以让这个过程更快、更省资源吗?

【问题讨论】:

  • 如果您遇到此问题并考虑添加新答案,请注意,他们要求的不是a 方式,而是最快方式。

标签: c# linq list


【解决方案1】:

使用Except:

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

我怀疑有些方法实际上会比这稍微快一点,但即使这样也会比你的 O(N * M) 方法大大快。

如果你想结合这些,你可以用上面的方法创建一个方法,然后是一个返回语句:

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

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

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

【讨论】:

  • 这真的是一个巨大的性能提升!感谢您的回答。
  • 我想知道两个巨大的列表,在比较之前排序有用吗?或者在Except扩展方法里面,传入的列表已经排序了。
  • @Larry:没有排序;它构建了一个哈希集。
  • @PranavSingh:它适用于任何具有适当相等性的东西 - 因此,如果您的自定义类型覆盖 Equals(object) 和/或实现 IEquatable<T> 应该没问题。
  • @k2ibegin:它使用默认的相等比较器,它将使用IEquatable<T> 实现或object.Equals(object) 方法。听起来您应该使用 minimal reproducible example 创建一个新问题 - 我们无法真正诊断 cmets 中的问题。
【解决方案2】:

Enumerable.SequenceEqual 方法

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

Enumerable.SequenceEqual(list1, list2);

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

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

IEqualityComparer 接口

定义了支持比较对象是否相等的方法。 MS.Docs for IEqualityComparer

【讨论】:

  • 这应该是公认的答案。问题不是关于SETS,而是关于LISTS,它可以包含重复的元素。
  • 我不明白这怎么可能是答案,因为SequenceEqual 的结果是一个简单的bool。 OP 需要两个结果列表 - 并根据集合操作描述他们想要的内容:“出现在第一个列表中但不在第二个列表中的项目”。没有迹象表明排序是相关的,而 SequenceEqual 确实 认为它是相关的。这似乎在回答一个完全不同的问题。
  • 是的,没错,好像是我回答的太快了,没看请求的第二部分...和前两个cmets一样...
【解决方案3】:

更高效的是使用Enumerable.Except:

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

这个方法是通过延迟执行来实现的。这意味着您可以编写例如:

var first10 = inListButNotInList2.Take(10);

它也很有效,因为它在内部使用Set<T> 来比较对象。它首先从第二个序列中收集所有不同的值,然后对第一个序列的结果进行流式传输,检查它们以前没有见过。

【讨论】:

  • 嗯。没有完全推迟。我会说部分延期。从第二个序列构建一个完整的Set<T>(即它完全迭代和存储),然后产生可以从第一个序列添加的项目。
  • @spender,这就像说Where 的执行被部分延迟了,因为在list.Where(x => x.Id == 5) 中,数字5 的值是在开始时存储的,而不是延迟执行的。
【解决方案4】:

如果您希望结果不区分大小写,则可以使用以下方法:

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

【讨论】:

    【解决方案5】:
    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 为它们中的每一个生成相同的值。我们可以通过将哈希码委托给列表中的每个元素,并使用标准的按位异或来组合它们来实现这一点。 XOR 与顺序无关,因此列表的排序方式是否不同并不重要。重要的是它们只包含等效的成员。

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

    【讨论】:

      【解决方案6】:

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

      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);
      }
      

      【讨论】:

      • 这是您比较自定义数据类型所需要的。然后使用Except
      • 使用可排序类型可能会做得更好。这在 O(n^2) 中运行,而你可以在 O(nlogn) 中运行。
      【解决方案7】:

      试试这个方法:

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

      【讨论】:

      • 这会受到糟糕的性能的影响,需要对第一个列表中的每个项目扫描第二个列表。不是因为它有效,而是因为它与原始代码一样糟糕。
      • 我认为 OP 正在解决没有为对象实现 IEquitable 的问题。
      【解决方案8】:

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

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

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

      【讨论】:

        【解决方案9】:

        我用这段代码比较了两个有数百万条记录的列表。

        这个方法不会花太多时间

            //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;
            }
        

        【讨论】:

          【解决方案10】:

          虽然 Jon Skeet 的回答对于日常使用少量到中等数量的元素(最多几百万)的练习来说是一个极好的建议,但它不是最快的方法,也不是非常有效的资源。一个明显的缺点是,要获得完整的差异,需要对数据进行两次传递(如果相同的元素也很感兴趣,即使是 3 次)。显然,可以通过自定义重新实现 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 的结果如下:

          Method Mean Error StdDev Ratio Gen 0 Gen 1 Gen 2 Allocated
          DiffLinq 115.19 ms 0.656 ms 0.582 ms 1.00 2800.0000 2800.0000 2800.0000 67110744 B
          DiffZip 23.48 ms 0.018 ms 0.015 ms 0.20 - - - 720 B

          对于 N = 100M:

          Method Mean Error StdDev Ratio Gen 0 Gen 1 Gen 2 Allocated
          DiffLinq 12.146 s 0.0427 s 0.0379 s 1.00 13000.0000 13000.0000 13000.0000 8589937032 B
          DiffZip 2.324 s 0.0019 s 0.0018 s 0.19 - - - 720 B

          请注意,此示例当然受益于列表已经排序并且可以非常有效地比较整数这一事实。但这正是重点:如果您确实有有利的环境,请确保利用它们。

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

          【讨论】:

            【解决方案11】:

            我比较了 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.Add

            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 (n=20,000,000) 慢大约 1 秒。
            • 使用WhereContains跑了很长时间

            关于 HashSets 的结束语:

            • 独特的数据
            • 确保为类类型覆盖GetHashCode(正确)
            • 如果您制作数据集的副本,可能需要多达 2 倍的内存,具体取决于实现
            • HashSetoptimized 用于使用 IEnumerable 构造函数克隆其他 HashSet,但将其他集合转换为 HashSet 会更慢(参见上面的特殊测试)

            【讨论】:

            • 只显示一次运行的数据并不令人信服。运行的顺序可能很重要,并且总是涉及噪音。像这样的事情应该可靠地进行基准测试,例如通过 benchmarkdotnet.org 之类的基准测试框架。
            • 我没有包含所有数据,但我确实在之前/之后/重复等运行过几次,结果是一致的。
            【解决方案12】:

            第一种方法:

            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;
            

            【讨论】:

            • 他们要求的不是a 方式,而是最快 方式。证明,如果你回答这个问题。此外,您的方法总是返回 true。
            【解决方案13】:

            一行:

            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");
            

            【讨论】:

            • 他们要求的不是a 方式,而是最快 方式。如果你回答了这个问题,就证明这一点。
            【解决方案14】:

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

            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)
            

            【讨论】:

            【解决方案15】:

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

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

            【讨论】:

            • 因为它写在这里它甚至不适用于 List 或 List,例如两个列表 11;2;3 和 1;12;3 将是相同的,因为你不要使用列表中不可能的某些唯一分隔符连接字符串。除此之外,为包含很多项目的列表连接字符串可能是性能杀手。
            • @SwissCoder:你错了,这不是字符串的性能杀手。如果您有两个包含 50.000 个字符串(每个长度为 3)的列表,则此算法在我的机器上需要 3 毫秒。接受的答案需要 7。我认为诀窍是 Jibz 只需要一个字符串比较。当然,他必须添加一个唯一的分隔符。
            • @user1027167:我不是在谈论直接比较字符串(因为这也不是问题)。调用具有 50.000 个对象的 List 中所有对象的 .ToString() 方法可以创建一个巨大的字符串,具体取决于它的实现方式。我不认为这是要走的路。那么依赖一个字符或字符串是“唯一的”也是有风险的,代码不会像那样真正可重用。
            • 好吧,没错。提问者要求最快的方法,但没有给出他列表的数据类型。对于提问者的用例来说,这个答案可能是最快的方式。
            • 更不用说两个对象的相等ToString 结果本身并不使它们相等,而且两个列表都应该被排序。
            【解决方案16】:

            这是您能找到的最佳解决方案

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

            【讨论】:

            • 这实际上非常糟糕,因为它为list1 中的每个元素创建了一个新的List&lt;T&gt;。当结果不是List&lt;T&gt; 时,结果也称为list3
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2014-02-26
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多