如何使用有限的结果计数对IEnumerable进行排序? (.OrderBy.Take的另一个实现)

时间:2013-11-08 12:18:07

标签: performance linq sorting .net-4.5 ienumerable

我有一个二进制文件,其中包含超过1亿个对象,我使用BinaryReader读取文件并返回(Yield)对象(文件阅读器和IEnumerable实现在这里:Performance comparison of IEnumerable and raising event for each item in source?

对象的一个​​属性表示对象排名(如A5)。假设我想根据属性获取已排序的top n个对象。

我看到OrderBy函数的代码:它使用QuickSort算法。我尝试使用IEnumerableOrderBy函数对Take(n)结果进行排序,但我得到了OutOfMemory异常,因为OrderBy函数创建了一个大小为total的数组对象计数实现Quicksort。

实际上,我需要的总内存是 n ,所以不需要创建大数组。例如,如果我得到Take(1000),它将只返回1000个对象,并且它不依赖于整个对象的总数。

如何使用OrderBy函数获取Take函数的结果?换句话说,我需要一个有限或阻塞的排序列表,其容量由最终用户定义。

2 个答案:

答案 0 :(得分:1)

如果你想要使用默认LINQ运算符的有序源中的前N个,那么只有选项将所有项加载到内存中,对它们进行排序并选择前N个结果:

items.Sort(condition).Take(N) // Out of memory

如果您只想排序前N个项目,那么只需先取项目,然后对它们进行排序:

items.Take(N).Sort(condition)

更新您可以使用缓冲区来保留N max个订购商品:

public static IEnumerable<T> TakeOrdered<T, TKey>(
    this IEnumerable<T> source, int count, Func<T, TKey> keySelector)
{
    Comparer<T, TKey> comparer = new Comparer<T,TKey>(keySelector);
    List<T> buffer = new List<T>();
    using (var iterator = source.GetEnumerator())
    {
        while (iterator.MoveNext())
        {
            T current = iterator.Current;
            if (buffer.Count == count)
            {
                // check if current item is less than minimal buffered item
                if (comparer.Compare(current, buffer[0]) <= 0)
                    continue;

                buffer.Remove(buffer[0]); // remove minimual item
            }
            // find index of current item
            int index = buffer.BinarySearch(current, comparer);
            buffer.Insert(index >= 0 ? index : ~index, current);
        }
    }

    return buffer;
}

此解决方案还使用项目的自定义比较器(按键比较):

public class Comparer<T, TKey> : IComparer<T>
{
    private readonly Func<T, TKey> _keySelector;
    private readonly Comparer<TKey> _comparer = Comparer<TKey>.Default;

    public Comparer(Func<T, TKey> keySelector)
    {
        _keySelector = keySelector;
    }

    public int Compare(T x, T y)
    {
        return _comparer.Compare(_keySelector(x), _keySelector(y));
    }
}

样本用法:

string[] items = { "b", "ab", "a", "abcd", "abc", "bcde", "b", "abc", "d" };
var top5byLength = items.TakeOrdered(5, s => s.Length);
var top3byValue = items.TakeOrdered(3, s => s);

答案 1 :(得分:1)

LINQ没有内置类,可以让你在没有将整个集合加载到内存中的情况下获取顶级n元素,但你可以自己构建它。

一种简单的方法是使用SortedDictionary列表:继续向其添加元素,直到达到n的限制。之后,检查您要添加的每个元素以及到目前为止找到的最小元素(即dict.Keys.First())。如果新元素较小,则将其丢弃;否则,删除最小的元素,并添加一个新元素。

在循环结束时,您的排序字典将包含最多n个元素,并且它们将根据您在字典上设置的比较器进行排序。