【问题标题】:I need to compare two very large collections with potentially missing elements我需要比较两个可能缺少元素的非常大的集合
【发布时间】:2013-10-11 07:52:44
【问题描述】:

我在为我的工作编写的程序中遇到了一堵砖墙。 您不需要具体了解上下文,但长话短说,我有两个集合,每个集合大约 65 万条记录。

假设集合 A 是我知道是正确的集合,集合 B 是我知道不正确的集合。

Collection B 包含一个复杂对象,它有一个与 Collection A 中的元素类型相同的 Property(换句话说,它看起来有点像这样):

// Where T : IComparable
IEnumerable<DateTime> A = ...; // Collection of T elements
IEnumerable<Complex> B = ...; // Collection of complex elements.
class Complex<DateTime>
{
   public DateTime Time { get; set; }
   .....
}

我的问题是我基本上需要依次枚举 A 并查看 A 的当前元素是否存在于 B 中的 Complex 对象中;如果它不存在,那么我需要创建一个 Complex 对象来封装该元素(以及其他内容)。

当我意识到两个列表的长度大约为 650,000 个元素时,就会出现问题。我无法减少数据集;我必须使用这 650,000 个。现在我已经使用了ICollection.Contains(),并且我尝试了二进制搜索的(天真的)实现,但它需要的时间太长了。

你对我有什么建议吗?

编辑:如果有帮助,T 实现 IComparable。 EDIT2:更多上下文: IEnumerable 是使用 Linq To Objects 从 DataTable 中检索的。

        IEnumerable<Complex> set = set.Tbl
            .Where(dbObject => dbObject.TS.CompareTo(default(DateTime)) != 0)
            .Select(Func<DataRow,Complex>) // Function that wraps the DataRow in a Complex object
            // Just done to make debugging a little easier so we still have a large sample but small enough that it doesn't make me grow a beard
            .Take(100000) 
            .AsEnumerable<Complex>();

为了完整起见,万一这个问题被归档并且其他人需要解决这个问题,我当前的实现看起来有点像这样

        BDataSet bSet = new BDataSet();
        B_LUTableAdapter adap = new B_LUTableAdapter();
        adap.Fill(bSet.B_LU);
        IEnumerable<Complex> w3 = bSet.B
            .Where(dbObject => dbObject.TS.CompareTo(default(DateTime)) != 0)
            // Function that just wraps datarow into a complex object
            .Select(Func<DataRow, Complex>)
            // Just for sake of debugging speed
            .Take(100000)
            .AsEnumerable<Complex>();

        List<Complex> b = bSet.OrderBy(x => x.Time).ToList<Complex>();
        // Get last & first timestamps
        // Some of the timestamps in b are 01/01/1011 for some reason,
        // So we do this check.
        Complex start = b.Where(x => x.Time != default(DateTime)).First();
        Complex end = b.Last();

        List<DateTime> a = new List<DateTime>();
        // RoundSeconds reduces seconds in a DateTime to 0.
        DateTime current = RoundSeconds(new DateTime(start.Time.Ticks));

        while (current.CompareTo(RoundSeconds(end.Time)) <= 0)
        {
            a.Add(current);
            current = current.Add(TimeSpan.FromMinutes(1));
        }

        IEnumerable<DateTime> times = b.Select(x => x.Time);
        var missing = a.Where(dt => times.Contains(dt));
        foreach (var dt in missing)
        {
            adap.Insert(dt, 0, "", "", "", null, 0, 0);
            // This has since been changed to List.Add()
        }

感谢 Cosmin,此问题现已解决,最终实现如下: 预期列表 = 新列表(); 当前日期时间 = RoundSeconds(new DateTime(start.Time.Ticks));

        while (current.CompareTo(RoundSeconds(end.Time)) <= 0)
        {
            expected.Add(current);
            current = current.Add(TimeSpan.FromMinutes(1));
        }

        Console.WriteLine("Expecting {0} intervals.", expected.Count);
        var missing = b.FindAllMissing(expected, x => x.Time);
        if(!missing.Any()) return;
        Console.WriteLine("{0} missing intervals.", missing.Count());
        foreach (var dt in missing)
        {
            b.Add(new Complex() { /* some values */ });
            //Console.WriteLine("\t> Inserted new record at {0}", dt);
        }

        //.....
        public static IEnumerable<Basic> FindAllMissing<Basic, Complex>(this IEnumerable<Complex> complexList,
        IEnumerable<Basic> basicList,
        Func<Complex, Basic> selector)
        {
            HashSet<Basic> inComplexList = new HashSet<Basic>();
            foreach (Complex c in complexList)
                inComplexList.Add(selector(c));
            List<Basic> missing = new List<Basic>();
            foreach (Basic basic in basicList)
                if (!(inComplexList.Contains(basic)))
                    missing.Add(basic);
            return missing;
        }

