我使用algs4 quickselect基于Wikipedia article实现了中位数选择算法中位数,但我的代码效果不佳:
1)据说中位数的中位数找到kth 最大元素。但是,我的代码找到了第k个最小元素。
2)我的实现比quickselect慢了1-20倍,但中位数算法的中位数应该渐近更快。
我已多次检查过所有内容,但我找不到问题。
public class MedianOfMedians {
public static Comparable medianOfMedians(Comparable[] nums, int k) {
return nums[select(nums, 0, nums.length - 1, k)];
}
private static int select(Comparable[] nums, int lo, int hi, int k) {
while (lo < hi) {
int pivotIndex = pivot(nums, lo, hi);
int j = partition(nums, lo, hi, pivotIndex);
if (j < k) {
lo = j + 1;
} else if (j > k) {
hi = j - 1;
} else {
return j;
}
}
return lo;
}
private static int pivot(Comparable[] list, int left, int right) {
// for 5 or less elements just get median
if (right - left < 5) {
return partition5(list, left, right);
}
// otherwise move the medians of five-element subgroups to the first n/5 positions
for (int i = left; i <= right; i += 5) {
// get the median of the i'th five-element subgroup
int subRight = i + 4;
if (subRight > right) {
subRight = right;
}
int median5 = partition5(list, i, subRight);
exch(list, median5, (int) (left + Math.floor((i - left) / 5d)));
}
// compute the median of the n/5 medians-of-five
return select(list,
left,
(int) (left + Math.ceil((right - left) / 5d) - 1),
(int) (left + (right - left) / 10d));
}
private static int partition5(Comparable[] list, int lo, int hi) {
for (int i = lo; i <= hi; i++) {
for (int j = i; j > lo; j--) {
if (less(list[j - 1], list[j])) {
exch(list, j, j - 1);
}
}
}
return (hi + lo) / 2;
}
private static int partition(Comparable[] a, int lo, int hi, int pivotIndex) {
exch(a, lo, pivotIndex);
int i = lo;
int j = hi + 1;
Comparable v = a[lo];
while (true) {
while (less(a[++i], v) && i != hi) { }
while (less(v, a[--j]) && j != lo) { }
if (j <= i) break;
exch(a, i, j);
}
exch(a, j, lo);
return j;
}
private static void exch(Comparable[] nums, int i, int j) { }
private static boolean less(Comparable v, Comparable w) { }
}
JUnit测试:
public class MedianOfMediansTest {
private final static int TESTS_COUNT = 100;
@org.junit.Test
public void test() {
// generate TESTS_COUNT arrays of 10000 entries from 0..Integer.MAX_VALUE
Integer[][] tests = generateTestComparables(TESTS_COUNT, 10000, 10000, 0, Integer.MAX_VALUE);
for (int i = 0; i < tests.length; i++) {
Integer[] array1 = Arrays.copyOf(tests[i], tests[i].length);
Integer[] array2 = Arrays.copyOf(tests[i], tests[i].length);
Integer[] array3 = Arrays.copyOf(tests[i], tests[i].length);
long time = System.nanoTime();
final int a = (Integer) MedianOfMedians.medianOfMedians(array1, 0);
long nanos_a = System.nanoTime() - time;
time = System.nanoTime();
final int b = (Integer) Quick.select(array2, 0);
long nanos_b = System.nanoTime() - time;
time = System.nanoTime();
Arrays.sort(array3);
final int c = array3[0];
long nanos_c = System.nanoTime() - time;
System.out.println("MedianOfMedians: " + a + " (" + nanos_a + ") " +
"QuickSelect: " + b + " (" + nanos_b + ") " +
"Arrays.sort: " + c + " (" + nanos_c + ")");
System.out.println(((double) nanos_a) / ((double) nanos_b));
Assert.assertEquals(c, a);
Assert.assertEquals(b, a);
}
}
public static Integer[][] generateTestComparables(int numberOfTests,
int arraySizeMin, int arraySizeMax,
int valueMin, int valueMax) {
Random rand = new Random(System.currentTimeMillis());
Integer[][] ans = new Integer[numberOfTests][];
for (int i = 0; i < ans.length; i++) {
ans[i] = new Integer[randInt(rand, arraySizeMin, arraySizeMax)];
for (int j = 0; j < ans[i].length; j++) {
ans[i][j] = randInt(rand, valueMin, valueMax);
}
}
return ans;
}
public static int randInt(Random rand, int min, int max) {
return (int) (min + (rand.nextDouble() * ((long) max - (long) min)));
}
}
答案 0 :(得分:4)
1)据说中位数的中位数找到第k个最大元素。 但是,我的代码找到第k个最小元素。
这并非严格属实。任何选择算法都可以找到最小或最大的元素,因为它基本上是相同的任务。这取决于你如何比较元素以及如何对它们进行分区(以后你总是可以做length - 1 - result
之类的事情)。您的代码确实似乎找到了 k 最小的元素,这是实现选择算法的最典型和最直观的方式。
2)我的实现比quickselect慢了1-20倍,但是 中位数算法的中位数应该渐近更快。
不仅渐近更快。在最坏的情况下渐进式渐近。在一般情况下,两者都是线性的,但MoM具有更高的常数因子。由于您随机生成测试,因此您不太可能遇到最坏的情况。如果您使用随机化的quickselect,那么对于任何输入,它不太可能遇到最坏的情况,否则概率将取决于所使用的枢轴选择算法。
考虑到这一点,以及中位数的中位数具有高常数因素的事实,你不应该期望它比快速选择表现更好!然而,它可能胜过排序,但即便如此 - 排序中的那些对数因子对于小输入而言并不大(lg 10000约为13-14)。
以my MoM solution for a LeetCode problem为例。对于 5亿个元素的数组,Arrays.sort
有时优于MoM。在最好的情况下,它运行速度快了两倍。
因此,MoM主要是理论上的兴趣。当你需要100%保证不超过一些时间限制时,我可以想象一个实际的用例。比如,飞机,航天器或核反应堆上的某些实时系统。时间限制不是很紧,但即使超过1纳秒,也是灾难性的。但这是一个非常人为的例子,我怀疑它实际上是它的运作方式。
即使您可以找到MoM的实际用例,也可以使用类似Introselect的内容。它基本上从quickselect开始,然后切换到MoM,如果事情看起来不好。但测试它将是一场噩梦 - 你会如何提出一个实际上迫使算法切换(因此测试MoM部分)的测试,特别是如果它是随机的?
你的代码整体上看起来很好,但是我将一些帮助方法包装为私有,或者甚至将它们移到另一个类中进行单独测试,因为这样的事情很难做到。如果结果正确,您可能不会注意到效果。例如,我不确定您的五人组代码是否100%正确。有时您会使用right - left
我希望看到的元素数量应为right - left + 1
。
另外,我会用纯整数算术等价替换那些ceil / floor调用。也就是说,Math.floor((i - left) / 5d))
=&gt; (i - left) / 5
,Math.ceil((right - left) / 5d)
=&gt; (right - left + 4) / 5
(顺便说一句,这是我不喜欢right - left
的部分,但我不确定它是否错了。