14

I would like to override the List object in C# in order to add a Median method like Sum or Average. I already found this function:

public static decimal GetMedian(int[] array)
{
    int[] tempArray = array;
    int count = tempArray.Length;

    Array.Sort(tempArray);

    decimal medianValue = 0;

    if (count % 2 == 0)
    {
        // count is even, need to get the middle two elements, add them together, then divide by 2
        int middleElement1 = tempArray[(count / 2) - 1];
        int middleElement2 = tempArray[(count / 2)];
        medianValue = (middleElement1 + middleElement2) / 2;
    }
    else
    {
        // count is odd, simply get the middle element.
        medianValue = tempArray[(count / 2)];
    }

    return medianValue;
}

Can you tell me how to do that?

Greg
  • 21,917
  • 11
  • 55
  • 77
Aladdin Gallas
  • 671
  • 2
  • 11
  • 34
  • 3
    Note that the `GetMedian` method you posted will have the side-effect of sorting the array that is passed into it. Since arrays are reference types, assigning the array to a new variable (tempArray) does not create a new array. – Greg Mar 11 '11 at 15:58
  • In addition to what everyone else is saying here, there are also faster methods of finding the median that do not include sorting the entire list. (This will only matter if the lists are really *huge* though.) There is a modified form of quick-sort that will find the median without sorting the entire list. – Jeffrey L Whitledge Mar 14 '11 at 14:29

8 Answers8

24

Use an extension method, and make a copy of the inputted array/list.

public static decimal GetMedian(this IEnumerable<int> source)
{
    // Create a copy of the input, and sort the copy
    int[] temp = source.ToArray();    
    Array.Sort(temp);

    int count = temp.Length;
    if (count == 0)
    {
        throw new InvalidOperationException("Empty collection");
    }
    else if (count % 2 == 0)
    {
        // count is even, average two middle elements
        int a = temp[count / 2 - 1];
        int b = temp[count / 2];
        return (a + b) / 2m;
    }
    else
    {
        // count is odd, return the middle element
        return temp[count / 2];
    }
}
Greg
  • 21,917
  • 11
  • 55
  • 77
16

Do not use that function. It is deeply flawed. Check this out:

int[] tempArray = array;     
Array.Sort(tempArray); 

Arrays are reference types in C#. This sorts the array that you give it, not a copy. Obtaining the median of an array should not change its order; it might already be sorted into a different order.

Use Array.Copy to first make a copy of the array and then sort the copy.

Eric Lippert
  • 612,321
  • 166
  • 1,175
  • 2,033
6

I would definitely make those Extension Methods:

public static class EnumerableExtensions
{
    public static decimal Median(this IEnumerable<int> list)
    {
        // Implementation goes here.
    }

    public static int Sum(this IEnumerable<int> list)
    {
        // While you could implement this, you could also use Enumerable.Sum()
    }
}

You could then use those methods in the following way:

List<int> values = new List<int>{ 1, 2, 3, 4, 5 };
var median = values.Median();

Update

Oh...and as Eric mentions, you should find another implementation of Median. The one you provided not only modifies the original array in place but, if I'm reading it correctly, will also return an integer rather than the expected decimal.

Justin Niessner
  • 229,755
  • 35
  • 391
  • 521
  • I would make it Mediatn(this List list) to make it fully generic! otherwise that is recommened apporach – luckyluke Mar 11 '11 at 16:01
  • @luckyluke - I would've done that, but there's no way to restrict T to numeric types. What happens when I call Median() on a List? – Justin Niessner Mar 11 '11 at 16:02
  • Extension methods are definitely the way to go. Preferably on the interface `IList`. That way the method can be used for Lists and Arrays. – Greg Mar 11 '11 at 16:02
  • @Greg - You're right...but you may as well take it one step further to use IEnumerable. – Justin Niessner Mar 11 '11 at 16:04
  • Yea, possibly, I would do that with IComparable types or at least IEquatable – luckyluke Mar 11 '11 at 16:14
  • @luckyluke - That may be a start, but the String class also implement IComparable and IEquatable. – Justin Niessner Mar 11 '11 at 16:19
  • Hence You can return a median string in the sequence:) i'd say we actually want precise definition of numeric type... which isn't regrettably there and there is no easy solution to this problem :( – luckyluke Mar 11 '11 at 17:39
2

You probably don't want to use sort to find median because there are more efficient way to calculate it otherwise. You can find the code for this which also adds Median as extension method for IList<T> in my following answer:

Calculate median in c#

Community
  • 1
  • 1
Shital Shah
  • 47,549
  • 10
  • 193
  • 157
0

I'll make some corrections to your method:

replace this:

     int[] tempArray = array; 

with:

     int[] tempArray = (int[])array.Clone();
Raz Megrelidze
  • 2,443
  • 1
  • 22
  • 26
0

I created my own solution I have big tables in SQL server and .ToList() and .ToArray() doesn't work well (you pull all rows from database bofore doing anything else, what I need simply is the length of the records, and middle 1 or 2 rows (odd or even) if anyone interested I have a version with Expression returns TResult instead if decimal

   public static decimal MedianBy<T, TResult>(this IQueryable<T> sequence, Expression<Func<T, TResult>> getValue)
{
    var count = sequence.Count();
    //Use Expression bodied fuction otherwise it won't be translated to SQL query
    var list = sequence.OrderByDescending(getValue).Select(getValue);
    var mid = count / 2;
    if (mid == 0)
    {
        throw new InvalidOperationException("Empty collection");
    }
    if (count % 2 == 0)
    {
        var elem1 = list.Skip(mid - 1).FirstOrDefault();
        var elem2 = list.Skip(mid).FirstOrDefault();

        return (Convert.ToDecimal(elem1) + Convert.ToDecimal(elem2)) / 2M;
        //TODO: search for a way to devide 2 for different types (int, double, decimal, float etc) till then convert to decimal to include all posibilites
    }
    else
    {
        return Convert.ToDecimal(list.Skip(mid).FirstOrDefault());
        //ElementAt Doesn't work with SQL
        //return list.ElementAt(mid);
    }
}
Murat Mercan
  • 473
  • 6
  • 21
0

You could create an extension method for the collection type you want to support. Then you'll be able to call it, just as if its part of that class.

MSDN - Extension Methods documentation and examples

Kurru
  • 13,337
  • 16
  • 59
  • 79
0

Average and sum are Extension methods available for any IEnumerable providing the correct transformation function as a parameter MSDN

decimal Median<TSource>(this IEnumerable<TSource> collection, Func<TSource,decimal> transform)
{
   var array = collection.Select(x=>transform(x)).ToArray();
   [...]
   return median;
}

transform will take a collection item and transform it to a decimal (averagable and comparable)

I won't dive here in the detail of the Median metho implementation, but it's not really complicated.

Edit: I Saw you added the further requirement of outputing a decimal average.

PS: parameter checking is ometted for brievety.

Eilistraee
  • 7,930
  • 1
  • 24
  • 30