-2

I have a list, and I want to select the fifth highest element from it:

List<int> list = new List<int>();

list.Add(2);
list.Add(18);
list.Add(21);
list.Add(10);
list.Add(20);
list.Add(80);
list.Add(23);
list.Add(81);
list.Add(27);
list.Add(85);

But OrderbyDescending is not working for this int list...

sɐunıɔןɐqɐp
  • 3,332
  • 15
  • 36
  • 40
Vishal Saini
  • 173
  • 1
  • 4
  • 2
    See https://stackoverflow.com/q/21726334/613130 for an explanation about what is happening. – xanatos Jun 06 '18 at 07:55
  • Note that by using a `OrderBy` you are transforming what should be a O(N) operation in a O(NlogN) operation. – xanatos Jun 06 '18 at 07:56
  • @Fabjan `OrderBy` doesn't "order" the `list`. It returns a ordered `IEnumerable<>`. He is probably doing `list.OrderByDescending(x => x);`, no assignment. – xanatos Jun 06 '18 at 07:57
  • 2
    @xanatos It would be nice if OP has added an [minimal complete and verifiable example](https://stackoverflow.com/help/mcve) where he shows what he *tried* to do and what exactly is not working – Fabjan Jun 06 '18 at 07:58
  • @Fabjan I always thought that a minimum level of long-distance mind-reading was necessary on SO :-) But you are right. – xanatos Jun 06 '18 at 08:05
  • Some interesting theoretical knowledge about picking the nth highest/lowest element in an unsorted collection: https://stackoverflow.com/q/251781/613130 – xanatos Jun 06 '18 at 08:10
  • 1
    You should specify what to do when there are 4 or less elements, when that is possible. – bommelding Jun 06 '18 at 09:42

6 Answers6

2
int fifth = list.OrderByDescending(x => x).Skip(4).First();
bommelding
  • 2,969
  • 9
  • 14
  • This approach throws an exception when the list is too small, which can lead to performance issues. @bommelding: you need to add an `if(list.Count >= 5)` guard wrapping this code to mitigate the issue. – sɐunıɔןɐqɐp Jun 06 '18 at 08:15
  • Agreed, but I will leave it as-is, unless the OP specifies it better. A Count could cost, and what to return for fewer items? The contract could guarantee there always is a fifth. – bommelding Jun 06 '18 at 08:18
  • The cost of the `if` guard is NONE compared with the cost of creating the enumerable AND sorting everything with `OrderByDescending`. – sɐunıɔןɐqɐp Jun 06 '18 at 09:38
  • I said _could_ , like when the source is not an in-memory List but a Queryable. – bommelding Jun 06 '18 at 09:41
1

Without LINQ expressions:

int result;
if(list != null && list.Count >= 5)
{
    list.Sort();
    result = list[list.Count - 5];
}
else // define behavior when list is null OR has less than 5 elements

This has a better performance compared to LINQ expressions, although the LINQ solutions presented in my second answer are comfortable and reliable.

In case you need extreme performance for a huge List of integers, I'd recommend a more specialized algorithm, like in Matthew Watson's answer.

Attention: The List gets modified when the Sort() method is called. If you don't want that, you must work with a copy of your list, like this:

List<int> copy = new List<int>(original);

List<int> copy = original.ToList();
sɐunıɔןɐqɐp
  • 3,332
  • 15
  • 36
  • 40
1

Depending on the severity of the list not having more than 5 elements you have 2 options.

If the list never should be over 5 i would catch it as an exception:

int fifth;
try
{
    fifth = list.OrderByDescending(x => x).ElementAt(4);
}
catch (ArgumentOutOfRangeException)
{
    //Handle the exception
}

If you expect that it will be less than 5 elements then you could leave it as default and check it for that.

int fifth = list.OrderByDescending(x => x).ElementAtOrDefault(4);

if (fifth == 0)
{
    //handle default
}

This is still some what flawed because you could end up having the fifth element being 0. This can be solved by typecasting the list into a list of nullable ints at before the linq:

var newList = list.Select(i => (int?)i).ToList();
int? fifth = newList.OrderByDescending(x => x).ElementAtOrDefault(4);

if (fifth == null)
{
    //handle default
}
Mike
  • 850
  • 10
  • 33
  • I didn't see the nullable int trick in the third block. Although now this does not seem to be a high performance solution, since it first creates a copy of the list, and then generates an enumerable during the LINQ expression – sɐunıɔןɐqɐp Jun 06 '18 at 08:39
  • Well you could ram it together in one line. However the conversion would be more subtle then. Tuning on performance is not included in this answer, since it isn't part of the question. – Mike Jun 06 '18 at 09:02
  • See both answers I've posted, which cover the case when the list itself is `null` OR it has less than 5 elements on it. – sɐunıɔןɐqɐp Jun 06 '18 at 09:06
1

The easiest way to do this is to just sort the data and take N items from the front. This is the recommended way for small data sets - anything more complicated is just not worth it otherwise.

However, for large data sets it can be a lot quicker to do what's known as a Partial Sort.

