在c#中计算中位数

Posted

技术标签:

【中文标题】在c#中计算中位数【英文标题】:Calculate median in c# 【发布时间】:2011-05-07 15:27:55 【问题描述】:

我需要编写一个函数来接受小数数组并找到中位数。

.net 数学库中有函数吗?

【问题讨论】:

【参考方案1】:

看起来其他答案正在使用排序。从性能的角度来看,这并不是最佳的,因为它需要O(n logn) 时间。可以改为以O(n) 时间计算中位数。这个问题的广义版本被称为“n 阶统计”,这意味着在集合中找到一个元素 K,使得我们有 n 个元素小于或等于 K,其余元素大于或等于 K。所以 0 阶统计量将是最小的集合中的元素(注意:一些文献使用从 1 到 N 的索引,而不是 0 到 N-1)。中位数就是(Count-1)/2-order statistic。

以下是从Cormen et al, 3rd Edition 的Introduction to Algorithms中采用的代码。

/// <summary>
/// Partitions the given list around a pivot element such that all elements on left of pivot are <= pivot
/// and the ones at thr right are > pivot. This method can be used for sorting, N-order statistics such as
/// as median finding algorithms.
/// Pivot is selected ranodmly if random number generator is supplied else its selected as last element in the list.
/// Reference: Introduction to Algorithms 3rd Edition, Corman et al, pp 171
/// </summary>
private static int Partition<T>(this IList<T> list, int start, int end, Random rnd = null) where T : IComparable<T>

    if (rnd != null)
        list.Swap(end, rnd.Next(start, end+1));

    var pivot = list[end];
    var lastLow = start - 1;
    for (var i = start; i < end; i++)
    
        if (list[i].CompareTo(pivot) <= 0)
            list.Swap(i, ++lastLow);
    
    list.Swap(end, ++lastLow);
    return lastLow;


/// <summary>
/// Returns Nth smallest element from the list. Here n starts from 0 so that n=0 returns minimum, n=1 returns 2nd smallest element etc.
/// Note: specified list would be mutated in the process.
/// Reference: Introduction to Algorithms 3rd Edition, Corman et al, pp 216
/// </summary>
public static T NthOrderStatistic<T>(this IList<T> list, int n, Random rnd = null) where T : IComparable<T>

    return NthOrderStatistic(list, n, 0, list.Count - 1, rnd);

private static T NthOrderStatistic<T>(this IList<T> list, int n, int start, int end, Random rnd) where T : IComparable<T>

    while (true)
    
        var pivotIndex = list.Partition(start, end, rnd);
        if (pivotIndex == n)
            return list[pivotIndex];

        if (n < pivotIndex)
            end = pivotIndex - 1;
        else
            start = pivotIndex + 1;
    


public static void Swap<T>(this IList<T> list, int i, int j)

    if (i==j)   //This check is not required but Partition function may make many calls so its for perf reason
        return;
    var temp = list[i];
    list[i] = list[j];
    list[j] = temp;


/// <summary>
/// Note: specified list would be mutated in the process.
/// </summary>
public static T Median<T>(this IList<T> list) where T : IComparable<T>

    return list.NthOrderStatistic((list.Count - 1)/2);


public static double Median<T>(this IEnumerable<T> sequence, Func<T, double> getValue)

    var list = sequence.Select(getValue).ToList();
    var mid = (list.Count - 1) / 2;
    return list.NthOrderStatistic(mid);