【问题讨论】:

  • 您可以尝试先通过 A 查找来索引您的集合 B。然后你可以尝试使用.AsParallel() 并行化它。并衡量两者的效果。
  • 您可以使用HashSet&lt;T&gt; 来加快查找速度。
  • 您选择了“linq”作为标签,但我没有看到 linq。
  • @ps2goat:为你解决了这个问题。 :)
  • 请问这个操作多久发生一次?多长时间才算过长?

标签: c# linq


【解决方案1】:

一步一步:

  • 使用O(1) 通用集合之一创建一个可快速搜索的T 列表,这些列表已经在第二个集合中。我可以建议HashSet&lt;T&gt;
  • 枚举 SECOND 集合并将所有T 放入第一步的集合中。
  • 枚举第一个集合并检查每个项目是否在第一步创建的集合中。由于该操作是O(1),因此您现在有了O(n) 解决方案。
  • 享受吧。

这是一个将该算法实现为通用扩展方法的类,以使其对 LINQ 更加友好。将其参数设为IEnumerable&lt;T&gt; 并返回IEnumerable&lt;T&gt;,对类型(TComplex)不做任何假设。在我的测试中,我使用Tuple&lt;int,int&gt; 列表作为复杂类型,使用简单int 作为简单类型。控制台应用程序用 600000 个值填充 List&lt;Tuple&lt;int,int&gt;&gt;,然后将 100000 个值放入简单的 List&lt;int&gt;,它使用枚举器计算在 List&lt;Tuple&lt;int,int&gt;&gt; 中找不到的所有简单值;它是如此之快,以至于您没有机会看到它在工作,当您点击F5 时,它只会显示结果。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication2
{

    static class FixProblem
    {
        public static IEnumerable<T> FindAllThatNeedCreating<T, Complex>(this IEnumerable<Complex> list_of_complex, IEnumerable<T> list_of_T, Func<Complex, T> extract)
        {
            HashSet<T> T_in_list_of_complex = new HashSet<T>();
            foreach (Complex c in list_of_complex)
                T_in_list_of_complex.Add(extract(c));
            List<T> answer = new List<T>();
            foreach (T t in list_of_T)
                if (!T_in_list_of_complex.Contains(t))
                    answer.Add(t);
            return answer;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Test the code
            List<Tuple<int, int>> complex = new List<Tuple<int, int>>();
            List<int> simple = new List<int>();

            // Fill in some random data
            Random rnd = new Random();
            for (int i = 1; i < 600000; i++)
                complex.Add(new Tuple<int, int>(rnd.Next(), rnd.Next()));

            for (int i = 1; i < 100000; i++)
                simple.Add(rnd.Next());

            // This is the magic line of code:
            Console.WriteLine(complex.FindAllThatNeedCreating(simple, x => x.Item1).Count());

            Console.ReadKey();

        }
    }
}

【讨论】:

  • 我确实走上了这条路,但是当我再次尝试做new HashSet(myEnumerable) 时,又花了太多时间。我想我需要在一些背景下详细说明我原来的问题。
  • 再试一次:看看我的代码,60 万条“复杂”记录和 10 万条“简单”记录,当你点击运行时,你会得到结果,没有时间“计时”它。这确实是一个O(n) 算法。
  • 我的代码有两个有趣的保证:它枚举整个complex 列表恰好一次,然后枚举简单列表恰好一次。如果这对您的数据来说很慢,那么枚举本身就很慢,这不是算法的错,需要花费魔法来加速基本枚举,而不是这个算法。
  • 我目前正在尝试实现您的算法,只是因为作为一种扩展方法,它很容易重用和传递。但是,我有一个问题 - 它似乎将每个 DateTime 添加到“答案”列表中。请参阅我在 pastebin here 上的实现
  • 我刚刚检查过,DateTime 正确实现了GetHashCode()(上面代码的唯一要求)所以你的问题出在其他地方。也许您的日期几乎相等但不相等(例如:一个有时间,另一个有不同的时间,等等)。这是上面的代码,可以与DateTime一起使用,玩一下吧:pastebin.com/z2YDst7U
【解决方案2】:

我建议将您的复杂对象存储在字典中,使用 A 类型属性作为键。然后您可以非常快速地查看 A 中的任何元素是否存在于 Dictionary 中。每个查找操作将是 O(1),整体性能应该是 O(n)。

或者,您可以在现有的 IEnumerable 上调用 .ToLookup(),并在 lambda 表达式中创建键和值。返回的 ILookup 应该非常适合您的需求(即从 A 中查找值)。这里的另一个优点是,如果您有多个包含 A 的复杂对象,则在使用 Lookup 时会返回它们的集合。

