【问题标题】:Something I dont understand about median of medians algorithm我对中位数算法的中位数不了解
【发布时间】:2019-02-26 21:37:48
【问题描述】:

关于中位数的算法,我有一些不明白的地方。 该算法的一个关键步骤是找到一个近似中值,根据Wikipedia,我们保证这个近似中值大于初始集合中元素的 30%。

为了找到这个近似中位数,我们计算每组 5 个元素的中位数,将这些中位数收集到一个新集合中,然后重新计算中位数,直到获得的集合中的元素少于 5 个。在这种情况下,我们得到集合的中位数。 (如果我的解释不清楚,请参阅维基百科页面)

但是,请考虑以下 125 个元素的集合:

1 2 3 1001 1002
4 5 6 1003 1004
7 8 9 1005 1006
1020 1021 1022 1023 1034 
1025 1026 1027 1028 1035 

10 11 12 1007 1008
13 14 15 1009 1010
16 17 18 1011 1013
1029 1030 1031 1032 1033 
1036 1037 1038 1039 1040 

19 20 21 1014 1015
22 23 24 1016 1017
25 26 27 1018 1019
1041 1042 1043 1044 1045
1046 1047 1048 1049 1050

1051 1052 1053 1054 1055
1056 1057 1058 1059 1060
1061 1062 1063 1064 1065
1066 1067 1068 1069 1070
1071 1072 1073 1074 1075

1076 1077 1078 1079 1080
1081 1082 1083 1084 1085
1086 1087 1088 1089 1090
1091 1092 1093 1094 1095
1096 1097 1098 1099 1100 

所以我们将集合划分为 5 个元素为一组,计算并收集中位数,因此,我们得到以下集合:

3 6 9 1022 1207
12 15 18 1031 1038
21 24 27 1043 1048
1053 1058 1063 1068 1073
1078 1083 1088 1093 1098

我们重做同样的算法,得到如下集合:

9 18 27 1063 1068

所以我们得到近似的中位数是 27。但是这个数字大于或等于只有 27 个元素。而 27/125 = 21.6%

所以我的问题是:我哪里错了?为什么在我的情况下,近似中位数不大于元素的 30%????

感谢您的回复!

