Как выполнить .Max () для свойства всех объектов в коллекции и вернуть объект с максимальным значением

У меня есть список объектов, у которых есть два свойства int. Список является результатом другого запроса linq. Объект:

public class DimensionPair  
{
    public int Height { get; set; }
    public int Width { get; set; }
}

Я хочу найти и вернуть в списке объект с наибольшим значением свойства Height.

Мне удается получить максимальное значение Height, но не сам объект.

Могу ли я сделать это с помощью Linq? Как?


person theringostarrs    schedule 09.07.2009    source источник
comment
var maxDimension = sizes.OrderByDesc (x = ›x.Height) .FirstOrDefault ();   -  person Fritjof Berggren    schedule 18.12.2015
comment
Какая простая и полезная функция. Функция MaxBy должна быть в стандартной библиотеке. Мы должны отправить запрос функции в Microsoft github.com/dotnet/corefx   -  person Colonel Panic    schedule 21.07.2017


Ответы (9)


У нас есть метод расширения, чтобы сделать это в MoreLINQ. Вы можете посмотреть на реализацию там, но в основном это случай итерации по данным, запоминания максимального элемента, который мы видели до сих пор, и максимального значения, которое он произвел в рамках проекции.

В вашем случае вы бы сделали что-то вроде:

var item = items.MaxBy(x => x.Height);

Это лучше (IMO), чем любое из представленных здесь решений, кроме второго решения Мехрдада (которое в основном совпадает с MaxBy):

  • Это O (n), в отличие от предыдущего принятого ответа, который находит максимальное значение на каждой итерации (что делает его O (n ^ 2 ))
  • Решение для упорядочивания - O (n log n)
  • Принимая значение Max, а затем находя первый элемент с этим значением, является O (n), но повторяет последовательность дважды. По возможности следует использовать LINQ в однопроходном режиме.
  • Ее намного проще читать и понимать, чем агрегированную версию, и она оценивает проекцию только один раз для каждого элемента.
person Jon Skeet    schedule 09.07.2009
comment
В расширениях Linq уже есть метод Max которое вы также можете использовать с лямбда-выражением var item = items.Max(x => x.Height); - person Sebastien GISSINGER; 22.04.2014
comment
@sgissinger: Вы упустили суть - это даст максимальную высоту в коллекции, а не элемент с максимальной высотой. - person Jon Skeet; 22.04.2014
comment
FWIW это агрегированная версия: items.Aggregate((i, j) => i.Height > j.Height ? i : j) - person orad; 19.07.2014
comment
@orad: Да, хватит, но гораздо менее выразительно, ИМО. - person Jon Skeet; 19.07.2014
comment
MaxBy / MinBy отлично справились со своей задачей. +1 За подарок MoreLinq! Спасибо @JonSkeet (кстати, добавлен MoreLinq через NuGet) - person Vitox; 19.12.2014
comment
Я со всем уважением не согласен с тем, что код MaxBy более выразителен. Я считаю, что при адекватном знакомстве с функциональным программированием люди действительно сочли бы метод агрегирования более выразительным. Чтобы использовать селектор для максимального элемента на каждой итерации, его можно сохранить как Tuple (Item, ComparisonValue). - person jam40jeff; 24.03.2015
comment
@ jam40jeff: MaxBy говорит именно о том, что вы от него хотите. В основном перевод не требуется. Вы бы предпочли также использовать Aggregate вместо Count()? Это так же возможно, но менее читабельно, ИМО ... - person Jon Skeet; 24.03.2015
comment
Я согласен с тем, что определение метода расширения MaxBy является наиболее выразительным. Я имел в виду содержание метода MaxBy. Я считаю, что было бы более читаемым, если бы он был реализован с использованием метода Aggregate, но это всего лишь мнение. - person jam40jeff; 24.03.2015
comment
@ jam40jeff: А, понятно. Не думаю, что когда-либо сравнивал реализацию MaxBy с агрегированной версией. Стоит отметить, что текущая реализация выполняет селектор только один раз для каждого элемента ввода, а не дважды, как это было бы при наивной агрегации. В некоторых случаях это может быть значительным. - person Jon Skeet; 24.03.2015
comment
Верно, хотя упомянутый мною Tuple может обойти это (совокупное значение может быть как максимальным элементом, так и его проекцией). Однако это, вероятно, еще больше снизит читаемость. Оба подхода одинаково действенны, я просто комментировал, что выразительность более субъективна. - person jam40jeff; 24.03.2015
comment
Это возвращает не один объект, а коллекцию. - person liang; 23.02.2021
comment
@liang: Судя по всему, со временем он изменился. Конечно, когда я писал первую версию, она возвращала единственный элемент. Похоже, что в марте 2018 года все изменилось. (У меня нет времени вернуться и найти все ответы, которые я упомянул о MaxBy в Stack Overflow. Похоже, что если вы используете версию 2.x, вы все равно получите одноэлементная версия.) - person Jon Skeet; 23.02.2021

