【问题标题】:Improve the time complexity of current Linq queries提高当前 Linq 查询的时间复杂度
【发布时间】:2016-12-31 05:55:39
【问题描述】:

我有以下列表:

RakeSnapshots, ProductMovements

目的是对两者进行处理,得到符合条件的元素个数,如下:

  • 考虑RakeSnapshotsStatusCode == "Dumping"

  • 考虑ProductMovementStatus == "InProgress"

  • 获取两个列表中满足RakeSnapshots.RakeCode等于ProductMovements.ProductCode条件的所有元素的count

以下是我目前的选择:

// 代码1:

 var resultCount =  ProductMovements.Where(x => RakeSnapshots
                                                .Where(r => r.StatusCode == "Dumping")
                                                .Any(y => y.RakeCode == x.ProductCode  && 
                                                          x.Status == "InProgress"))
                                                .Count();

// 代码2:

var productMovementsInprogress = ProductMovements.Where(x => x.Status == "InProgress");

var rakeSnapShotsDumping = RakeSnapshots.Where(r => r.StatusCode == "Dumping");

var resultCount = productMovementsInprogress.Zip(rakeSnapShotsDumping,(x,y) => (y.RakeCode == x.ProductCode) ?  true : false)
                                            .Where(x => x).Count();

挑战是两个代码都是O(n^2)复杂度,有没有办法改进它,如果数据很大,这会伤害

【问题讨论】:

  • RakeSnapshots - RakeCode equal to ProductMovements - ProductCode 是什么意思?
  • 我们可以将RakeCodeProductCode 进行比较,如问题中的示例所示。已编辑,出现复制粘贴错误
  • 第二个变体不等同于第一个。第一个是否返回预期结果?
  • 你的意思是RakeSnapshots.RakeCode equal to ProductMovements.ProductCode 吗?因为RakeSnapshots - RakeCode equal to ProductMovements - ProductCode 意味着某种减法。
  • @Matthew Watson 为这种混乱道歉,RakeCode 属于RakeSnapshots 实体,ProductCode 属于ProductMovements,你理解正确

标签: c# algorithm linq data-structures


【解决方案1】:

您可以使用inner join 来执行此操作:

var dumpingRakeSnapshots       = rakeSnapshots.Where(r => r.StatusCode == "Dumping");
var inProgressProductMovements = productMovements.Where(p => p.Status == "InProgress");

var matches =
    from r in dumpingRakeSnapshots
    join p in inProgressProductMovements on r.RakeCode equals p.ProductCode
    select r;

int count = matches.Count(); // Here's the answer.

请注意(正如 Ivan Stoev 指出的那样)这仅在 RakeCode 是 RakeSnapshots 的主键时才有效。

如果不是,您将不得不使用grouped join

这是您在这种情况下应该使用的 Linq 查询语法版本,但请注意,这与 Ivan 的答案完全相同(仅在 Linq 查询形式中):

var matches =
    from r in dumpingRakeSnapshots
    join p in inProgressProductMovements on r.RakeCode equals p.ProductCode into gj
    select gj;

为了完整起见,这里有一个可编译的控制台应用程序,演示了如果 RakeCodeProductCode 不是主键,您将获得不同的结果:

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

namespace ConsoleApp1
{
    class RakeSnapshot
    {
        public string StatusCode;
        public string RakeCode;
    }

    class ProductMovement
    {
        public string Status;
        public string ProductCode;
    }

    sealed class Program
    {
        void run()
        {
            var rakeSnapshots = new List<RakeSnapshot>
            {
                new RakeSnapshot {StatusCode = "Dumping", RakeCode = "1"},
                new RakeSnapshot {StatusCode = "Dumping", RakeCode = "1"},
                new RakeSnapshot {StatusCode = "Dumping", RakeCode = "2"}
            };

            var productMovements = new List<ProductMovement>
            {
                new ProductMovement {Status = "InProgress", ProductCode = "1"},
                new ProductMovement {Status = "InProgress", ProductCode = "2"},
                new ProductMovement {Status = "InProgress", ProductCode = "2"}
            };

            var dumpingRakeSnapshots       = rakeSnapshots.Where(r => r.StatusCode == "Dumping");
            var inProgressProductMovements = productMovements.Where(p => p.Status == "InProgress");

            // Inner join.

            var matches1 =
                from r in dumpingRakeSnapshots
                join p in inProgressProductMovements on r.RakeCode equals p.ProductCode
                select r;

            Console.WriteLine(matches1.Count());

            // Grouped join.

            var matches2 =
                from r in dumpingRakeSnapshots
                join p in inProgressProductMovements on r.RakeCode equals p.ProductCode into gj
                select gj;

            Console.WriteLine(matches2.Count());

            // OP's code.

            var resultCount = 
                productMovements
                .Count(x => rakeSnapshots
                .Where(r => r.StatusCode == "Dumping")
                .Any(y => y.RakeCode == x.ProductCode && x.Status == "InProgress"));

            Console.WriteLine(resultCount);
        }

