在C#中如何计算“五数中位数”?

33

五个数的中位数有时被用作算法设计练习,已知可以使用仅6次比较进行计算。

如何在C#中最好地实现这个"使用6次比较的五个数的中位数"?我所有的尝试似乎都导致笨拙的代码 :( 我需要漂亮易读的代码,同时仍然只使用6次比较。

public double medianOfFive(double a, double b, double c, double d, double e){
    //
    // return median
    //
    return c;
}

注意: 我认为我应该在这里提供"算法":

我发现自己无法像Azereal在他的论坛帖子中那样清楚地解释这个算法。因此,我将参考他的帖子。来自http://www.ocf.berkeley.edu/~wwu/cgi-bin/yabb/YaBB.cgi?board=riddles_cs;action=display;num=1061827085

好吧,我在我的任务中遇到了这个问题,我求助于这个论坛,但是没有得到帮助。 最终我找出了如何做。

  1. 使用前4个元素开始归并排序,并对每一对进行排序(2次比较)

  2. 比较每一对中的两个较小数,并从可能性中排除最低的一个(3次比较)

  3. 将第5个数字添加到没有成对的数字中,并进行比较(4次比较)

  4. 比较两个新对中的最低值,并排除最低的一个(5次比较)

  5. 将孤立的数字和最后一对中的较低数字进行比较,较低的数字是中位数

    可能的中位数在括号内

(54321)

5:4 3:2 2次比较

(4<5 2<3 1)

4:2 3次比较

2(4<5 3 1)

1:3 4次比较

2(4<5 1<3)

4:1 5次比较

1,2(4<5 3)

4:3 6次比较

1,2(3)4,5

三是中位数

这是我编写的用于查找五个数的中位数的C++代码。不要介意它的笨拙:

double StageGenerator::MedianOfFive(double n1, double n2, double n3, double n4, double n5){
    double *a = &n1, *b = &n2, *c = &n3, *d = &n4, *e = &n5;
    double *tmp;

    // makes a < b and b < d
    if(*b < *a){
        tmp = a; a = b; b = tmp;
    }

    if(*d < *c){
        tmp = c; c = d; d = tmp;
    }

    // eleminate the lowest
    if(*c < *a){
        tmp = b; b = d; d = tmp; 
        c = a;
    }

    // gets e in
    a = e;

    // makes a < b and b < d
    if(*b < *a){
        tmp = a; a = b; b = tmp;
    }

    // eliminate another lowest
    // remaing: a,b,d
    if(*a < *c){
        tmp = b; b = d; d = tmp; 
        a = c;
    }

    if(*d < *a)
        return *d;
    else
        return *a;

} 

它应该更紧凑,不是吗?


正如@pablito在他的答案中指出的那样,内置的List.Sort()无法满足此要求,因为它最多使用13次比较 :]


嗯...这是某种作业吗? - driAn
不需要,如果你真的想看,我有自己用C++写的版本 :) - Gant
我很好奇,如果可读性是如此重要,那么为什么要保持只使用6次比较的限制?这种花招通常与可读性相矛盾。 - Vojislav Stojkovic
不是一道作业问题,我发誓T_T。为什么教授要求用C#编写可读性强的代码而不是伪代码算法呢? - Gant
@ Vojislav Stojkovic:实际上,6个比较是一个严格的下限。只需绘制一些哈斯图,您就会发现没有办法进行更少的比较... - Gnark
显示剩余10条评论
11个回答

45

我觉得这篇帖子很有趣,于是我做了一个东西,它仅仅进行了6次比较,没有其他操作:

