这是一个基于
PriorityQueue<TElement, TPriority>
集合的可枚举序列 Linqy
TopN
操作符:
public static IEnumerable<T> TopN<T>(this IEnumerable<T> source, int n,
IComparer<T> comparer = default)
{
ArgumentNullException.ThrowIfNull(source);
if (n < 1) throw new ArgumentOutOfRangeException(nameof(n));
PriorityQueue<bool, T> top = new(comparer);
foreach (var item in source)
{
if (top.Count < n)
top.Enqueue(default, item);
else
top.EnqueueDequeue(default, item);
}
List<T> topList = new(top.Count);
while (top.TryDequeue(out _, out var item)) topList.Add(item);
for (int i = topList.Count - 1; i >= 0; i--) yield return topList[i];
}
使用示例:
IEnumerable<double> topValues = values.TopN(k);
topValues
序列包含 values
中前 k
个最大的值,按降序排列。如果在 topValues
中存在重复的值,则相等值的顺序是未定义的(非稳定排序)。
对于基于 SortedSet<T>
的实现,可以查看此答案的 第5版,它可以在 .NET 6 之前的版本上编译。
在编程方面,PartialSort
操作符与 MoreLinq 包中的操作符功能类似。但是它的实现并不是最优的(源代码)。它对于每个项目都执行二进制搜索,而不是将其与 top
列表中的最小项进行比较,导致比必要的更多的比较。
令人惊讶的是,LINQ本身对于OrderByDescending
+Take
组合进行了良好的优化,从而实现了出色的性能。它只比上面的TopN
运算符稍慢一点。这适用于所有版本的.NET Core以及之后的版本(.NET 5和.NET 6)。但在.NET Framework平台上不适用,其复杂度如预期的O(n*log n)。
可以在这里找到一个比较4种不同方法的演示。
values.OrderByDescending(x => x).Take(k)
。
values.OrderByDescending(x => x).HideIdentity().Take(k)
,其中HideIdentity
是一个微不足道的LINQ传播器,它隐藏了基础可枚举对象的身份,因此有效地禁用了LINQ优化。
values.PartialSort(k, MoreLinq.OrderByDirection.Descending)
(MoreLinq)。
values.TopN(k)
以下是在.NET 6上以发布模式运行时演示的典型输出:
.NET 6.0.0-rtm.21522.10
Extract the 100 maximum elements from 2,000,000 random values, and calculate the sum.
OrderByDescending+Take Duration: 156 msec, Comparisons: 3,129,640, Sum: 99.997344
OrderByDescending+HideIdentity+Take Duration: 1,415 msec, Comparisons: 48,602,298, Sum: 99.997344
MoreLinq.PartialSort Duration: 277 msec, Comparisons: 13,999,582, Sum: 99.997344
TopN Duration: 62 msec, Comparisons: 2,013,207, Sum: 99.997344
OrderBy(...).Take(...)
的低效率点个赞。我在这里和其他地方看到的OrderBy(...).First()
的次数令人沮丧。如果微软将其纳入LINQ中,并通过对Take
、First
等进行特殊重载来处理IOrderedEnumerable
,那将会很有趣。 - Rawling