Хобрук: Ваш путь к мастерству в программировании

Использование LINQ для получения 100 лучших и 100 худших?

Я хотел бы сделать что-то подобное (ниже), но не уверен, есть ли для этого формальный/оптимизированный синтаксис?

.Orderby(i => i.Value1)
.Take("Bottom 100 & Top 100")
.Orderby(i => i.Value2);

в основном, я хочу отсортировать по одной переменной, затем взять первые 100 и нижние 100, а затем отсортировать эти результаты по другой переменной.

Какие-либо предложения?

03.08.2014

  • Содержит ли ваш список более 200 элементов каждый раз, или нужно учитывать, что 100 лучших и 100 последних могут иметь одни и те же записи списка? 03.08.2014
  • Было бы лучше написать собственный метод расширения с именем: TakeLastAndFirst(number) 03.08.2014
  • Какой поставщик LINQ вы используете? LINQ для объектов? LINQ для сущностей? Что-то другое? 03.08.2014

Ответы:


1
var sorted = list.OrderBy(i => i.Value);
var top100 = sorted.Take(100);
var last100 = sorted.Reverse().Take(100);
var result = top100.Concat(last100).OrderBy(i => i.Value2);

Я не знаю, хотите ли вы Concat или Union в конце. Concat объединит все записи обоих списков, даже если есть похожие записи, что может быть в том случае, если исходный список содержит менее 200 записей. Union будет добавлять только материалы из last100, которых еще нет в top100.

Некоторые вещи, которые не ясны, но которые следует учитывать:

  • Если список представляет собой IQueryable для базы данных, возможно, целесообразно использовать ToArray() или ToList(), например

    var sorted = list.OrderBy(i => i.Value).ToArray();
    

    с начала. Таким образом, выполняется только один запрос к базе данных, а остальные выполняются в памяти.

  • Метод Reverse не оптимизирован так, как я надеялся, но это не должно быть проблемой, так как упорядочивание списка — это реальное дело. Для справки, метод пропуска, описанный в других ответах здесь, вероятно, немного быстрее, но ему необходимо знать количество элементов в списке.

  • Если бы список был бы LinkedList или другим классом, реализующим IList, метод Reverse можно было бы сделать оптимизированным способом.