static double MedianOfFive(double a, double b, double c, double d, double e)
{
    return b < a ? d < c ? b < d ? a < e ? a < d ? e < d ? e : d
                                                 : c < a ? c : a
                                         : e < d ? a < d ? a : d
                                                 : c < e ? c : e
                                 : c < e ? b < c ? a < c ? a : c
                                                 : e < b ? e : b
                                         : b < e ? a < e ? a : e
                                                 : c < b ? c : b
                         : b < c ? a < e ? a < c ? e < c ? e : c
                                                 : d < a ? d : a
                                         : e < c ? a < c ? a : c
                                                 : d < e ? d : e
                                 : d < e ? b < d ? a < d ? a : d
                                                 : e < b ? e : b
                                         : b < e ? a < e ? a : e
                                                 : d < b ? d : b
                 : d < c ? a < d ? b < e ? b < d ? e < d ? e : d
                                                 : c < b ? c : b
                                         : e < d ? b < d ? b : d
                                                 : c < e ? c : e
                                 : c < e ? a < c ? b < c ? b : c
                                                 : e < a ? e : a
                                         : a < e ? b < e ? b : e
                                                 : c < a ? c : a
                         : a < c ? b < e ? b < c ? e < c ? e : c
                                                 : d < b ? d : b
                                         : e < c ? b < c ? b : c
                                                 : d < e ? d : e
                                 : d < e ? a < d ? b < d ? b : d
                                                 : e < a ? e : a
                                         : a < e ? b < e ? b : e
                                                 : d < a ? d : a;
}

太棒了!很高兴你发现这个问题有用 :) - Gant
非常好,但编译时不一定是最快的。无论如何,记录一下,对于无符号字符数据类型,SSE实现(用于图像处理)在i7处理器上的速度大约快了60倍。 - Antonio

16
这基本上只是从你的C++示例中分离出交换和排序代码:
private static void Swap(ref double a, ref double b) {
    double t = a;
    a = b;
    b = t;
}

private static void Sort(ref double a, ref double b) {
    if (a > b) {
        double t = a;
        a = b;
        b = t;
    }
}

private static double MedianOfFive(double a, double b, double c, double d, double e){
    // makes a < b and c < d
    Sort(ref a, ref b);
    Sort(ref c, ref d);

    // eleminate the lowest
    if (c < a) {
        Swap(ref b, ref d);
        c = a;
    }

    // gets e in
    a = e;

    // makes a < b
    Sort(ref a, ref b);

    // eliminate another lowest
    // remaing: a,b,d
    if (a < c) {
        Swap(ref b, ref d);
        a = c;
    }

    return Math.Min(d, a);
}

很好的提高了清晰度,如果这个程序被用在紧密循环中,任何值得一试的编译器都会内联Swap和Sort(我能想到实现如此特定功能的唯一原因)。 - Godeke
我发现你的代码是这里最紧凑的代码,并将其标记为被接受的答案。其他的代码也很有帮助,我会给它们投票支持 :) - Gant
1
这段代码是正确的,但是注释“// makes a < b and b < d”是错误的;如果在排序之后检查,你会发现有时候“b > d”。例如,当你调用MedianOfFive(1, 2, 3, 4, 5)时,通常情况下e是五个参数中最大的,就会出现这种情况。 - Jason Orendorff
@Jason,好发现。我只是从原始帖子的代码中复制了注释,而没有检查它们是否合理。 - Matthew Crumley

12

谢谢。我知道你的帖子相当老,但对我的问题很有用。

我需要一种计算5个SSE/AVX寄存器(一次4个浮点数/8个浮点数,或一次2个双精度数/4个双精度数)中位数的方法:

  • 没有任何条件跳转

  • 只使用min/max指令

如果min/max函数针对带有条件跳转的标量寄存器编程,那么我的代码在比较方面并不是最优的。 但是如果min/max函数使用相应的CPU指令编码,那么我的代码非常有效,因为CPU在执行时不执行任何条件跳转。

    template<class V> 
    inline V median(const V &a, const V &b, const V &c)
    {
      return max(min(a,b),min(c,max(a,b))); 
    } 

    template<class V> 
    inline V median(const V &a, const V &b, const V &c, const V &d, const V &e)
    {
      V f=max(min(a,b),min(c,d)); // discards lowest from first 4
      V g=min(max(a,b),max(c,d)); // discards biggest from first 4
      return median(e,f,g);
    } 