Для этого потребуется сортировка (O (n log n)), но это очень просто и гибко. Еще одно преимущество - возможность использовать его с LINQ to SQL:

var maxObject = list.OrderByDescending(item => item.Height).First();

Обратите внимание, что это дает преимущество перечисления последовательности list только один раз. Хотя это может не иметь значения, если list является List<T>, который за это время не меняется, это может иметь значение для произвольных IEnumerable<T> объектов. Ничто не гарантирует, что последовательность не изменится в разных перечислениях, поэтому методы, которые делают это несколько раз, могут быть опасными (и неэффективными, в зависимости от характера последовательности). Однако это все еще далеко не идеальное решение для больших последовательностей. Я предлагаю написать собственное MaxObject расширение вручную, если у вас большой набор элементов, чтобы можно было сделать это за один проход без сортировки и прочего (O (n)):

static class EnumerableExtensions {
    public static T MaxObject<T,U>(this IEnumerable<T> source, Func<T,U> selector)
      where U : IComparable<U> {
       if (source == null) throw new ArgumentNullException("source");
       bool first = true;
       T maxObj = default(T);
       U maxKey = default(U);
       foreach (var item in source) {
           if (first) {
                maxObj = item;
                maxKey = selector(maxObj);
                first = false;
           } else {
                U currentKey = selector(item);
                if (currentKey.CompareTo(maxKey) > 0) {
                    maxKey = currentKey;
                    maxObj = item;
                }
           }
       }
       if (first) throw new InvalidOperationException("Sequence is empty.");
       return maxObj;
    }
}

и используйте его с:

var maxObject = list.MaxObject(item => item.Height);
person mmx    schedule 09.07.2009
comment
Крысы, не видел этого до того, как опубликовал. В основном это то, что мы делаем в MoreLINQ, за исключением того, что мы выполняем итерацию напрямую с помощью GetEnumerator, а не с помощью первого флага. Думаю, это немного упрощает код. Мы также разрешаем использовать Comparer ‹U› и позволяем передавать один, вместо того, чтобы требовать от U реализации IComparable, но это побочный вопрос. - person Jon Skeet; 09.07.2009
comment
Последняя часть представляет собой множество ненужных сложностей для простой задачи. - person KristoferA; 09.07.2009
comment
Это решение более общей проблемы. По сути, вы объявляете такой метод расширения, чтобы абстрагироваться от сложности, когда вам это нужно. - person mmx; 09.07.2009
comment
Разве не так работает разработка программного обеспечения? - person mmx; 09.07.2009
comment
Для полноты картины, есть ли причина предпочесть OrderByDescending().First() или OrderBy().Last()? Я использую второй вариант в своем коде (короче, по крайней мере), потому что я думаю, что оба варианта будут иметь одинаковую стоимость? (отредактируйте: ну, неважно ... stackoverflow .com / questions / 6680666 /). - person heltonbiker; 17.06.2014

Выполнение заказа с последующим выбором первого элемента - это трата времени на размещение элементов после первого. Вам наплевать на их порядок.

Вместо этого вы можете использовать агрегатную функцию, чтобы выбрать лучший элемент на основе того, что вы ищете.

var maxHeight = dimensions
    .Aggregate((agg, next) => 
        next.Height > agg.Height ? next : agg);

var maxHeightAndWidth = dimensions
    .Aggregate((agg, next) => 
        next.Height >= agg.Height && next.Width >= agg.Width ? next: agg);
