向列表添加一个中位数方法

14

我想在C#中重写List对象,以添加一个像Sum或Average一样的Median方法。我已经找到了这个函数:

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;
}

你能告诉我如何做到这一点吗?


3
请注意,您发布的 GetMedian 方法将具有对传递给该方法的数组进行排序的副作用。由于数组是引用类型,将数组分配给一个新变量(tempArray)不会创建一个新的数组。 - Greg
除了其他人在这里说的,还有更快的方法可以找到中位数,而不需要对整个列表进行排序。(这只有在列表真的很大的情况下才会有影响。)有一种修改过的快速排序形式,可以在不对整个列表进行排序的情况下找到中位数。 - Jeffrey L Whitledge
8个回答

25

使用扩展方法,复制输入的数组/列表。

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];
    }
}

2
在尝试对集合进行排序之前,我会先检查其大小。如果计数为0,则抛出异常;如果为1,则返回唯一值temp[0];否则,进行排序并执行相应操作。 - Moop
1
原始帖子中有一个错误:我们需要取 count/2 和 count/2 + 1 个元素,而不是减一。 - Vitas
@Vitas:这是正确的,因为数组是从零开始索引的。 - Sai Manoj Kumar Yadlapati

16

不要使用那个函数。它存在严重缺陷。看看这个:

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

在C#中,数组是引用类型。这个函数对你给出的数组进行排序,而不是复制它。获取数组的中位数不应该改变其顺序;它可能已经按照不同的顺序排序。

使用Array.Copy首先复制数组,然后再对副本进行排序。


6

我一定会制作那些扩展方法

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()
    }
}

您可以按以下方式使用这些方法:

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

更新

哦……正如Eric所提到的,你应该找到另一个中位数的实现方式。你提供的这个不仅会直接修改原始数组,而且如果我理解正确的话,还将返回一个整数,而不是预期的小数。


我会将它写成 Mediatn<T>(this List<T> list) 以使其完全泛型化!否则这是推荐的方法。 - luckyluke
@luckyluke - 我本来想这么做,但是没有办法限制T为数字类型。如果我在List<string>上调用Median()会发生什么? - Justin Niessner
扩展方法绝对是正确的选择,最好在接口IList<int>上实现。这样该方法就可以用于列表和数组。 - Greg
是的,可能我会使用IComparable类型或至少IEquatable。 - luckyluke
因此,您可以在序列中返回一个中位字符串:) 我想我们实际上需要数字类型的精确定义...不幸的是,这里没有简单的解决方案:( - luckyluke
显示剩余2条评论

2
您可能不想使用sort来查找中位数,因为还有更有效的方法可以计算它。您可以在我的以下答案中找到此代码,该代码还将Median作为IList<T>的扩展方法添加: 在C#中计算中位数

0

平均值和总和是可用于任何IEnumerable的扩展方法,只要将正确的转换函数作为参数提供MSDN

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

transform函数将接收一个集合项并将其转换为十进制数(可平均和可比较)。

我不会在这里深入介绍中位数方法的实现细节,但它并不是很复杂。

编辑:我看到您添加了输出十进制平均值的进一步要求。

PS:出于简洁起见,省略了参数检查。


0

我创建了自己的解决方案。在SQL Server中,当使用.ToList()和.ToArray()时,由于需要先从数据库中获取所有行,这会导致性能问题。我只需要记录的长度以及中间的1或2行(奇数或偶数),因此这种方法并不适用。

如果有人感兴趣,我还有一个版本,使用Expression返回TResult而不是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);
    }
}

0

我会对你的方法进行一些修正:

将这部分替换为:

     int[] tempArray = array; 

使用:

     int[] tempArray = (int[])array.Clone();

0
你可以为你想要支持的集合类型创建一个扩展方法。然后,你就可以像调用该类的一部分一样调用它了。 MSDN - 扩展方法文档和示例

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接