        static void Main(string[] args)
        {
            new Program().run();
        }
    }
}

【讨论】:

  • 这真的取决于 OP 代码 #1 是否产生了预期的结果。既然它使用Any,那么你应该使用join intowhere,基本上把它变成GroupJoin的查询语法。
  • 虽然如果加入字段在各自的集合中是唯一的,那么简单的inner join 就像你的答案一样:)
  • @IvanStoev 这是一个很好的观点。我将更新我的答案以包含组加入的查询语法版本(但该部分将等同于您当时的答案)。
  • 实际上,您在(已删除)之前的评论中所说的话非常有道理。而且我不太正确 - 如果RakeCodeRakeSnapshots 的PK(例如在该集合中是唯一的),那么简单的inner join 就可以了。 +1
  • 为什么这样更快? linq-to-objects 上的 join 操作仍然需要遍历所有左侧表,然后对于左侧的每个对象,遍历右侧的所有项目,时间复杂度为 O(n.m)。
【解决方案2】:

听起来像Group Join(以及Join)是关联两组的最有效的LINQ方法:

var resultCount = ProductMovements.Where(p => p.Status == "InProgress")
    .GroupJoin(RakeSnapshots.Where(r => r.StatusCode == "Dumping"), 
        p => p.ProductCode, r => r.RakeCode, (p, match) => match)
    .Count(match => match.Any());

上面的时间复杂度是O(N+M)。

【讨论】:

    【解决方案3】:

    通常,使用 O(N^2),您会希望创建一个中间“搜索”数据结构来加快查找速度。类似于 O(1) 访问的哈希表,或 O(log N) 访问的排序列表。

    从技术上讲,您有两个不同的列表,因此实际顺序为 O(P.R),其中 P 是产品移动的数量,R 是 rake 快照的数量。

    在您的情况下,这是您的原始代码;

    var resultCount =  ProductMovements
        .Where(x => RakeSnapshots
            .Where(r => r.StatusCode == "Dumping")
            .Any(y => y.RakeCode == x.ProductCode  && 
                      x.Status == "InProgress"))
            .Count();
    

    是 O(PR),因为对于每个 P,内部 where 子句循环通过每个 R。我希望创建一个 Dictionary&lt;T&gt;HashSet&lt;T&gt;,然后将您的代码转换为类似的东西

    var rakeSnapshotSummary = ... magic happens here ...;
    var resultCount =  ProductMovements
        .Where(x => rakeSnapshotSummary[x.ProductCode] == true)
        .Count();
    

    这样,创建快照是 O(R),查找数据结构是 O(1),创建结果是 O(P),更健康的 O(P+R)。我认为这是最好的。

    所以我对你的索引例程的建议是这样的;

    var rakeSnapshotSummary = new HashSet<string>(RakeSnapshots
        .Where(r => r.StatusCode == "Dumping")
        .Select(r => r.RakeCode));
    

    这将创建一个HashSet&lt;string&gt;,它具有 O(1) 的时间复杂度来测试 rake 代码的存在性。然后你的最后一行看起来像

    var resultCount =  ProductMovements
        .Where(x => x.Status == "InProgress" && rakeSnapshotSummary.Contains(x.ProductCode))
        .Count();
    

    所以总体而言,O(P+R) 或粗略地说,O(2N) => O(N)。

    【讨论】:

    • 谢谢,我确实考虑过类似的设计,但我需要一个纯 linq 解决方案,这是运行时执行框架所必需的,并且没有中间数据结构的范围
    猜你喜欢
    • 1970-01-01
    • 2018-08-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-17
    • 2019-10-30
    相关资源
    最近更新 更多