C# LINQ';下一步是什么?

C# LINQ';下一步是什么?,c#,.net,linq,C#,.net,Linq,说LINQ的OrderBy使用快速排序。考虑到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.GetEnu

说LINQ的
OrderBy
使用快速排序。考虑到
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()
时,
IEnumerator
都会找到下一个最小的元素。这没有意义,因为在执行快速排序期间,没有当前最小和次最小元素的概念

我的思想缺陷在哪里?

LINQ的工作方式是,除非您使用ToList()或类似方法“兑现”查询,否则不应该创建任何中间列表

这句话是假的。你思想上的缺陷是你相信一个错误的陈述

LINQtoObjects实现在尽可能以合理的成本推迟工作方面是明智的。正如您正确指出的,在排序的情况下是不可能的
OrderBy
生成一个对象作为其结果,当调用
MoveNext
时,该对象枚举整个源序列,在内存中生成排序列表,然后枚举排序列表

类似地,联接和分组也必须在枚举第一个元素之前枚举整个序列。(从逻辑上讲,连接只是一个带过滤器的叉积,工作可能会分散到每个MoveNext()上,但这将是低效的;为了实用起见,会建立一个查找表。计算出渐进的空间与时间的折衷是有教育意义的;试一试。)


源代码可用;如果您对实施有疑问,我鼓励您阅读。或者看看Jon的“edulinq”系列。

已经有了一个很好的答案,但要补充几点:

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

不过,快速排序在两个方面是懒惰的。一种是,它不是根据传递给方法的委托的键对要返回的元素进行排序,而是对映射进行排序:

  • 缓冲所有元素
  • 去拿钥匙。请注意,这意味着每个元素只运行一次委托。除其他外,这意味着非纯键选择器不会引起问题
  • 获取从0到n的数字地图
  • 把地图分类
  • 枚举映射,每次生成关联的元素
  • 因此,在元素的最终排序中存在某种惰性。这在移动元素昂贵(大值类型)的情况下非常重要

    当然也有惰性,在第一次尝试枚举之前,上面的任何一项都不会完成,因此,在第一次调用
    MoveNext()
    之前,不会发生这种情况

    在.NETCore中,还有更多的惰性建立在这一点上,这取决于您随后如何处理
    OrderBy
    的结果。由于
    OrderBy
    包含有关如何排序的信息,而不是有关排序缓冲区的信息,因此
    OrderBy
    返回的类可以使用该信息执行快速排序以外的其他操作:

  • 最明显的是
    ThenBy
    ,所有实现都是这样做的。当您调用
    ThenBy
    ThenBy Descending
    时,您会得到一个新的类似类,其中包含关于如何排序的不同信息,并且
    OrderBy
    结果可能永远不会完成的排序
  • First()
    Last()
    根本不需要排序。逻辑上
    source.OrderBy(del).First()
    source.Min()
    的一个变体,其中
    del
    包含确定该
    Min()的“小于”定义的信息。因此,如果您对
    OrderBy()
    的结果调用
    First()
    ,这正是要做的。OrderBy的惰性使得它可以这样做,而不是快速排序。(这意味着O(n)的时间复杂度和O(1)的空间复杂度,而不是O(n logn)和O(n)
  • Skip()
    Take()
    定义序列的子序列,该序列在概念上必须与
    OrderBy
    一起发生在排序之后。但既然他们也很懒,能得到的回报就是一个知道的对象;如何排序,跳过多少,取多少。因此,可以使用部分快速排序,以便只对源进行部分排序:如果分区超出将返回的范围,则对其进行排序没有意义
  • ElementAt()。Quickselect只能用于查找一个结果;如果您正在寻找第三个元素,并且已经在第90个元素周围划分了一组200个元素,那么您只需要进一步查看第一个分区,并且可以从现在开始忽略第二个分区。最佳情况和平均情况时间复杂度为O(n)
  • 上述内容可以组合使用,例如
    .Skip(10).First()
    相当于
    ElementAt(10)
    ,可以这样处理
  • 所有这些获取整个缓冲区并对其进行排序的例外都有一个共同点:它们都是在确定了一种方法之后实现的,在使计算机执行更少的操作后,可以返回正确的结果