LINQ的OrderBy如何与MoveNext合作?

时间:2016-06-24 05:06:39

标签: c# .net linq

This thread说LINQ的OrderBy使用Quicksort。鉴于OrderBy返回IEnumerable

,我正在努力解决这个问题

我们以下面的代码为例。

int[] arr = new int[] { 1, -1, 0, 60, -1032, 9, 1 }; 
var ordered = arr.OrderBy(i => i); 
foreach(int i in ordered)
    Console.WriteLine(i); 

循环相当于

var mover = ordered.GetEnumerator();
while(mover.MoveNext())
    Console.WriteLine(mover.Current);

MoveNext()返回下一个最小的元素。 LINQ的工作方式,除非您通过使用ToList()或类似方法“兑现”查询,否则不应该创建任何中间列表,因此每次调用MoveNext() {{1找到下一个最小的元素。这没有意义,因为在执行Quicksort期间,没有当前最小和次要元素的概念。

我的思维缺陷在哪里?

2 个答案:

答案 0 :(得分:12)

  

LINQ的工作方式,除非你"兑现"通过使用ToList()或类似的查询,不应该是任何创建的中间列表

这句话是错误的。你的想法中的缺陷是你相信一个错误的陈述。

LINQ to Objects实现非常聪明,在可能的情况下以合理的成本延迟工作 。正如您所记录的那样,在排序的情况下是不可能的。 OrderBy生成一个对象,当调用MoveNext时,它会枚举整个源序列,在内存中生成已排序的列表,然后枚举已排序的列表。

类似地,连接和分组也必须在枚举第一个元素之前枚举整个序列。 (逻辑,连接只是一个带有过滤器的交叉产品,工作可以分散在每个MoveNext()上,但这样效率很低;为了实用,建立了一个查找表。教育来计算渐近空间与时间的权衡;试一试。)

源代码可用;如果您对实施有疑问,我建议您阅读。或者查看Jon" edulinq"系列。

答案 1 :(得分:0)

已经有了一个很好的答案,但要添加一些内容:

枚举OrderBy()的结果显然不能产生一个元素,直到它处理完所有元素,因为直到它看到最后一个输入元素才能知道所见的最后一个元素不是它必须产生的第一个元素。它还必须处理不能重复或每次都会产生不同结果的来源。因此,即使某种热情意味着开发人员希望每个周期重新找到第n个元素,缓冲也是一个合乎逻辑的要求。

尽管如此,快速入口在两个问题上都是懒惰的。一种方法是,不是根据传递给方法的委托中的键对要返回的元素进行排序,而是对映射进行排序:

  1. 缓冲所有元素。
  2. 拿到钥匙。请注意,这意味着每个元素只运行一次委托。除此之外,它意味着非纯粹的键选择器不会引起问题。
  3. 获取0到n之间的数字地图。
  4. 对地图进行排序。
  5. 通过地图枚举,每次都会产生相关元素。
  6. 因此在元素的最终排序中存在一种懒惰。这在移动元素昂贵(大值类型)的情况下非常重要。

    当然也有懒惰,因为在第一次尝试枚举之前,上述任何一项都没有完成,所以在你第一次打电话给MoveNext()之前,它就不会发生。

    在.NET Core中,还有进一步的懒惰,取决于你对OrderBy的结果所做的事情。由于OrderBy包含有关如何排序而不是排序缓冲区的信息,OrderBy返回的类可以使用除快速排序之外的其他信息执行其他操作:

    1. 最明显的是ThenBy,所有实现都是这样做的。当您致电ThenByThenByDescending时,您会收到一个新的类似类,其中包含有关如何排序的不同信息,以及OrderBy结果可能完成的排序可能永远不会。
    2. First()Last()根本不需要排序。逻辑上source.OrderBy(del).First()source.Min()的变体,其中del包含用于确定为Min()定义“小于”的内容的信息。因此,如果您对First()的结果致电OrderBy(),那就是完成的工作。 OrderBy的懒惰允许它执行此操作而不是快速排序。 (这意味着O(n)时间复杂度和O(1)空间复杂度,而不是O(n log n)和O(n)。
    3. Skip()Take()定义序列的子序列,其中OrderBy必须在概念上发生。但是因为它们也是懒惰的,所以可以归还的是一个知道的对象;如何排序,跳过多少,取多少。因此可以使用部分快速排序,以便源只需要进行部分排序:如果分区超出了将返回的范围,那么就没有必要对其进行排序。
    4. ElementAt()First()Last()承担更多的负担,但同样不需要完整的快速排序。 Quickselect可用于查找一个结果;如果你正在寻找第3个元素并且你在第90个元素周围划分了一组200个元素,那么你只需要在第一个分区中进一步查看,并且从现在开始可以忽略第二个分区。最佳案例和平均案例时间复杂度为O(n)。
    5. 以上可以组合,例如.Skip(10).First()相当于ElementAt(10),可以这样对待。
    6. 获取整个缓冲区并对其进行排序的所有这些异常都有一个共同点:它们都是在确定了一种在计算机执行较少工作后可以返回正确结果的方法之后实现的。在new [] {1, 2, 3, 4}.Where(i => i % 2 == 0)看到2(或者甚至不会产生4之后3产生Where()时,Enumerable.Range(1, 10000).Where(i => i >= 10000)会产生OrderBy。它只是来自它更容易(虽然在幕后仍有专门的Sum()结果变体提供其他优化)。

      但请注意,OrderBy会扫描9999个元素以获得第一个元素。真的,与OrderBy的缓冲并没有什么不同;他们都会尽快给你带来下一个结果†,不同之处就在于它意味着什么。

      *并且还要确定检测和利用特定案例的功能的努力是值得的。例如。许多聚合调用如Sum()可以通过完全跳过排序来优化OrderBy的结果。但这通常可以通过调用者来实现,他们可以省略{{1}},所以添加它会使大多数调用{{1}}稍微慢一点,以便使案例更快更好的情况下不应该无论如何真的会发生。

      †嗯,差不多快。有可能比{{1}}更快地得到第一个结果 - 当你得到一个序列的最左边部分排序开始给出结果 - 但这会产生影响后来结果的成本因此,权衡并不一定会做得更好。