【讨论】:

    【解决方案3】:

    如果我理解了您的要求,那么我认为这段代码运行良好。我已将这两个集合的记录提高到 650 万条,并在 11 秒内完成。将其减少到 65 万条记录只需不到一秒的时间。

    var rnd = new Random();
    
    var xs = Enumerable
        .Range(0, 650000)
        .Select(x => new Complex<int>()
        {
            MyComplex = rnd.Next(0, 100001)
        })
        .ToList();
    
    var ys = Enumerable
        .Range(0, 650000)
        .Select(x => rnd.Next(0, 100001))
        .ToArray();
    
    var xsLookup = xs.ToLookup(x => x.MyComplex);
    
    var newYs = ys.Where(y => !xsLookup[y].Any()).ToArray();
    
    newYs
        .ToList()
        .ForEach(y =>
        {
            xs.Add(new Complex<int>()
            {
                MyComplex = y
            });
        });
    

    【讨论】:

      【解决方案4】:

      更新:首先,停止使用数据集。我建议你使用 Linq to SQL 或 Entity Framework。

      试试这个:

              var lookup = B.ToLookup(c => c.MyComplex);
      
              var noMatch = from a in A
                            where !lookup.Contains(a)
                            select a;
      

      应该更快,但要衡量。

      那就试试

              var lookup = B.AsParallel().ToLookup(c => c.MyComplex);
      
              var noMatch = from a in A.AsParallel()
                            where !lookup.Contains(a)
                            select a;
      

      然后再次测量。

      显然,确保 A 中的对象类型正确且有效地覆盖 GetHashCode()Equals(object)。尤其是GetHashCode()应该大概率不同的对象有不同的hash码,而且还是很快的。

      更新:由于我们现在知道 A 中对象的类型是 DateTime,所以 GetHashCode()Equals(object) 的要求是可以的。

      代码变成

              var lookup = B.ToLookup(c => c.Time);
      
              var noMatch = from a in A
                            where !lookup.Contains(a)
                            select a;
      

      【讨论】:

      • 关于数据集的话题:EntityFramework 看起来工作量很大,而 Linq to SQL 似乎不合适。无论如何,我不确定如何使用 Access DB 来实现。
      • 快速读取 Access 文件,请使用原始 ADO.NET,即 DbConnection、DbCommand、DbDataReader。并且不要创建你不需要的对象,不要在不需要的时候调用 .OrderBy() 和 .ToList()。
      • 不知道使用原始 ADO.NET 会带来性能优势,请记住这一点 :) 谢谢。
      • 使用 DataSet/DataTable 保存数据表示有什么根本错误吗?我们的很多遗留代码(在我来这份工作之前很久就已经在使用,我 6 周前开始在这里工作)几乎完全使用它。假设集合中带有一些 LINQ 操作 (.Select()) 的 POCO 几乎肯定会更快,我是否正确?
      • @DanPantry 这个问题需要比评论更多的空间来回答。但总的来说,Linq2SQL 很有可能比数据集更快。在原始问题的情况下,OP 大概是将整个表读入数据集,即所有行、所有列,而真正需要的只是日期序列。
      【解决方案5】:

      如果您的列表都按相同的键排序,您可以同时遍历这两个列表。这样做我能够将时间缩短到少于 containsexcept 的时间,甚至少于我看到的 toLookup 时间。

      var A = SimpleCollection; // source of truth
      var B = GetComplexOnDemand().ToArray();
      var missing = new List<Complex<DateTime>>();
      int cpxIx = 0;
      int simpleIx = 0;
      while (simpleIx < A.Count)
      {
          if (cpxIx >= B.Length)
          {
              missing.Add(new Complex<DateTime>() {InnerValue = A[simpleIx]});
              simpleIx++;
              continue;
          }
      
          if (A[simpleIx] != B[cpxIx].InnerValue)
          {
              missing.Add(new Complex<DateTime>() {InnerValue = A[simpleIx]});
              simpleIx++;
              continue;
          }
      
          cpxIx++;
          simpleIx++;
      }
      

      为了生成测试数据,我做了以下操作:

      private static readonly List<DateTime> SimpleCollection = 
          Enumerable.Range(1, SimpleSize).Select(t => new DateTime(DateTime.Now.Ticks + t)).ToList();
      public static IEnumerable<Complex<DateTime>> GetComplexOnDemand()
      {
          for (int i = 1; i <= ComplexSize; i+=2)
          {
              yield return new Complex<DateTime>() { InnerValue = SimpleCollection[i] };
          }
      }
      

      【讨论】:

      • 我正计划这样做,但如果我没有错(缺少咖啡意味着我很可能是这样),那么这个实现不会很好地处理丢失的条目。
      猜你喜欢
      • 2023-03-23
      • 2021-08-06
      • 1970-01-01
      • 2014-03-16
      • 1970-01-01
      • 2017-08-16
      • 1970-01-01
      • 1970-01-01
      • 2016-01-25
      相关资源
      最近更新 更多