There are two main ways to do this: Use a heap, or use a specialised quicksort.

The article I linked describes how to use a heap. I shall present a partial sort below:

public static IList<T> PartialSort<T>(IList<T> data, int k) where T : IComparable<T>
{
    int start = 0;
    int end = data.Count - 1;

    while (end > start)
    {
        var index = partition(data, start, end);
        var rank = index + 1;

        if (rank >= k)
        {
            end = index - 1;
        }
        else if ((index - start) > (end - index))
        {
            quickSort(data, index + 1, end);
            end = index - 1;
        }
        else
        {
            quickSort(data, start, index - 1);
            start = index + 1;
        }
    }

    return data;
}

static int partition<T>(IList<T> lst, int start, int end) where T : IComparable<T>
{
    T x = lst[start];
    int i = start;

    for (int j = start + 1; j <= end; j++)
    {
        if (lst[j].CompareTo(x) < 0) // Or "> 0" to reverse sort order.
        {
            i = i + 1;
            swap(lst, i, j);
        }
    }

    swap(lst, start, i);
    return i;
}

static void swap<T>(IList<T> lst, int p, int q)
{
    T temp = lst[p];
    lst[p] = lst[q];
    lst[q] = temp;
}

static void quickSort<T>(IList<T> lst, int start, int end) where T : IComparable<T>
{
    if (start >= end)
        return;

    int index = partition(lst, start, end);
    quickSort(lst, start, index - 1);
    quickSort(lst, index + 1, end);
}

Then to access the 5th largest element in a list you could do this:

PartialSort(list, 5);
Console.WriteLine(list[4]);

For large data sets, a partial sort can be significantly faster than a full sort.


Addendum

See here for another (probably better) solution that uses a QuickSelect algorithm.

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • This is a very nice solution for large datasets, thank you a lot this contribution +1. Just a question for clarifying: Does this algorithm sort the list until the Kth sorted element is found, and then it exits? – sɐunıɔןɐqɐp Jun 06 '18 at 10:43
  • @sɐunıɔןɐqɐp Yes, effectively it does (so it *usually* reduces the amount of work significantly). The expected complexity is `O(n + k log k)` - however, be aware that the worst-case time can be quite bad (this is noted in the Wikipedia article that I linked). – Matthew Watson Jun 06 '18 at 11:00
0

This LINQ approach retrieves the 5th biggest element OR throws an exception WHEN the list is null or contains less than 5 elements:

int fifth = list?.Count >= 5 ?
    list.OrderByDescending(x => x).Take(5).Last() :
    throw new Exception("list is null OR has not enough elements");


This one retrieves the 5th biggest element OR null WHEN the list is null or contains less than 5 elements:

int? fifth = list?.Count >= 5 ?
    list.OrderByDescending(x => x).Take(5).Last() :
    default(int?);

if(fifth == null) // define behavior


This one retrieves the 5th biggest element OR the smallest element WHEN the list contains less than 5 elements:

if(list == null || list.Count <= 0)
    throw new Exception("Unable to retrieve Nth biggest element");

int fifth = list.OrderByDescending(x => x).Take(5).Last();


All these solutions are reliable, they should NEVER throw "unexpected" exceptions.

PS: I'm using .NET 4.7 in this answer.

sɐunıɔןɐqɐp
  • 3,332
  • 15
  • 36
  • 40
0

Here there is a C# implementation of the QuickSelect algorithm to select the nth element in an unordered IList<>.

You have to put all the code contained in that page in a static class, like:

public static class QuickHelpers
{
    // Put the code here
}

Given that "library" (in truth a big fat block of code), then you can:

int resA = list.QuickSelect(2, (x, y) => Comparer<int>.Default.Compare(y, x));
int resB = list.QuickSelect(list.Count - 1 - 2);

Now... Normally the QuickSelect would select the nth lowest element. We reverse it in two ways:

  • For resA we create a reverse comparer based on the default int comparer. We do this by reversing the parameters of the Compare method. Note that the index is 0 based. So there is a 0th, 1th, 2th and so on.

  • For resB we use the fact that the 0th element is the list-1 th element in the reverse order. So we count from the back. The highest element would be the list.Count - 1 in an ordered list, the next one list.Count - 1 - 1, then list.Count - 1 - 2 and so on

Theorically using Quicksort should be better than ordering the list and then picking the nth element, because ordering a list is on average a O(NlogN) operation and picking the nth element is then a O(1) operation, so the composite is O(NlogN) operation, while QuickSelect is on average a O(N) operation. Clearly there is a but. The O notation doesn't show the k factor... So a O(k1 * NlogN) with a small k1 could be better than a O(k2 * N) with a big k2. Only multiple real life benchmarks can tell us (you) what is better, and it depends on the size of the collection.

A small note about the algorithm:

As with quicksort, quickselect is generally implemented as an in-place algorithm, and beyond selecting the k'th element, it also partially sorts the data. See selection algorithm for further discussion of the connection with sorting.

So it modifies the ordering of the original list.

xanatos
  • 109,618
  • 12
  • 197
  • 280