如果仅请求第一个元素,Enumerable.OrderBy是否订购完整列表

时间:2017-06-30 08:18:46

标签: performance linq

如果订购了序列。而且你只要求有序序列的第一个元素。 Orderby足够聪明,不能订购完整的序列吗?

IEnumerable<MyClass> myItems = ...
MyClass maxItem = myItems.OrderBy(item => item.Id).FirstOrDefault();

因此,如果询问第一个元素,则只将具有最小值的项目排序为序列的第一个元素。当询问下一个元素时,将订购具有剩余序列的最小值的项目等。

如果你只想要第一个元素,那么完整的序列是完全有序的吗?

加成

显然问题不明确。我们举个例子。

Sort函数可以执行以下操作:

  • 创建包含所有元素的链接列表
  • 只要链表包含元素:
    • 将链表的第一个元素作为最小的
    • 扫描链接列表的其余部分以查找任何较小的元素
    • 从链接列表中删除最小元素
    • yield返回最小元素

代码:

public static IEnumerable<TSource> Sort<TSource, TKey>(
    this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));

    IComparer<TKey> comparer = Comparer<TKey>.Default;

    // create a linkedList with keyValuePairs of TKey and TSource
    var keyValuePairs = source
        .Select(source => new KeyValuePair<TKey, TSource>(keySelector(source), source);
    var itemsToSort = new LinkedList<KeyValuePair<Tkey, TSource>>(keyValuePairs);

    while (itemsToSort.Any())
    {   // there are still items in the list
        // select the first element as the smallest one
        var smallest = itemsToSort.First();

        // scan the rest of the linkedList to find the smallest one
        foreach (var element in itemsToSort.Skip(1))
        {
           if (comparer.Compare(element.Key, smallest.Key) < 1)
        {   // element.Key is smaller than smallest.Key: element becomes the smallest:
            smallest = element;
        }
    }

    // remove the smallest element from the linked list and return the value:
    itemsToSort.Remove(smallestElement);
    yield return smallestElement.Value;
}

假设我有一个整数序列。

Suppose I have the following sequence of integers:

{4, 8, 3, 1, 7}

在第一次迭代中,迭代器在内部创建一个键/值对的链接列表,并将列表的第一个元素指定为最小

Linked List =  4 - 8 - 3 - 1 - 7
Smallest = 4

扫描链表以查看是否有较小的链表。

Linked List =  4 - 8 - 3 - 1 - 7
Smallest = 1

从链表中删除最小值并返回:

Linked List =  4 - 8 - 3 - 7
return 1

使用较短的链表

进行第二次迭代
Linked List =  4 - 8 - 3 - 7
smallest = 4

再次扫描链表以找到最小的

Linked List =  4 - 8 - 3 - 7
smallest = 3

从链表中删除最小值并返回最小的

Linked List =  4 - 8 -  7
return 3

很容易看出,如果您只询问排序列表中的第一个元素,则列表只扫描一次。每次迭代,要扫描的列表都会变小。

回到我原来的问题:

据我所知,如果你只想要第一个元素,你必须至少扫描一次列表。如果您不要求第二个元素,则不会对列表的其余部分进行排序。

Enumerable.OrderBy使用的排序是否如此聪明,如果您只询问第一个订购的商品,是否不对列表的其余部分进行排序?

4 个答案:

答案 0 :(得分:3)

这取决于版本。

在框架版本(4.0,4.5等)中,然后:

  1. 将整个源加载到缓冲区中。
  2. 生成密钥映射(以便每个元素只生成一次密钥生成)。
  3. 生成整数映射,然后根据这些键进行排序(如果源元素是大值类型,则使用映射表示交换操作具有更便宜的副本)。
  4. FirstOrDefault尝试通过在结果对象上使用MoveNextCurrent来根据此映射获取第一个项目。要么找到一个,要么(如果缓冲区为空,因为源为空)返回default(TSource)
  5. 在.NET Core中,然后:

    1. FirstOrDefault上的IOrderedEnumerable操作会扫描来源。如果没有元素,则返回default(TSource),否则它将保留在找到的第一个元素和密钥生成器生成的密钥上,并将其与后续所有内容进行比较,替换保留的值和密钥,如果下一个,则替换为下一个found比较低于当前值。
    2. 保留值与框架版本通过第一次排序找到的元素相同,因此返回。
    3. 这意味着在框架版本myItems.OrderBy(item => item.Id).FirstOrDefault()O(n log n)时间复杂度(更糟糕的情况O(n²))和O(n)空间复杂度,但在.NET Core版本中它是O(n) O(1)时间复杂度和FirstOrDefault()空间复杂度。

      这里的主要区别在于.NET Core OrderBy知道ThenBy(和myItems等)的结果与其他可能的来源有何不同,并且有代码可以处理它*,而在框架版本中却没有。

      两者都扫描整个序列(你不能知道myItems中的最后一个元素不是排序规则中的第一个元素,直到你检查它为止)但它们之后的机制和效率不同

        

      当询问下一个元素时,订购剩余序列的最小值的项目等。

      如果询问下一个元素,那么不仅会再次进行任何排序,而且 要再次完成,因为myItems.OrderBy(item => item.Id).ElementAtOrDefault(i)的内容可能会在此期间发生变化

      如果您尝试使用O(n log n)获取它,那么框架版本会首先通过排序(O(n))找到该元素,然后扫描(i相对于{{ 1}})虽然.NET Core版本会通过快速选择找到它(O(n)虽然常数因子大于FirstOrDefault()但在同样的情况下可能高达O(n²)那个排序是,所以它比O(n)更慢(由于这个原因,它足够聪明,可以将ElementAtOrDefault(0)变成FirstOrDefault()。)两个版本也使用O(n)的空间复杂度(除非.NET Core可以将其转换为FirstOrDefault())。

      如果您使用myItems.OrderBy(item => item.Id).Take(k)找到前几个值,那么Framework版本将再次进行排序(O(n log n))并对结果的后续枚举设置限制,以便它停止返回获得k之后的元素。 .NET Core版本会进行部分排序,而不是在对所采用的部分进行排序的元素进行排序,这是O(n + k log k)时间复杂度。Take时间复杂度。 .NET Core还会对SkipOrderBy(cmp)的组合进行单一部分排序,从而进一步减少必要的排序量。

      理论上,只有yield的排序可能会更加懒散:

      1. 将元素加载到缓冲区中。
      2. 做分类,可能偏好“左”分区,因为正在进行分区。
      3. FirstOrDefault()元素一旦发现它们是枚举的下一个元素。
      4. 这将改善首次结果的时间(首次结果时间较短通常是其他Linq操作的一个很好的特征),特别是对可能在结束时停止工作的消费者有利。然而,它为排序操作增加了额外的恒定成本,并且要么阻止选择下一个分区,以减少递归量(基于分区的排序的重要优化),否则通常不会产生任何东西直到无论如何接近尾声(使运动毫无意义)。这也会使排序变得更加复杂。虽然我尝试了这种方法,但某些案例的回报并不能证明其他案件的成本是合理的,特别是因为它似乎可能会伤害更多的人而不是受益。

        *严格地说,几个linq操作的结果知道如何以针对每个元素优化的方式查找第一个元素,{{1}}知道如何检测任何这些情况。

答案 1 :(得分:2)

  

如果订购了序列......

这很好,但不是IEnumerable的属性,所以OrderBy永远不会直接'知道'这个。

虽然有先例,Count()会在运行时检查其IEnumerable<> source是否实际指向List,然后选择Count属性的快捷方式。

同样,OrderBy可以查看它是否在SortedList或其他东西上调用,但是没有明确的标记接口,并且这些集合的使用太少,以至于不值得努力。

还有其他方法可以优化这一点,.OrderBy().First()可以想象地映射到.Min()但是,就我所知,没有人会打扰直到现在。见Jon的回答。

答案 2 :(得分:0)

不,不是。如何在不遍历整个列表的情况下知道列表是否有序?

这是一个简单的测试:

void Main()
{
    Console.WriteLine(OrderedEnumerable().OrderBy(x => x).First());
}

public IEnumerable<int> OrderedEnumerable()
{
    Console.WriteLine(1);
    yield return 1;
    Console.WriteLine(2);
    yield return 2;
    Console.WriteLine(3);
    yield return 3;
}

正如预期的那样,输出:

1
2
3
1

答案 3 :(得分:-1)

如果查看reference source并按照classes,您将看到所有密钥都将被计算,然后快速排序算法将根据密钥对索引表进行排序。

因此,序列被读取一次,计算所有键,然后根据键对索引进行排序,然后得到第一个输出。