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

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

.net 数学库中有函数吗?

【问题讨论】:

    标签: c# .net algorithm median


    【解决方案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);
    }
    

    几点说明:

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

                  【讨论】:

                    猜你喜欢
                    • 2016-05-21
                    • 1970-01-01
                    • 1970-01-01
                    • 2014-08-25
                    • 2010-10-03
                    • 2012-04-16
                    • 1970-01-01
                    • 2019-01-23
                    相关资源
                    最近更新 更多