【问题标题】:OrderBy and Top in LINQ with good performanceLINQ 中的 OrderBy 和 Top 性能良好
【发布时间】:2010-01-16 16:20:35
【问题描述】:

从一个非常大的集合中获取前 10 条记录并使用自定义 OrderBy 的好方法是什么?如果我使用 LINQ to Objects OrderBy 方法,它会很慢并且占用大量内存,因为它会使用新顺序创建一个全新的集合。我想要一种新方法,其签名如下,它不会对整个集合重新排序并且速度非常快:

public static IEnumerable<TSource> OrderByTop<TSource, TKey>(
    IEnumerable<TSource> source,
    Func<TSource, TKey> keySelector,
    IComparer<TKey> comparer,
    int topCount)

我尝试编写它,但它变得非常复杂,我认为使用 Aggregate 或其他东西可能有更简单的方法。任何帮助将不胜感激。

回答

感谢您的帮助。我最终得到了以下代码:

public static List<TSource> OrderByTop<TSource, TKey>(
    this IEnumerable<TSource> source,
    Func<TSource, TKey> keySelector,
    IComparer<TKey> comparer,
    int topCount)
{
    var itemComparer = keySelector.ToIComparer(comparer);
    return source.Aggregate(
        new List<TSource>(topCount),
        (List<TSource> list, TSource item) =>
            list.SortedInsert(item, itemComparer, topCount));
}

列表扩展方法 SortedInsert 如下:

public static List<T> SortedInsert<T>(
    this List<T> list,
    T item,
    IComparer<T> comparer,
    int maxLength)
{
    if (list.Count == maxLength)
        if (comparer.Compare(item, list[maxLength - 1]) >= 0)
            return list;
        else
            list.RemoveAt(maxLength - 1);
    int insertIndex = list.BinarySearch(item, comparer);
    if (insertIndex < 0)
        insertIndex = ~insertIndex;
    list.Insert(insertIndex, item);
    return list;
}

对于那些感兴趣的人,我还有 keySelector 扩展方法可以转换为 IComparer。

public static IComparer<TSource> ToIComparer<TSource, TKey>(
    this Func<TSource, TKey> keySelector,
    IComparer<TKey> comparer)
{
    return new KeySelectorToIComparerConverter<TSource, TKey>(
        keySelector,
        comparer);
}
private class KeySelectorToIComparerConverter<TSource, TKey>
    : IComparer<TSource>
{
    private readonly IComparer<TKey> comparer;
    private readonly Func<TSource, TKey> keySelector;
    public KeySelectorToIComparerConverter(
        Func<TSource, TKey> keySelector,
        IComparer<TKey> comparer)
    {
        this.comparer = comparer;
        this.keySelector = keySelector;
    }
    public int Compare(TSource x, TSource y)
    {
        return comparer.Compare(keySelector(x), keySelector(y));
    }
}

【问题讨论】:

标签: c# linq performance linq-to-objects sql-order-by


【解决方案1】:

Aggregate 是一个很好的起点:

SortedList<TKey, TSource> resultlist = new SortedList<TKey, TSource>();
MyBigList.Aggregate(resultlist, (aktlist,entry) => {
   aktlist.Add(entry.Key, entry);
   if (aktlist.Count > 10) aktlist.RemoveAt(10);
   return aktlist;
});

如果你想要一个不同的比较器,你可以在SortedList的构造函数中指定一个。

编辑 正如 nikie 所说,SortedList不能包含双精度值。您可以使用标准列表和BinarySearch 来达到相同的效果:

List<TSource> resultlist = new List<TSource>();
MyBigList.Aggregate(resultlist, (aktlist, entry) => {
   int index = aktlist.BinarySearch(entry);
   if (index < 0) index = ~index;
   if (index < 10) aktlist.Insert(index, entry);
   if (aktlist.Count > 10) aktlist.RemoveAt(10);
   return aktlist;
});