03.08.2014
  • Это может быть действительно большой проблемой производительности, если у нас есть list как IQueryable для БД, который возвращает тысячи расширяемых записей - я почти уверен, что Reverse будет читать целую кучу записей из БД для работы. Я думаю, что list.Skip(list.Count() - 100).Take(100) будет лучше вместо reverse ... take, даже с дополнительным запросом к Count 03.08.2014
  • Я надеялся, что Reverse будет оптимизирован для определенных типов IEnumerable, таких как LinkedList, например. ОП пока не сказал, от чего мы запрашиваем. Я бы добавил ToArray в первую строку, чтобы сделать это всего лишь одним запросом для БД. 03.08.2014
  • Многократное перечисление может быть проблемой, если список не находится в памяти (например, IQueryable). 03.08.2014
  • @T_D ToArray() не очень поможет, например, для 600K+ элементов с точки зрения производительности. 03.08.2014
  • Это выглядит очень неэффективно. Если я не ошибаюсь, сортировка на самом деле будет выполняться дважды (большинство расширений Enumerable переоцениваются при каждом перечислении, и я считаю, что OrderBy не является исключением), а затем Reverse придется буферизовать всю последовательность, которая занимает столько же памяти как просто преобразование в список (ToList). Таким образом, вы получаете худшее из всех миров, памяти и времени работы. Было бы лучше просто использовать два сорта, один OrderBy и OrderByDescending. 04.08.2014

  • 2

    Вы можете использовать метод расширения следующим образом:

    public static IEnumerable<T> TakeFirstAndLast<T>(this IEnumerable<T> source, int count)
    {
        var first = new List<T>();
        var last = new LinkedList<T>();
        foreach (var item in source)
        {
            if (first.Count < count)
                first.Add(item);
            if (last.Count >= count)
                last.RemoveFirst();
            last.AddLast(item);
        }
    
        return first.Concat(last);
    }
    

    (Я использую LinkedList<T> для last, потому что он может удалять элементы в O(1))

    Вы можете использовать его следующим образом:

    .Orderby(i => i.Value1)
    .TakeFirstAndLast(100)
    .Orderby(i => i.Value2);
    

    Обратите внимание, что он не обрабатывает случай, когда элементов меньше 200: в этом случае вы получите дубликаты. При необходимости их можно удалить с помощью Distinct.

    03.08.2014
  • +1 Я также ответил на вопрос методом расширения. Но в моей функции мне нужно общее количество элементов в Enumerable, чтобы найти последние N элементов. Но, это не нужно. 03.08.2014

  • 3

    Возьмите верхние 100 и нижние 100 отдельно и объедините их:

    var tempresults = yourenumerable.OrderBy(i => i.Value1);
    var results = tempresults.Take(100);
    results = results.Union(tempresults.Skip(tempresults.Count() - 100).Take(100))
                     .OrderBy(i => i.Value2);
    
    03.08.2014
  • Union будет пропускать повторяющиеся записи, вместо этого вам может понадобиться использовать Concat 03.08.2014
  • Это будет основано на запросе ОП. Если он не хочет дубликатов, союз будет в порядке. 03.08.2014
  • Я могу ошибаться, но оп говорит, что верхние 100 и нижние 100. Что интуитивно означает, что мне нужно 200 элементов. Во всяком случае, пусть ОП скажет :) 03.08.2014
  • Это делает три операции сортировки, хотя я думаю, что нужны только две. 03.08.2014
  • ОП запросил 100 лучших (1 сорт), 100 нижних (2-й сорт) и сортировку результатов по другому элементу (3-й сорт). Так что я думаю, что это по его просьбе. 03.08.2014
  • 100 лучших и 100 последних можно взять из списка, который был отсортирован только один раз ;) 03.08.2014
  • @T_D Что бы вы подумали о моем редактировании? Я думаю, что это сработает, но это добавило счет. 03.08.2014
  • Count() будет оценивать всю последовательность, поэтому это значительно медленнее, чем императивное решение, и я полагаю, что также может вызвать исключение, если элементов меньше 100. 04.08.2014

  • 4

    Вы можете сделать это с помощью одного оператора, также используя этот .Where перегружается, если у вас есть доступное количество элементов:

    var elements = ...
    
    var count = elements.Length; // or .Count for list
    
    var result = elements
        .OrderBy(i => i.Value1)
        .Where((v, i) => i < 100 || i >= count - 100)
        .OrderBy(i => i.Value2)
        .ToArray();             // evaluate
    

    Вот как это работает:

    | first 100 elements | middle elements | last 100 elements |
            i < 100        i < count - 100    i >= count - 100
    
    03.08.2014
  • Будет работать, но нужно будет проверять все записи, а не просто брать первую и последнюю 100. Так что с очень длинными списками это может привести к снижению производительности. 03.08.2014
  • @T_D Не уверен, что это займет слишком много времени. Это просто счетчик, никаких побочных эффектов (насколько я знаю). 03.08.2014
  • @T_D Доминирующей операцией здесь является сортировка. Таким образом, один дополнительный линейный поиск кажется неуместным. 03.08.2014

  • 5

    Вы можете написать свой собственный метод расширения, например Take(), Skip() и другие методы класса Enumerable. Он будет принимать количество элементов и общую длину в списке в качестве входных данных. Затем он вернет первый и последний N элементы из последовательности.

    var result = yourList.OrderBy(x => x.Value1)
                         .GetLastAndFirst(100, yourList.Length)
                         .OrderBy(x => x.Value2)
                         .ToList();
    

    Вот метод расширения:

    public static class SOExtensions
    {
        public static IEnumerable<T> GetLastAndFirst<T>(
            this IEnumerable<T> seq, int number, int totalLength
        )
        {
            if (totalLength < number*2) 
                throw new Exception("List length must be >= (number * 2)");
    
            using (var en = seq.GetEnumerator())
            {
                int i = 0;
    
                while (en.MoveNext())
                {
                    i++;
                    if (i <= number || i >= totalLength - number) 
                         yield return en.Current;
                }
            }
        }
    }
    
    03.08.2014
  • Будет работать, но обязательно просматривает все элементы в середине первых 100 и последних 100 и не использует оптимизацию для массивов или списков, как это делают собственные методы LINQ. 03.08.2014
  • @T_D Какие оптимизации типов выполняют собственные методы LINQ? Не могли бы вы объяснить. 03.08.2014
  • После небольшого исследования я должен признать, что у меня сложилось неправильное впечатление, потому что некоторое время назад я заглянул сюда: referencesource.microsoft.com/#q=Enumerable, где проверяется, является ли IEnumerable списком для более быстрого индексирования, но почти все другие методы LINQ не проверяют подобные вещи. Reverse, например, создает буфер всех элементов и начинается с конца. Хотя, например, для LinkedList было бы намного проще. Так что извините за неправильный комментарий. 03.08.2014
  • @T_D Нет проблем. Я думал так же, как и вы, до того, как прочитал книгу, в которой говорилось о What does LINQ compiled to in CLR?. Большинство методов LINQ выполняются с шаблоном deferred execution. И CLR инкапсулирует связанную информацию, такую ​​как исходная последовательность, предикат или селектор (если есть), в итератор, который будет использоваться при извлечении информации из исходной последовательности с помощью метода ToList или метода ForEach или вручную с помощью метода базовые методы GetEnumerator и MoveNext. 03.08.2014
  • Да, я понимаю эту концепцию, но я думал, что это будет более умно;) 03.08.2014
  • Новые материалы

    Основы принципов S.O.L.I.D, Javascript, Git и NoSQL
    каковы принципы S.O.L.I.D? Принципы SOLID призваны помочь разработчикам создавать надежные, удобные в сопровождении приложения. мы видим пять ключевых принципов. Принципы SOLID были разработаны..

    Как настроить Selenium в проекте Angular
    Угловой | Селен Как настроить Selenium в проекте Angular Держите свое приложение Angular и тесты Selenium в одной рабочей области и запускайте их с помощью Mocha. В этой статье мы..

    Аргументы прогрессивного улучшения почти всегда упускают суть
    В наши дни в кругах веб-разработчиков много болтают о Progressive Enhancement — PE, но на самом деле почти все аргументы с обеих сторон упускают самую фундаментальную причину, по которой PE..

    Введение в Джанго Фреймворк
    Схема «работать умно, а не усердно» В этой и последующих статьях я познакомлю вас с тем, что такое фреймворк Django и как создать свое первое приложение с помощью простых и понятных шагов, а..

    Настольный ПК как «одно кольцо, чтобы править всеми» домашних компьютеров
    Вид после 9 месяцев использования С настольных компьютеров все началось, но в какой-то момент они стали «серверами», и мы все перешли на ноутбуки. В прошлом году я столкнулся с идеей настольных..

    Расширенные методы безопасности для VueJS: реализация аутентификации без пароля
    Руководство, которое поможет вам создавать безопасные приложения в долгосрочной перспективе Безопасность приложений часто упускается из виду в процессе разработки, потому что основная..

    стройный-i18следующий
    Представляем стройную оболочку для i18next. Эта библиотека, основанная на i18next, заключает экземпляр i18next в хранилище svelte и отслеживает события i18next, такие как languageChanged,..