person Cameron MacFarland    schedule 09.07.2009
comment
Значит, это O (n)? И как он обрабатывает пустой список? - person André C. Andersen; 14.04.2013
comment
Это должен быть принятый ответ. Любой другой метод будет перебирать объекты более одного раза или использовать ненужную библиотеку. См. Также: stackoverflow.com/questions/3188693/ - person Seattle Leonard; 26.11.2013
comment
maxHeightAndWidth будет работать непоследовательно в зависимости от порядка в списке. Например, если у вас есть только {1, 3} и {3, 1}, он вернет первый элемент в списке независимо от порядка. Так что на самом деле это бесполезно. - person meustrus; 06.08.2015
comment
@meustrus Для maxHeightAndWidth {1, 3} и {3, 1} эквивалентны, поскольку нет порядка значений (высота перед шириной или ширина перед высотой). Поскольку они эквивалентны, не должно иметь значения, что возвращается первым. - person Cameron MacFarland; 07.08.2015
comment
В худшем случае это работает так же плохо, как MaxBy, не так ли? MaxBy будет перебирать весь список, как и этот. - person Christophe De Troyer; 26.08.2018
comment
@ChristopheDeTroyer невозможно найти максимальное значение без повторения всего списка. - person Koja; 28.11.2019
comment
Это действительно было моей точкой зрения. - person Christophe De Troyer; 29.11.2019

А почему бы тебе не попробовать с этим ??? :

var itemsMax = items.Where(x => x.Height == items.Max(y => y.Height));

ИЛИ больше оптимизировать:

var itemMaxHeight = items.Max(y => y.Height);
var itemsMax = items.Where(x => x.Height == itemMaxHeight);

М-м-м ?

person Dragouf    schedule 13.06.2011
comment
Потому что это O (n ^ 2). Для каждого элемента в списке он просматривает все элементы и находит тот, который имеет наибольшую высоту, затем проверяет, имеет ли текущий элемент такую ​​же высоту, и возвращает его. Прекрасно подходит для небольших списков, но при просмотре более 100 пунктов вы заметите разницу. - person Cameron MacFarland; 26.03.2012
comment
Вы правы, поэтому перед извлечением items.Max (y = ›y.Height) из лямбда-выражения необходимо сохранить его в переменной. var itemMaxHeight = items.Max (y = ›y.Height); var item = items.Where (x = ›x.Height == itemMaxHeight); Я написал это быстро, потому что подумал, что это может быть более простой способ сделать это. Но для оптимизации лучше это сделать, это правда. - person Dragouf; 26.03.2012
comment
Более оптимизированная версия теперь O (2n), что лучше, но теперь требует временной переменной и 2 строк, и все еще не O (n), как некоторые другие ответы. пожать плечами - person Cameron MacFarland; 27.03.2012
comment
вы уверены, что результат items.Max (y = ›y.Height) не будет кэширован? Также я подумал (что касается нотации большого O) O (2n) было таким же, как O (n)? - person hdgarrood; 04.04.2013
comment
Я обнаружил, что в LINQ to SQL первая версия создает «лучший» SQL. Результатом будет только один вызов базы данных вместо двух, и план выполнения выглядит лучше, когда полученный SQL запускается в SQL Server Management Studio. - person David Dowdle; 29.01.2014
comment
@CameronMacFarland Я хочу указать, что математически говоря, O (n) = O (2n), факторы, которые имеют значение для сложности, - это экспоненты и степени. (Возможно, я выражаю это неточно, так как я когда-то изучал это, если кто-то может выразить это лучше, не стесняйтесь редактировать мой пост) - person Souhaieb Besbes; 14.08.2015

Ответы пока прекрасны! Но я вижу необходимость в решении со следующими ограничениями:

  1. Простой и лаконичный LINQ;
  2. O (n) сложность;
  3. Не оценивайте свойство более одного раза для каждого элемента.

Вот:

public static T MaxBy<T, R>(this IEnumerable<T> en, Func<T, R> evaluate) where R : IComparable<R> {
    return en.Select(t => new Tuple<T, R>(t, evaluate(t)))
        .Aggregate((max, next) => next.Item2.CompareTo(max.Item2) > 0 ? next : max).Item1;
}

public static T MinBy<T, R>(this IEnumerable<T> en, Func<T, R> evaluate) where R : IComparable<R> {
    return en.Select(t => new Tuple<T, R>(t, evaluate(t)))
        .Aggregate((max, next) => next.Item2.CompareTo(max.Item2) < 0 ? next : max).Item1;
}

