您对中位数算法感到困惑的原因是,虽然中位数返回的近似结果在实际中位数的 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,则枢轴是第 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
使用枢轴 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% 范围内,而且意味着它实际上是确切的中位数(请注意,这是这个特定示例的巧合)。