几点说明:

    此代码将书中原始版本的尾递归代码替换为迭代循环。 它还消除了在 start==end 时对原始版本进行不必要的额外检查。 我提供了两个版本的 Median,一个接受 IEnumerable 然后创建一个列表。如果您使用接受 IList 的版本,请记住它会修改列表中的顺序。 以上方法计算O(n)预期时间中的中位数或任何 i-order 统计数据。如果您想要O(n) 更糟糕的情况,那么可以使用中位数的技术。虽然这会提高更坏情况的性能,但它会降低平均情况,因为O(n) 中的常数现在更大。但是,如果您主要根据非常大的数据计算中位数,那么值得一看。 NthOrderStatistics 方法允许传递随机数生成器,然后使用该生成器在分区期间选择随机枢轴。这通常是没有必要的,除非您知道您的数据具有某些模式,因此最后一个元素不会足够随机,或者您的代码以某种方式暴露在外部以进行有针对性的利用。 如果您有奇数个元素,中位数的定义就很清楚了。它只是排序数组中索引为(Count-1)/2 的元素。但是当你偶数个元素(Count-1)/2 不再是整数并且你有两个中位数:下中位数Math.Floor((Count-1)/2)Math.Ceiling((Count-1)/2)。一些教科书使用较低的中位数作为“标准”,而其他教科书则建议使用两个的平均值。这个问题对于 2 个元素的集合变得尤为重要。上面的代码返回较低的中位数。如果您想要取而代之的是较低和较高的平均值,那么您需要调用上述代码两次。在这种情况下,请务必衡量数据的性能,以决定是否应该使用上述代码 VS 直接排序。 对于 .net 4.5+,您可以在 Swap&lt;T&gt; 方法上添加 MethodImplOptions.AggressiveInlining 属性以稍微提高性能。

【讨论】:

@ShitalShah: re: 6,如果我想用平均值计算中位数,而不是对 NthOrderStatistic 进行 2 次调用,我不能利用列表发生突变的事实,而且基本上选择下一项。我不确定 NthOrderStatistic 方法最终是对列表进行升序排序还是仅对其中的一部分进行排序(最终取决于列表中的数据)。 @costa - NthOrderStatistics 没有任何正在排序的一半的受托人。下一个项目也不是下一个更小或更大的项目。 这非常方便,谢谢!我更新了使用 C# 6 表达式主体成员的方法,并坚持了一个要点,以及标准偏差算法 - gist.github.com/cchamberlain/478bf7a3411beb47abb6 我发现算法有两个问题。首先,将rnd.Next(start, end) 替换为rnd.Next(start, end + 1),以不排除end 成为枢纽。其次,如果数组包含许多相同的值,算法将变为O(n^2)。为避免这种情况,如果pivotlist[prevPivotIndex] 相同,请在Partition&lt;T&gt;() 中添加检查以返回end @G。 Cohen - rnd.Next(start, end+1) 的好消息。但是,如果枢轴与上一个相同,我不确定返回端。我需要考虑一下这个......【参考方案2】:

感谢 Rafe,这已考虑到您的回复者发布的问题。

public static double GetMedian(double[] sourceNumbers) 
        //Framework 2.0 version of this method. there is an easier way in F4        
        if (sourceNumbers == null || sourceNumbers.Length == 0)
            throw new System.Exception("Median of empty array not defined.");

        //make sure the list is sorted, but use a new array
        double[] sortedPNumbers = (double[])sourceNumbers.Clone();
        Array.Sort(sortedPNumbers);

        //get the median
        int size = sortedPNumbers.Length;
        int mid = size / 2;
        double median = (size % 2 != 0) ? (double)sortedPNumbers[mid] : ((double)sortedPNumbers[mid] + (double)sortedPNumbers[mid - 1]) / 2;
        return median;
    

【讨论】:

这里的函数为什么是静态的? @richieqianle:因为一切可以是静态的都应该是静态的。从virtual functions table的角度来看效率更高。 @abatishchev 默认情况下,C# 上的方法不是虚拟的(与 Java 相比)。但即使它,性能也是一个非常糟糕的原因,可以让某些东西变成静态或不静态。一个更好的理由 - 至少在这个答案中 - 可能是如果该方法是某种亲属或实用方法,则不需要该类的实例。 @HimBromBeere:“不需要类的实例”基本上等于“所有可以静态的东西都应该是静态的” @abatishchev 我同意,静态完全可以。【参考方案3】:

Math.NET 是一个开源库,它提供了一种计算Median 的方法。 nuget 包名为MathNet.Numerics。

用法很简单:

using MathNet.Numerics.Statistics;

IEnumerable<double> data;
double median = data.Median();

【讨论】:

【参考方案4】:
decimal Median(decimal[] xs) 
  Array.Sort(xs);
  return xs[xs.Length / 2];

应该做的伎俩。

-- 编辑--

对于那些想要完整的蒙蒂的人,这里是完整、简短、纯粹的解决方案(假设输入数组非空):

decimal Median(decimal[] xs) 
  var ys = xs.OrderBy(x => x).ToList();
  double mid = (ys.Count - 1) / 2.0;
  return (ys[(int)(mid)] + ys[(int)(mid + 0.5)]) / 2;

【讨论】:

这是O(n log n)。可以在O(n) 时间找到中位数。此外,如果数组的长度是偶数,这将无法返回中位数(它应该是数组排序后两个中间元素的平均值)。 当然,但是问题没有提到 O(n) 作为要求,并且对于偶数或空案例,它们留给学生作为练习。 这也修改了你传递给方法的数组,这很傻。 @Gleno,我宁愿认为规范。让所有这些都保持开放(好吧,我是在 C# 意义上解释“函数”,这可能会产生副作用)。目的只是为了展示一个简短的答案。【参考方案5】:

.net 数学库中有函数吗?

没有。

不过,自己编写并不难。朴素算法对数组进行排序并选择中间(或两个中间的平均值)元素。然而,这个算法是O(n log n),而它有可能在O(n) 时间内解决这个问题。你想看看selection algorithms得到这样的算法。

【讨论】:

【参考方案6】:

这是 Jason 答案的通用版本

    /// <summary>
    /// Gets the median value from an array
    /// </summary>
    /// <typeparam name="T">The array type</typeparam>
    /// <param name="sourceArray">The source array</param>
    /// <param name="cloneArray">If it doesn't matter if the source array is sorted, you can pass false to improve performance</param>
    /// <returns></returns>
    public static T GetMedian<T>(T[] sourceArray, bool cloneArray = true) where T : IComparable<T>
    
        //Framework 2.0 version of this method. there is an easier way in F4        
        if (sourceArray == null || sourceArray.Length == 0)
            throw new ArgumentException("Median of empty array not defined.");

        //make sure the list is sorted, but use a new array
        T[] sortedArray = cloneArray ? (T[])sourceArray.Clone() : sourceArray;
        Array.Sort(sortedArray);

        //get the median
        int size = sortedArray.Length;
        int mid = size / 2;
        if (size % 2 != 0)
            return sortedArray[mid];

        dynamic value1 = sortedArray[mid];
        dynamic value2 = sortedArray[mid - 1];
        return (sortedArray[mid] + value2) * 0.5;
    

【讨论】:

【参考方案7】:

这是最快的不安全实现, 之前的算法相同,取自source

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static unsafe void SwapElements(int* p, int* q)
    
        int temp = *p;
        *p = *q;
        *q = temp;
    

    public static unsafe int Median(int[] arr, int n)
    
        int middle, ll, hh;

        int low = 0; int high = n - 1; int median = (low + high) / 2;
        fixed (int* arrptr = arr)
        
            for (;;)
            
                if (high <= low)
                    return arr[median];

                if (high == low + 1)
                
                    if (arr[low] > arr[high])
                        SwapElements(arrptr + low, arrptr + high);
                    return arr[median];
                

                middle = (low + high) / 2;
                if (arr[middle] > arr[high])
                    SwapElements(arrptr + middle, arrptr + high);

                if (arr[low] > arr[high])
                    SwapElements(arrptr + low, arrptr + high);

                if (arr[middle] > arr[low])
                    SwapElements(arrptr + middle, arrptr + low);

                SwapElements(arrptr + middle, arrptr + low + 1);

                ll = low + 1;
                hh = high;
                for (;;)
                
                    do ll++; while (arr[low] > arr[ll]);
                    do hh--; while (arr[hh] > arr[low]);

                    if (hh < ll)
                        break;

                    SwapElements(arrptr + ll, arrptr + hh);
                

                SwapElements(arrptr + low, arrptr + hh);

                if (hh <= median)
                    low = ll;
                if (hh >= median)
                    high = hh - 1;
            
        
    

【讨论】:

【参考方案8】:

CenterSpace 的 NMath 库提供了一个函数:

double[] values = new double[arraySize];
double median = NMathFunctions.Median(values);

您可以选择使用 NaNMedian(如果您的数组可能包含空值),但您需要将数组转换为向量:

double median = NMathFunctions.NaNMedian(new DoubleVector(values));

CenterSpace's NMath Library 不是免费的,但许多大学都有许可证

【讨论】:

【参考方案9】:

未来的某个时候。这是我认为最简单的方法。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Median

    class Program
    
        static void Main(string[] args)
        
            var mediaValue = 0.0;
            var items = new[]  1, 2, 3, 4,5 ;
            var getLengthItems = items.Length;
            Array.Sort(items);
            if (getLengthItems % 2 == 0)
            
                var firstValue = items[(items.Length / 2) - 1];
                var secondValue = items[(items.Length / 2)];
                mediaValue = (firstValue + secondValue) / 2.0;
            
            if (getLengthItems % 2 == 1)
            
                mediaValue = items[(items.Length / 2)];
            
            Console.WriteLine(mediaValue);
            Console.WriteLine("Enter to Exit!");
            Console.ReadKey();
        
    

【讨论】:

你实际上可以不用 if 语句。只需设置medianValue = (items[items.Length / 2] + items[(items.Length - 1) / 2])/2。由于数组中奇数个项目的整数除法,您只会得到相同的项目两次,当您将其添加到自身然后除以二时,您将得到相同的数字。对于偶数个项目,您将获得两个不同的索引。为清楚起见,您也可以考虑保持原样,但这种方式更简洁。【参考方案10】:

以下代码有效:但不是非常有效的方式。 :(

static void Main(String[] args) 
        int n = Convert.ToInt32(Console.ReadLine());            
        int[] medList = new int[n];

        for (int x = 0; x < n; x++)
            medList[x] = int.Parse(Console.ReadLine());

        //sort the input array:
        //Array.Sort(medList);            
        for (int x = 0; x < n; x++)
        
            double[] newArr = new double[x + 1];
            for (int y = 0; y <= x; y++)
                newArr[y] = medList[y];

            Array.Sort(newArr);
            int curInd = x + 1;
            if (curInd % 2 == 0) //even
            
                int mid = (x / 2) <= 0 ? 0 : (newArr.Length / 2);
                if (mid > 1) mid--;
                double median = (newArr[mid] + newArr[mid+1]) / 2;
                Console.WriteLine("0:F1", median);
            
            else //odd
            
                int mid = (x / 2) <= 0 ? 0 : (newArr.Length / 2);
                double median = newArr[mid];
                Console.WriteLine("0:F1", median);
            
        


【讨论】:

【参考方案11】:

我的 5 美分(因为它看起来更直接/更简单,并且足以用于短名单):

public static T Median<T>(this IEnumerable<T> items)

    var i = (int)Math.Ceiling((double)(items.Count() - 1) / 2);
    if (i >= 0)
    
        var values = items.ToList();
        values.Sort();
        return values[i];
    

    return default(T);

附:使用 ShitalShah 描述的“较高中位数”。

【讨论】:

【参考方案12】:

我有一个带有变量的直方图:组 这里我如何计算我的中位数:

int[] group = new int[nbr]; 

// -- Fill the group with values---

// sum all data in median
int median = 0;
for (int i =0;i<nbr;i++) median += group[i];

// then divide by 2 
median = median / 2;

// find 50% first part 
for (int i = 0; i < nbr; i++)

   median -= group[i];
   if (median <= 0)
   
      median = i;
      break;
   

median 是中位数的组索引

【讨论】:

以上是关于在c#中计算中位数的主要内容,如果未能解决你的问题,请参考以下文章

如何计算一个数字的总位数?

C#中的decimal怎么保留两位小数

计算 Code128 类型条码的校验和位数

C#中用PadLeftPadRight 补足位数

在地图减少中计算中位数

在 Ruby 中计算中位数