正是我正在寻找的!我卡在如何用最小值和最大值计算三个值的中位数上,太遗憾了,这需要4次操作! - Antonio
@Antonio:【三个数中找中间值的最快方法是什么?】(https://dev59.com/wXI-5IYBdhLWcg3w-9wK#19045659) 这里是median5(),它使用min/max CPU指令(将此代码与@Gyorgy Szekely code结合起来)。在Python中测试median5()(以防万一)。 - jfs
@jfs 真棒!我觉得它很好,你应该在这里发布一个答案。我已经编写了显式向量化的代码,但不能发布代码。 - Antonio

9

这里有一个有趣的讨论:

http://www.ocf.berkeley.edu/~wwu/cgi-bin/yabb/YaBB.cgi?board=riddles_cs;action=display;num=1061827085

引用自该讨论:

  1. 将数字放入数组中。

  2. 使用三个比较操作,将数字洗牌,使得a[1] < a[2],a[4] < a[5],且a[1] < a[4]。

  3. 如果a[3] > a[2],那么问题就很简单了。如果a[2] < a[4],则中位数是a[3]和a[4]中较小的那个。否则,中位数是a[2]和a[5]中较小的那个。

  4. 所以a[3] < a[2]。如果a[3] > a[4],那么解就是a[3]和a[5]中较小的那个。否则,解就是a[2]和a[4]中较小的那个。


这看起来很不错。我找到的解决方案也来自同一线程。C#中的一个稳定实现也很好:D - Gant

9

这段代码比较混乱,需要进行重构,但它明确地遍历了所有的比较和交换,以便您了解其工作原理。

public double medianOfFive(double a, double b, double c, double d, double e){
    double median;
    // sort a and b
    if(a > b) // comparison # 1
    {
        double temp = a;
        a = b;
        b = temp;
    }

    // sort c and d
    if(c > d)  // comparison # 2
    {
        double temp = c;
        c = d;
        d = temp;
    }

    // replace the lower of a and c with e
    // because the lowest of the first four cannot be the median
    if(a < c) // comparison # 3
    {
        a = e;
        // re-sort a and b
        if(a > b) // comparison # 4
        {
            double temp = a;
            a = b;
            b = temp;
        }
    }
    else
    {
        c = e;
        // re-sort c and d
        if(c > d)  // comparison # 4
        {
            double temp = c;
            c = d;
            d = temp;
        }
    }

    // eliminate a or c, because the lowest
    // of the remaining four can't be the median either
    if(a < c) // comparison #5
    {
         if(b < c) // comparison #6
         {
              median = c;
         }
         else
         {
              median = b;
         }
    }
    else
    {
         if(d < a) // comparison #6
         {
              median = a;
         }
         else
         {
              median = d;
         }
    }
    return median;
}

4

4

只是为了检查有多少比较:

    class MyComparable : IComparable
{

    public static int NumberOfComparisons = 0;

    public int NumPart { get; set; }

    #region IComparable Members

    public int CompareTo(object obj)
    {
        NumberOfComparisons++; //I know, not thread safe but just for the sample
        MyComparable mc = obj as MyComparable;
        if (mc == null)
            return -1;
        else
            return NumPart.CompareTo(mc.NumPart);
    }

    #endregion
}

class Program
{
    static void Main(string[] args)
    {
        List<MyComparable> list = new List<MyComparable>();
        list.Add(new MyComparable() { NumPart = 5 });
        list.Add(new MyComparable() { NumPart = 4 });
        list.Add(new MyComparable() { NumPart = 3 });
        list.Add(new MyComparable() { NumPart = 2 });
        list.Add(new MyComparable() { NumPart = 1 });
        list.Sort();


        Console.WriteLine(MyComparable.NumberOfComparisons);
    }
}

结果是13。


1

应该可以了

private Double medianofFive(double[] input)
{
    Double temp;
if (input[0] > input[1])//#1 - sort First and Second
{
    temp = input[0];
    input[0] = input[1];
    input[1] = temp;
}
if (input[2] > input[3])//#2 sort Third and Fourth
{
    temp = input[2];
    input[2] = input[3];
    input[3] = temp;
}

// replace the smaller of first and third with 5th, then sort
int smallerIndex = input[0] < input[2] ? 0 : 2;//#3
input[smallerIndex] = input[4];

//sort the new pair
if(input[smallerIndex]>input[smallerIndex+1])//#4
{
    temp = input[smallerIndex];
    input[smallerIndex] = input[smallerIndex+1];
    input[smallerIndex+1] = temp;
}

//compare the two smaller numbers
// then compare the smaller of the two's partner with larger of the two
// the smaller of THOSE two is the median
if (input[2] > input[0])
//#5
{
    temp = input[2] > input[1] ? input[1] : input[2];//#6
}
else
{
    temp = input[0] > input[3] ? input[3] : input[0];//#6
}
    return temp;
}

不知道发生了什么事,但在预览中看起来很好,但在主页面上被截断了。 - Kevin
已修复。请使用Markdown编辑器,而非您自己的Markdown格式。 - GEOCHET

1
这是其他答案的一种变体,平均提高了3.33%至66.67%,通过6次比较,完全将5个元素分区到它们的中位数周围,而无需额外费用。
使用中位数法和快速选择算法,从5个不同的元素中找到中位数平均需要5.8次比较(对所有排列进行平均),其中使用中位数法从5个元素中选择3个样本来选择枢轴。中位数法将这三个元素分区,当对剩余的2个元素进行分区时,无需重新与枢轴进行比较。到目前为止,需要4-5次比较将5个元素分区到三个中间元素之一周围(中位数法不能是5个元素的最小值或最大值)。最多需要3次比较将5个元素分区到它们的中位数周围(严格来说,这比仅找到中位数更费力),总共需要4到7次比较,其中平均值为5.8(如上所述)对于5个不同元素的所有可能排列(如果元素不是不同的,则需要更少的比较)。请注意,这与通常的总是6次比较的解决方案不同,因为少数情况下,不同的输入可能需要多达7次比较,但另一方面,大多数不同输入的排列最多只需要6次甚至更少;此外,对于非不同的输入,编码以节省比较相当容易(如果所有输入都相等,则仅需要2次比较;在输入不是不同的情况下使用通常的6次比较方法来保存比较的代码变得相当复杂(试试吧!),而且即使所有输入都相等,它仍然需要6次比较)。

除了中位数以外,可以通过以下方法找到其他的顺序统计量: 第二小或第二大的平均需要更多一些比较 (5.81666...次),当然也可以通过只进行4次比较来找到最小值或最大值。

基于此,并应请求,以下是一个带有详细注释的函数,使用可变参数的比较函数返回五个元素的中位数指针。它是用C语言编写的,但在quadrathorpe-y deviant或sea ploose ploose中同样适用。请注意,它仅返回指向中位数值元素的指针; 它不会对元素进行分区(实际上它不会移动任何元素)。

/* a virtual swap, preserving both pointers for later use */
#define VSWAP(ma,mb,mt) mt=ma,ma=mb,mb=mt
/* a virtual swap only preserving the first pointer */
#define VSWAP2(ma,mb,munused) ma=mb
/* virtual rotation to the left; reverse the first 3 args for right rotation */
#define ROTATE(ma,mb,mc,mt) (mt)=(ma),(ma)=(mb),(mb)=(mc),(mc)=(mt)


/* median of 5 elements, no data movement, minimum (average 5.8) comparisons */
/* This implementation minimizes the number of comparisons made (min. 2) when
   elements compare equal.
*/
/* As no elements are moved, the elements are of course not partitioned around
   the element pointed to by the returned pointer (this differs from selection
   of the median via quickselect).
*/
/* Results are biased toward the middle: pc, then pb or pd, last pa or pe. */
/* The implementation is based on a simulation of quickselect using partition3
   to select a pivot from the middle three elements, with partitioning by
   skipping over the 3 partitioned elements.  For distinct inputs, it uses on
   average 5.8 comparisons (averaged over all permutations of 5 distinct
   elements); fewer for inputs with equal elements.  That's an improvement of
   about 3% over the usual 6-comparison implementation of median-of-5.
*/
void *fmed5(void *pa, void *pb, void *pc, void *pd, void *pe,
    int(*compar)(const void *,const void *))
{
    void *t;
    int c, d;
    /* First compare the 3 middle elements to determine their relative
       ordering; pb will point to the minimum, pc to the median, and pd to
       the maximum of the three.
    */
    /* Ternary median-of-3, biased toward middle element if possible, else
       first element.  Average 8/3 comparisons for 3 elements (distinct values)
       = 0.889 comparisons/element
    */
    c=compar(pb,pc); /* 1 */
    if (0<c) { /* *pb,*pc out-of-order: *pb>*pc */
        /* Before doing anything about pb,pc, compare *pc,*pd. */
        d=compar(pc,pd); /* 2a */
        if (0>d) { /* *pc<*pd: strictly in order */
            /* But *pb might be either larger than or smaller than (or equal
               to) *pd, so they may (i.e. unless it's known from the earlier
               comparison of original *pc and *pd that *pb is larger than
               both) have to be compared,  Possibilities:
               *pc<*pb<=*pd (virtual swap of pb,pc corrects relationship)
               *pc<*pd<*pb (virtual rotation to the left corrects it)
            */
            c=compar(pb,pd); /* 3a (if needed) */
            if (0<c) { /* *pc < *pd < *pb */
                ROTATE(pb,pc,pd,t);
            } else { /* *pc < *pb <= *pd */
                VSWAP(pb,pc,t);
            }
        } else { /* *pd==*pc<*pb or reversed ordering: *pd<*pc<*pb */
            VSWAP(pb,pd,t); /* one (virtual) swap takes care of it */
        } /* *pc:*pd comparisons results if-else */
        /* Note that if pc,pd compare equal, pc remains as the chosen
           median (biased toward the middle element).
        */
    } else if (0==c) { /* *pb,*pc compare equal */
        /* Either pb or pc can be taken as the median; bias here is towards
           pc, which is already in the middle position. But pd might be
           the minimum of the three or the maximum (or it may also be equal
           to both pb and pc).
        */
        d=compar(pb,pd); /* 2b */
        if (0<d) { /* pb,pd are out-of-order */
            VSWAP(pb,pd,t);
        }
    } else { /* *pb,*pc in strict order: *pb < *pc; how about *pc,*pd? */
        d=compar(pc,pd); /* 2c */
        if (0<d) { /* *pc,*pd are strictly out-of-order: *pd < *pc */
            /* But *pb might be either larger than or smaller than (or equal
               to) *pd, so they may (i.e. unless it's known from the earlier
               comparison of original *pc and *pd that *pb is larger than
               both) have to be compared,  Possibilities:
               *pb<=*pd<*pc (virtual swap of pc,pd corrects relationship)
               *pd<*pb<*pc (virtual rotation to the right corrects it)
            */
            c=compar(pb,pd); /* 3b (if needed) */
            if (0<c) { /* *pd < *pb < *pc */
                ROTATE(pd,pc,pb,t);
            } else { /* *pc < *pb <= *pd */
                VSWAP(pc,pd,t);
            }
        } /* *pc:*pd comparisons results if-else */
        /* Note that if pc,pd compare equal, pc remains as the chosen
           median (biased toward the middle element).
        */
    } /* *pb:*pc comparisons results if-else chain */
    /* Now pb points to the minimum of pb,pc,pd; pc to the median, and pd
       to the maximum.
    */
    /* Special case: if all three middle elements compare equal (0==c==d),
       any one can be returned as the median of 5, as it's impossible for
       either of the other two elements to be the median (unless of course
       one or both of them also compares equal to pb,pc,pd, in which case
       returning any of pb,pc,pd is still correct).  Nothing more needs to
       be done in that case.
    */
    if ((0!=c)||(0!=d)) { /* Not all three of pb,pc,pd compare equal */
        int e;
        /* Compare pa and pe to pc. */
        e=compar(pa,pc); /* 3c or 4a (iff needed) */
        /* If three (or more) of the four elements so far compared are
           equal, any of those equal-comparing elements can be chhosen as
           the median of 5.  If all five elements were arranged in order,
           one of the three equal-comparing elements would necessarily be
           in the middle (at most both of the remaining elements might be
           either larger or smaller than the equal elements).  So if pa
           compares equal to pc and pc also compared equal to pb or to pd,
           nothing more need be done; pc can be considered as the median of
           five.
        */
        if ((0!=e)||(0!=c)||(0!=d)) { /* no three elements compare equal */
            int f;
            f=compar(pe,pc); /* 4b or 5a (iff needed) */
            /* Check again for three equal comparisons to avoid doing any
               unnecessary additional work.
            */
            if (
                (0!=f) /* also not equal; still not three */
              ||
                ( /* 0==f && */
                 ((0!=c)&&(0!=d)) /* at most e and f; not three */
               ||
                 ((0!=c)&&(0!=e)) /* at most d and f; not three */
               ||
                 ((0!=d)&&(0!=e)) /* at most c and f; not three */
                )
            ) {
                /* Possibilites are that:
                     one of *pa,*pe is less than (or equal to) *pc and one
                       is greater than (or equal to) *pc; *pc is the median
                       of five.
                     *pa and *pe are both less than *pc; the median of five
                       is then the maximum of *pa,*pb,*pe (*pc and *pd are
                       at least as large as those three).  The ordering of
                       those 3 elements has not been established, and it
                       will take two additional comparisons to do so.
                     *pa and *pe are both greater than *pc; the median of
                       five is the minimum of *pa,*pd,*pe (neither *pb nor
                       *pc can be larger than any of those three).
                */
                if ((0<e)&&(0<f)) { /* *pa,*pe both > *pc; median of five is
                                       the minimum of *pa,*pe,*pd
                                    */
                    /* Bias towards *pd (originally closest of these three
                       to the middle.  Neither *pa nor *pe has yet been
                       compared to *pd.
                    */
                    e=compar(pa,pe); /* 5b or 6a (iff needed) */
                    if (0<e) { /* *pe is smaller */
                        f=compar(pd,pe); /* 6b or 7a (iff needed) */
                        if (0<f) { /* *pe is smaller */
                            VSWAP2(pc,pe,t);
                        } else { /* *pd is smaller or *pd==*pe */
                            VSWAP2(pc,pd,t);
                        }
                    } else { /* *pa==*pe or *pa is smaller */
                        f=compar(pd,pa); /* 6c or 7b (iff needed) */
                        if (0<f) { /* *pa is smaller */
                            VSWAP2(pc,pa,t);
                        } else { /* *pd is smaller or *pd==*pa */
                            VSWAP2(pc,pd,t);
                        }
                    }
                } else if ((0>e)&&(0>f)) { /* *pa,*pe both < *pc; median of
                                       five is the maximum of *pa,*pb,*pe
                                    */
                    /* Bias towards *pb (originally closest of these three
                       to the middle.  Neither *pa nor *pe has yet been
                       compared to *pb.
                    */
                    e=compar(pa,pe); /* 5c or 6d (iff needed) */
                    if (0<e) { /* *pa is larger */
                        f=compar(pa,pb); /* 6e or 7c (iff needed) */
                        if (0<f) { /* *pa is larger */
                            VSWAP2(pc,pa,t);
                        } else { /* *pb is larger or *pa==*pb */
                            VSWAP2(pc,pb,t);
                        }
                    } else { /* *pe is larger or *pa==*pe */
                        f=compar(pe,pb); /* 6f or 7d (iff needed) */
                        if (0<f) { /* *pe is larger */
                            VSWAP2(pc,pe,t);
                        } else { /* *pb is larger or *pe==*pb */
                            VSWAP2(pc,pb,t);
                        }
                    } /* *pa:*pe results if-else */
                } /* median of five: minimum or maximum of three if-else */
            } /* not three equal elements among five */
        } /* not three equal elements among four */
    } /* not three equal elements among three */
    return pc;
}

0

编程不是我的强项,但这里是我会用自然语言表达的算法。 我们用abcde来表示这五个数字。

比较abcd。不失一般性,假设a < bc < d。(到目前为止已经进行了2次比较)

比较ac。不失一般性,假设a < c。(3)

比较be。(4)请注意,使用b而不是d(它们不能交换),因为bac中较小数的“对应数”。

情况1:假设b < e

____比较bc —— 较大的值是中位数。(5)

情况2:让b>e

____比较ae。(5)

____情况2.1:让a<e

________比较ce —— 较大的值是中位数。(6)

____情况2.2:让a>e

________比较bc —— 较小的值是中位数。(6)

(格式很丑,我知道 >.<)


问题是如何在C#中实现。这个答案描述了如何使用伪代码来实现它。还有其他答案描述了如何在C#中实现它。 - Troy Turley
这并没有回答问题。一旦您拥有足够的声望,您将能够评论任何帖子;相反,提供不需要询问者澄清的答案。- 来自审核 - Furkan Öztürk

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