同样,自定义比较器(连同自定义键选择)可用作BinarySearch 的参数。

【讨论】:

  • 当键已经存在时,IIRC SortedList 会抛出异常。
  • 非常好!它应该是 RemoveAt(10) ,就像 nikie 说的那样,它不接受重复的键。
  • 哇,我不知道 BinarySearch 为您提供了较大元素的按位补码。我给你答案!
  • 其实你可以节省很多时间,如果你添加一个插入条件(索引
【解决方案2】:

我认为你真正想要的是selection algorithm。我不知道 LINQ 是实现它的最佳方式,因为我认为它基本上最终会通过排序进行选择。您应该能够在 O(kN) 中执行此操作,其中 k 是通过迭代集合的“顶部”项目数,跟踪到目前为止看到的最小“顶部”元素以及当前元素是否大于即,用当前元素替换该元素(并更新新的最小元素)。这也很节省空间。

完成后,您可以将“顶部”元素作为有序集合返回。

注意:我在这里假设 LINQ to Objects。如果您使用的是 LINQ to SQL,那么我会简单地将排序/选择推迟到 SQL 服务器,并简单地将方法链接起来以获取 select top N ... from ... order by ... 查询。

完全未经测试,甚至没有编译。使用通用斐波那契堆实现。我很快会在我的博客 (http://farm-fresh-code.blogspot.com) 上发布代码。由于我正在做的一些优先队列实验的结果,我有一个闲逛(不确定它是否是通用的)。在此之前,请参阅 wikipedia 获取信息和伪代码。

public static IEnumerable<TSource> OrderByTop<TSource, TKey>(
    IEnumerable<TSource> source,
    Func<TSource, TKey> keySelector,
    IComparer<TKey> comparer,
    int topCount)
{
    // allocate enough space to hold the number of elements (+1 as a new candidate is added)
    FibonacciHeap<TKey,TSource> top = new FibonacciHeap<TKey,TSource>( comparer );
    foreach (var candidate in source) // O(n)
    {
         TKey key = keySelector(candidate);
         TKey minimum = top.AccessMinimum();
         if (minimum == null || comparer.Compare( key, minimum.Key ) > 0) // O(1)
         {
             top.Insert( key, candidate ); // O(1)
             if (top.Count >= topCount)
             {
                 top.DeleteMinimum(); // O(logk)
             }
         }
    }
    return top.ToList().Reverse().Select( t.Value ); // O(k)   
}

【讨论】:

  • 感谢您的链接。这就是我想要的算法类型。我希望已经用 C# 编写了类似的东西,而我不必自己编写。这似乎是一个常见问题,应该已经有了很好的解决方案。
  • 感谢您的代码,但我选择了 MartinStettner 的版本,因为他处理重复项并始终保持列表排序。
  • 我真的想不出任何简单的方法来扩展重复键,而不会使更复杂、更昂贵,或者更改为使用排序堆——或者使用相同的 BinarySearch 技巧。我有一个斐波那契堆实现,它是 O(1) min/insert 和 O(logn) delete,但这会添加很多代码。使用它会导致 O(logkN) 但就像我说的那样需要堆实现。
【解决方案3】:

我不知道除了写这个方法之外的其他解决方案。不过这个方法应该没那么复杂。

您需要维护一个包含前 10 个元素的排序列表,并遍历原始集合一次。

如果迭代期间的当前记录小于前 10 列表中的最后一条,或者当您还没有前 10 条记录时,则必须将该项目添加到此列表中。 (当然,在适当的时候,从前 10 名列表中删除最后一项。)

【讨论】:

    【解决方案4】:

    你也可以实现一个分而治之的排序算法,比如快速排序,一旦你有前 k 个排序的元素就中断。但是如果 k tvanfosson 的建议可能会更快

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-08-14
      • 1970-01-01
      • 2012-07-25
      • 2020-06-10
      • 1970-01-01
      相关资源
      最近更新 更多