6

I have a list of immutable objects (in my specific case a list of Tuple<double, double>) and I'd like to change the one with the highest Item2 value.

Ideally there would be an IndexOfMaxBy function I could use, so I could do:

var indexOfPointWithHighestItem2 = myList.IndexOfMaxBy(x => x.Item2);

var original = myList[indexOfPointWithHighestItem2];

myList[indexOfPointWithHighestItem2] = 
  new Tuple<double, double>(original.Item1, original.Item2 - 1);

I have seen How can I get LINQ to return the object which has the max value for a given property?, and using Jon Skeet's MaxBy function combined with Select I could do:

var indexOfPointWithHighestItem2 = 
  myList.Select((x, i) => new { Index = i, Value = x })
        .MaxBy(x => x.Item2).Index;

But this creates a new object for every object in my list, and there must be a neater way. Does anyone have any good suggestions?

Community
  • 1
  • 1
dominic
  • 401
  • 5
  • 12
  • Couldn't you sort your list by `Item2` and then take the first item? – Metro Smurf Mar 04 '11 at 04:04
  • 1
    @Metro: Not if we don't want the definition of `myList` to be permanently rearranged. OP wants a reference to the item's index to modify it, not just the item itself. – mellamokb Mar 04 '11 at 04:06
  • Also, sorting is nlogn instead of linear time. – dominic Mar 04 '11 at 04:12
  • 1
    If you are already using sorting for other algorithms run on the array, then the max would become constant time. It may easily be worth it depending on what other code you are writing. – Merlyn Morgan-Graham Mar 04 '11 at 05:02

2 Answers2

5

It looks like there is a FindIndex method defined on List that would be perfect for this:

double max = myList.Max(t => t.Item2);
int index = myList.FindIndex(t => t.Item2 == max);
mellamokb
  • 56,094
  • 12
  • 110
  • 136
  • +1 because the code is as short and sweet as it gets (since there is no method that directly does what the original question wants), and is still linear time. – Merlyn Morgan-Graham Mar 04 '11 at 04:26
  • I didn't know about FindIndex, so that's handy to know - thanks. However, this solution uses two passes of the list, it would be nice to use just one. – dominic Mar 04 '11 at 04:31
  • 2
    @dominic: Depending on the size of the lists in question, it may be worth it, simply to avoid having to write boilerplate code. Fewer sources of bugs, and intent is clearly documented :) On average you won't have to go through the list twice, anyhow. The worst case would be a sorted list with the maximum value at the end. – Merlyn Morgan-Graham Mar 04 '11 at 05:00
4

Well, if you wanted to, you could of course write an IndexOfMaxByextension yourself.

Example(untested):

public static int IndexOfMaxBy<TSource, TProjected>
    (this IEnumerable<TSource> source,
     Func<TSource, TProjected> selector,
     IComparer<TProjected> comparer = null
    )
{

    //null-checks here

    using (var erator = source.GetEnumerator())
    {
        if (!erator.MoveNext())
            throw new InvalidOperationException("Sequence is empty.");

        if (comparer == null)
            comparer = Comparer<TProjected>.Default;

        int index = 0, maxIndex = 0;
        var maxProjection = selector(erator.Current);

        while (erator.MoveNext())
        {
            index++;
            var projectedItem = selector(erator.Current);

            if (comparer.Compare(projectedItem, maxProjection) > 0)
            {
                maxIndex = index;
                maxProjection = projectedItem;
            }
        }
        return maxIndex;
    }
}

Usage:

var indexOfPointWithHighestItem2 = myList.IndexOfMaxBy(x => x.Item2);
Ani
  • 111,048
  • 26
  • 262
  • 307
  • 2
    why just don't use `foreach` instead of `MoveNext` and `Current`? – Oscar Mederos Mar 04 '11 at 04:24
  • 1
    Yes, this is probably the most speed-efficient approach. It would be nice though to share logic in both MaxyBy (see [morelinq](http://code.google.com/p/morelinq/)) and IndexOfMaxBy since they are so similar. – dominic Mar 04 '11 at 04:37