Использование:

IEnumerable<Tuple<string, int>> list = new[] {
    new Tuple<string, int>("other", 2),
    new Tuple<string, int>("max", 4),
    new Tuple<string, int>("min", 1),
    new Tuple<string, int>("other", 3),
};
Tuple<string, int> min = list.MinBy(x => x.Item2); // "min", 1
Tuple<string, int> max = list.MaxBy(x => x.Item2); // "max", 4
person meustrus    schedule 06.08.2015

Я считаю, что сортировка по столбцу, для которого вы хотите получить MAX, а затем захват первого, должна работать. Однако, если есть несколько объектов с одинаковым значением MAX, будет захвачен только один:

private void Test()
{
    test v1 = new test();
    v1.Id = 12;

    test v2 = new test();
    v2.Id = 12;

    test v3 = new test();
    v3.Id = 12;

    List<test> arr = new List<test>();
    arr.Add(v1);
    arr.Add(v2);
    arr.Add(v3);

    test max = arr.OrderByDescending(t => t.Id).First();
}

class test
{
    public int Id { get; set; }
}
person regex    schedule 09.07.2009

В NHibernate (с NHibernate.Linq) вы можете сделать это следующим образом:

return session.Query<T>()
              .Single(a => a.Filter == filter &&
                           a.Id == session.Query<T>()
                                          .Where(a2 => a2.Filter == filter)
                                          .Max(a2 => a2.Id));

Что будет генерировать SQL следующим образом:

select *
from TableName foo
where foo.Filter = 'Filter On String'
and foo.Id = (select cast(max(bar.RowVersion) as INT)
              from TableName bar
              where bar.Name = 'Filter On String')

Что мне кажется довольно эффективным.

person Tod Thomson    schedule 01.05.2013

Основываясь на первоначальном ответе Кэмерона, вот что я только что добавил в свою расширенную версию FloatingWindowHost библиотеки SilverFlow (копирование из FloatingWindowHost.cs по адресу http://clipflair.codeplex.com исходный код)

    public int MaxZIndex
    {
      get {
        return FloatingWindows.Aggregate(-1, (maxZIndex, window) => {
          int w = Canvas.GetZIndex(window);
          return (w > maxZIndex) ? w : maxZIndex;
        });
      }
    }

    private void SetTopmost(UIElement element)
    {
        if (element == null)
            throw new ArgumentNullException("element");

        Canvas.SetZIndex(element, MaxZIndex + 1);
    }

В отношении приведенного выше кода стоит отметить, что Canvas.ZIndex - это присоединенное свойство, доступное для UIElements в различных контейнерах, а не только используемое при размещении на Canvas (см. Управление порядком рендеринга (ZOrder) в Silverlight без использования элемента управления Canvas). Думаю, можно было бы даже легко создать статические методы расширения SetTopmost и SetBottomMost для UIElement, адаптировав этот код.

person George Birbilis    schedule 06.12.2013
comment
упс, извините, не видел, что вы хотите вернуть объект, а не значение - в любом случае оставив здесь ответ для всех, кто задается вопросом, как вернуть максимальное значение, как я - person George Birbilis; 06.12.2013

Вы также можете обновить решение Мехрдада Афшари, переписав метод расширения на более быстрый (и более красивый):

static class EnumerableExtensions
{
    public static T MaxElement<T, R>(this IEnumerable<T> container, Func<T, R> valuingFoo) where R : IComparable
    {
        var enumerator = container.GetEnumerator();
        if (!enumerator.MoveNext())
            throw new ArgumentException("Container is empty!");

        var maxElem = enumerator.Current;
        var maxVal = valuingFoo(maxElem);

        while (enumerator.MoveNext())
        {
            var currVal = valuingFoo(enumerator.Current);

            if (currVal.CompareTo(maxVal) > 0)
            {
                maxVal = currVal;
                maxElem = enumerator.Current;
            }
        }

        return maxElem;
    }
}

А потом просто используйте это:

var maxObject = list.MaxElement(item => item.Height);

Это имя будет понятно людям, использующим C ++ (потому что там есть std :: max_element).

person Maciej Oziębły    schedule 27.12.2014