如果订购了序列。而且你只要求有序序列的第一个元素。 Orderby足够聪明,不能订购完整的序列吗?
IEnumerable<MyClass> myItems = ...
MyClass maxItem = myItems.OrderBy(item => item.Id).FirstOrDefault();
因此,如果询问第一个元素,则只将具有最小值的项目排序为序列的第一个元素。当询问下一个元素时,将订购具有剩余序列的最小值的项目等。
如果你只想要第一个元素,那么完整的序列是完全有序的吗?
加成
显然问题不明确。我们举个例子。
Sort函数可以执行以下操作:
代码:
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使用的排序是否如此聪明,如果您只询问第一个订购的商品,是否不对列表的其余部分进行排序?
答案 0 :(得分:3)
这取决于版本。
在框架版本(4.0,4.5等)中,然后:
FirstOrDefault
尝试通过在结果对象上使用MoveNext
和Current
来根据此映射获取第一个项目。要么找到一个,要么(如果缓冲区为空,因为源为空)返回default(TSource)
。在.NET Core中,然后:
FirstOrDefault
上的IOrderedEnumerable
操作会扫描来源。如果没有元素,则返回default(TSource)
,否则它将保留在找到的第一个元素和密钥生成器生成的密钥上,并将其与后续所有内容进行比较,替换保留的值和密钥,如果下一个,则替换为下一个found比较低于当前值。这意味着在框架版本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还会对Skip
和OrderBy(cmp)
的组合进行单一部分排序,从而进一步减少必要的排序量。
理论上,只有yield
的排序可能会更加懒散:
FirstOrDefault()
元素一旦发现它们是枚举的下一个元素。这将改善首次结果的时间(首次结果时间较短通常是其他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,您将看到所有密钥都将被计算,然后快速排序算法将根据密钥对索引表进行排序。
因此,序列被读取一次,计算所有键,然后根据键对索引进行排序,然后得到第一个输出。