OrderBy и Top в LINQ с хорошей производительностью
Что такое хороший способ получить 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 Extension для преобразования в 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));
}
}
Ответы
Ответ 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
.
EDIT Как упоминалось в 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
.
Ответ 2
Я думаю, что вы действительно являетесь алгоритмом выбора. Я не знаю, что LINQ - лучший способ реализовать его, поскольку я думаю, что он в основном заканчивается выбором путем сортировки. Вы должны иметь возможность сделать это в O (kN), где k - это "верхнее" количество элементов, итерируя через коллекцию, отслеживая минимальный "верхний" элемент, увиденный до сих пор, и если текущий элемент больше, чем что, заменяя этот элемент на текущий элемент (и обновляя новый минимальный элемент). Это также экономит место.
Когда вы закончите, вы можете вернуть "верхние" элементы в виде упорядоченной коллекции.
Примечание. Я предполагаю LINQ для объектов здесь. Если вы используете 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)
}
Ответ 3
Я не знаю другого решения, кроме написания этого метода. Однако этот метод не должен быть таким сложным.
Вам нужно сохранить отсортированный список с 10 верхними элементами и выполнить однократную повторную сортировку коллекции.
Если текущая запись во время итерации меньше, чем последняя из списка 10 лучших, или когда у вас еще нет первых 10 записей, вам нужно добавить элемент в этот список. (И, конечно, удалите последний элемент из списка лучших 10, если это необходимо.)
Ответ 4
Вы также можете реализовать алгоритм сортировки с разделением и победой, например, quicksort и break, как только у вас будут первые отсортированные элементы. Но предложение tvanfosson, вероятно, быстрее, если k < N