【问题讨论】:

    标签: algorithm median-of-medians


    【解决方案1】:

    您对中位数算法感到困惑的原因是,虽然中位数返回的近似结果在实际中位数的 20% 以内,但在算法的某些阶段,我们还需要计算准确的中位数.如果您将两者混为一谈,您将无法获得预期的结果,如您的示例所示。

    Median-of-medians 使用三个函数作为其构建块:

    medianOfFive(array, first, last) {
        // ...
        return median;
    }
    

    此函数返回数组(部分)中五个(或更少)元素的精确中位数。有几种方法可以对此进行编码,例如排序网络或插入排序。细节对于这个问题并不重要,但重要的是要注意这个函数返回的是准确的中位数,而不是近似值。

    medianOfMedians(array, first, last) {
        // ...
        return median;
    }
    

    此函数返回数组(部分)的中位数的近似值,保证大于 30% 的最小元素,小于 30% 的最大元素。我们将在下面详细介绍。

    select(array, first, last, n) {
        // ...
        return element;
    }
    

    此函数返回数组(部分)中的第 n 个最小元素。这个函数也返回一个精确的结果,而不是一个近似值。

    在最基本的情况下,整个算法是这样工作的:

    medianOfMedians(array, first, last) {
        call medianOfFive() for every group of five elements
        fill an array with these medians
        call select() for this array to find the middle element
        return this middle element (i.e. the median of medians)
    }
    

    所以这就是你的计算出错的地方。在创建了一个中位数为 5 的数组后,您在该数组上再次使用了中位数的中位数函数,这为您提供了中位数 (27) 的近似值,但在这里您需要实际的中位数 (1038)。

    这一切听起来都相当简单,但变得复杂的是函数 select() 调用 medianOfMedians() 来获得中值的第一个估计值,然后它使用它来计算确切的中值,所以你得到一个两个-方式递归,两个函数相互调用。当为 25 个或更少的元素调用 medianOfMedians() 时,此递归停止,因为那时只有 5 个中位数,而不是使用 select() 来查找它们的中位数,它可以使用 medianOfFive()。

    select() 调用 medianOfMedians() 的原因是它使用分区将数组(的一部分)拆分为大小接近相等的两部分,并且它需要一个好的枢轴值来做到这一点。在它将数组分成两部分,其中元素小于和大于枢轴后,它会检查第 n 个最小元素在哪一部分中,并使用该部分进行递归。如果值较小的部分的大小为n-1,则pivot是第n个值,不需要进一步递归。

    select(array, first, last, n) {
        call medianOfMedians() to get approximate median as pivot
        partition (the range of) the array into smaller and larger than pivot
        if part with smaller elements is size n-1, return pivot
        call select() on the part which contains the n-th element
    }
    

    如您所见,select() 函数会递归(除非枢轴恰好是第 n 个元素),但在数组的更小的范围内,因此在某个点(例如两个元素)找到第 n 个元素将变得微不足道,不再需要进一步递归。

    所以最后我们得到了一些更详细的信息:

    medianOfFive(array, first, last) {
        // some algorithmic magic ...
        return median;
    }
    
    medianOfMedians(array, first, last) {
        if 5 elements or fewer, call medianOfFive() and return result
        call medianOfFive() for every group of five elements
        store the results in an array medians[]
        if 5 elements or fewer, call medianOfFive() and return result
        call select(medians[]) to find the middle element
        return the result (i.e. the median of medians)
    }
    
    select(array, first, last, n) {
        if 2 elements, compare and return n-th element
        if 5 elements or fewer, call medianOfFive() to get median as pivot
        else call medianOfMedians() to get approximate median as pivot
        partition (the range of) the array into smaller and larger than pivot
        if part with smaller elements is size n-1, return pivot
        if n-th value is in part with larger values, recalculate value of n
        call select() on the part which contains the n-th element
    }
    

    示例

    输入数组(125 个值,25 组,每组 5 个):

     #1    #2    #3    #4    #5    #6    #7    #8    #9    #10   #11   #12   #13   #14   #15   #16   #17   #18   #19   #20   #21   #22   #23   #24   #25
    
       1     4     7  1020  1025    10    13    16  1029  1036    19    22    25  1041  1046  1051  1056  1061  1066  1071  1076  1081  1086  1091  1096
       2     5     8  1021  1026    11    14    17  1030  1037    20    23    26  1042  1047  1052  1057  1062  1067  1072  1077  1082  1087  1092  1097
       3     6     9  1022  1027    12    15    18  1031  1038    21    24    27  1043  1048  1053  1058  1063  1068  1073  1078  1083  1088  1093  1098
    1001  1003  1005  1023  1028  1007  1009  1011  1032  1039  1014  1016  1018  1044  1049  1054  1059  1064  1069  1074  1079  1084  1089  1094  1099
    1002  1004  1006  1034  1035  1008  1010  1013  1033  1040  1015  1017  1019  1045  1050  1055  1060  1065  1070  1075  1080  1085  1090  1095  1100
    

    五组的中位数(25 个值):

    3, 6, 9, 1022, 1027, 12, 15, 18, 1031, 1038, 21, 24, 27, 1043,  
    1048, 1053, 1058, 1063, 1068, 1073, 1078, 1083, 1088, 1093, 1098
    

    五人一组的近似中位数:

     #1    #2    #3    #4    #5
    
       3    12    21  1053  1078
       6    15    24  1058  1083
       9    18    27  1063  1088
    1022  1031  1043  1068  1096
    1027  1038  1048  1073  1098
    

    中位数为五的近似中位数:

    9, 18, 27, 1063, 1088
    

    作为支点的近似中位数:

    27
    

    5 个分区的中位数为 27(取决于方法):

    small: 3, 6, 9, 24, 21, 12, 15, 18
    pivot: 27
    large: 1031, 1038, 1027, 1022, 1043, 1048, 1053, 1058,  
           1063, 1068, 1073, 1078, 1083, 1088, 1093, 1098
    

    较小的组有 8 个元素,较大的组有 16 个元素。我们在 25 个元素中寻找中间的第 13 个元素,所以现在我们在 16 个元素中寻找第 13 - 8 - 1 = 第 4 个元素:

    五人一组:

     #1    #2    #3    #4
    
    1031  1048  1073  1098
    1038  1053  1078
    1027  1058  1083
    1022  1063  1088
    1043  1068  1093
    

    五人组的中位数:

    1031, 1058, 1083, 1098
    

    作为支点的近似中位数:

    1058
    

    以 1058 为轴心划分的五个中位数的范围(取决于方法):

    small: 1031, 1038, 1027, 1022, 1043, 1048, 1053
    pivot: 1058
    large: 1063, 1068, 1073, 1078, 1083, 1088, 1093, 1098
    

    较小的组有 7 个元素。我们在寻找 16 的第 4 个元素,所以现在我们在 7 个中寻找第 4 个元素:

    五人一组:

     #1    #2
    
    1031  1048
    1038  1053
    1027
    1022
    1043
    

    五人组的中位数:

    1031, 1048
    

    作为支点的近似中位数:

    1031
    

    以 1031 为轴心划分的五个中位数的范围(取决于方法):

    small: 1022, 1027
    pivot: 1031
    large: 1038, 1043, 1048, 1053
    

    较小的部分有 2 个元素,较大的部分有 4 个,所以现在我们寻找 4 - 2 - 1 = 4 个元素中的第一个:

    五的中位数作为支点:

    1043
    

    以 1043 为中心划分的五个中位数的范围(取决于方法):

    small: 1038
    pivot: 1043
    large: 1048, 1053
    

    较小的部分只有一个元素,我们正在寻找第一个元素,所以我们可以返回小元素 1038。

    如您所见,1038 是原始 25 个 5 的中位数的确切中位数,原始数组 125 中有 62 个较小的值:

    1 ~ 27, 1001 ~ 1011, 1013 ~ 1023, 1025 ~ 1037
    

    这不仅将其置于 30~70% 的范围内,而且意味着它实际上是确切的中位数(请注意,这是这个特定示例的巧合)。

    【讨论】:

    • 我也有和 OP 一样的困惑。我曾认为(直到阅读您的帖子)近似中位数在 INITIAL ARRAY 的中位数的 20% 以内(即在程序的最开始时),但它实际上在您的数组中位数的 20% 以内传入,当你递归超过 1 级深度时,它不是初始数组。
    【解决方案2】:

    我完全同意你的分析,直到你得到五个元素的每个块的中值,当你剩下这个元素集合时:

    3 6 9 1022 1207 12 15 18 1031 1038  21 24 27 1043 1048 1053 1058 1063 1068 1073 1078 1083 1088 1093 1098
    

    你说得对,在这一点上,我们需要得到这个元素集合的中位数。但是,中位数算法完成此操作的方式与您提出的不同。

    当您进行分析时,您再次尝试通过将输入分成大小为 5 的块并取每个块的中值来获得这组值的中值。但是,这种方法实际上不会给你中位数的中位数。 (您可以通过注意返回 27 来了解这一点,这不是该值集合的真正中位数)。

    median-of-medians 算法实际取回中位数的方法是递归调用整个算法来获取这些元素的中位数。这与只是重复地将事物分解成块并计算每个块的中位数略有不同。特别是,每个递归调用都会

    • 使用 5 组启发式方法估算枢轴,
    • 递归调用自身的函数以找到这些中位数的中位数,然后
    • 对该中间值应用分区步骤,并使用它来确定如何从那里开始。

    在我看来,这个算法太复杂了,无法手动追踪。您确实需要相信这一点,因为您进行的每个递归调用都在比您开始时更小的数组上工作,每个递归调用确实会按照它说的去做。因此,当您像以前一样只剩下每个组的中位数时,您应该相信,当您需要通过递归调用获得中位数时,您最终会得到真正的中位数。

    如果您查看您在第一步中生成的中位数的真实中位数,您会发现它确实位于原始数据集的第 30 和第 70 个百分位数之间。

    如果这看起来令人困惑,请不要担心 - 您的陪伴非常好。该算法以难以理解而著称。对我来说,理解它的最简单方法是只相信递归有效,并且只追踪一层,在所有递归调用都有效的假设下工作,而不是试图一直走到底层递归树。

    【讨论】:

    • 据我从 Wikipedia 页面了解,median-of-medians 不会在中间值 5 列表中递归调用自身,而是调用 quickselect 算法,然后调用中间值-中位数。不同之处在于快速选择返回实际中位数,而不是近似中位数。这是正确的解释吗?
    • 中位数算法与快速选择是分开的,因此它不应该对快速选择进行任何递归调用。 (Quickselect 是一种随机选择算法,它随机选择枢轴。中位数在被发现时如此重要的原因之一是它具有完全确定性和最坏情况有效)。
    • 嗯,那么维基百科的文章充其量是令人困惑的,可能是不正确的。
    • @m69 是的,我同意。我想我应该去解决这个问题。 :-)
    猜你喜欢
    • 2012-03-18
    • 2012-01-18
    • 2012-09-14
    • 2015-06-22
    • 1970-01-01
    • 1970-01-01
    • 2012-02-18
    • 2021-11-14
    • 2015-08-07
    相关资源
    最近更新 更多