【问题标题】:Java Quicksort quadratic runtime behaviourJava Quicksort 二次运行时行为
【发布时间】:2016-04-04 17:15:03
【问题描述】:

我尝试在 Java 中实现一种高效的排序算法。为此,我还实现了快速排序并使用以下代码:

public class Sorting {
    private static Random prng;

    private static Random getPrng() {
        if (prng == null) {
            prng = new Random();
        }
        return prng;
    }

    public static void sort(int[] array) {
        sortInternal(array, 0, array.length - 1);
    }

    public static void sortInternal(int[] array, int start, int end) {
        if (end - start < 50) {
            insertionSortInternal(array, start, end);
        } else {
            quickSortInternal(array, start, end);
        }
    }

    private static void insertionSortInternal(int[] array, int start, int end) {
        for (int i=start; i<end - 1; ++i) {
            for (int ptr=i; ptr>0 && array[ptr - 1] < array[ptr]; ptr--) {
                ArrayUtilities.swap(array, ptr, ptr - 1);
            }
        }
    }

    private static void quickSortInternal(int[] array, int start, int end) {
        int pivotPos = getPrng().nextInt(end - start);
        int pivot = array[start + pivotPos];
        ArrayUtilities.swap(array, start + pivotPos, end - 1);
        int left = start;
        int right = end - 2;
        while (left < right) {
            while (array[left] <= pivot && left < right) {
                ++left;
            }
            if (left == right) break;
            while (array[right] >= pivot && left < right) {
                right--;
            }
            if (left == right) break;
            ArrayUtilities.swap(array, left, right);
        }
        ArrayUtilities.swap(array, left, end - 1);
        sortInternal(array, start, left);
        sortInternal(array, left + 1, end);
    }
}

ArrayUtilities.swap 只是交换数组中的两个给定元素。从这段代码中,我期望 O(n log(n)) 运行时行为。但是,要排序的一些不同长度的数组会产生以下结果:

10000 个元素:32 毫秒

20000 个元素:128 毫秒

30000 个元素:296 毫秒

测试在每种情况下运行 100 次,然后计算运行次数的算术平均值。但显然,与预期的行为相反,运行时是O(n^2)。我的算法有什么问题?

【问题讨论】:

  • Quicksort 当然有O(n lg n) 最佳情况 性能,但也有O(n^2) worst case performance。为什么您希望在基准测试中获得最佳案例性能? (当然假设您使用 JMH 进行了基准测试,而不是手动进行,在这种情况下,无论如何这些数字可能毫无意义)
  • 如何填充输入数组?看起来您的快速排序对重复元素没有任何智能处理,因此如果输入有很多重复元素,性能将下降到 O(n^2)。
  • @blazs 抱歉;但你想告诉我什么?
  • 抱歉。我想确保 OP 在给定长度的同一数组上重复时间测量 100 次。 (由于运行时间分析与输入无关,这应该无关紧要,但让我们看看实验是如何完成的。也许这可以提示我们问题出在哪里。)

标签: java algorithm big-o quicksort


【解决方案1】:

在您的插入排序实现中,您的数组将按降序排序,而在您的快速排序中,数组按升序排序。所以替换(降序):

for (int ptr=i; ptr>0 && array[ptr - 1] < array[ptr]; ptr--)

for (int ptr=i; ptr>0 && array[ptr - 1] > array[ptr]; ptr--)

您的索引似乎也不正确。 尝试替换:

sortInternal(array, 0, array.length - 1);

与:

sortInternal(array, 0, array.length);

并且在插入排序中首先为循环你不需要做end - 1,即使用:

for (int i=start; i<end; ++i)

最后,在快速排序方法的开头添加if (start &gt;= end) return;

正如@ljeabmreosn 提到的,50 有点太大了,我会选择 5 到 20 之间的值。

希望有帮助!

【讨论】:

  • 这里几乎一切都是正确的。插入排序按降序排序,for (int ptr=i; ptr&gt;0 &amp;&amp; array[ptr - 1] &lt; array[ptr]; ptr--)需要替换成for (int ptr=i; ptr&gt;start &amp;&amp; array[ptr-1] &gt; array[ptr]; ptr--)(其实错误的版本是用插入排序对数组进行降序排序,有二次运行时间)。索引问题也存在。 if (start &gt;= end) return; 是不必要的,因为快速排序不用于那个小数组。最后,我将 50 替换为 30,这是我的最佳选择。
【解决方案2】:

对于长度小于 50 个元素的数组,使用插入排序“优化”的快速排序似乎是个问题。

假设我有一个大小为 65 的数组,而枢轴恰好是该数组的中位数。如果我通过您的代码运行该数组,您的代码将在枢轴左侧和右侧的两个 32 长度子数组上使用插入排序。这将导致 ~O(2*(n/2)^2 + n) = ~O(n^2) 平均情况。使用快速排序并为第一个枢轴实现a pivot picking strategy,时间平均情况为 ~O((nlog(n)) + n) = ~O(n(log(n) + 1)) = ~O(n*log(n))。不要使用插入排序,因为它仅在数组几乎排序时使用。如果您使用插入排序仅仅是因为对小数组进行排序的实际运行时间可能比标准快速排序算法(深度递归)运行得更快,那么您始终可以使用比插入排序运行得更快的非递归快速排序算法。

也许将“50”改为“20”并观察结果。

【讨论】:

  • 由于我还不能评论帖子(我的代表
  • 插入排序优化不是问题:由于边界 50 是固定的,因此运行时行为不应该是二次的。对于较小的数组,插入排序比快速排序更快,较大的数组按平均情况排序O(n log(n))quicksort。
  • 此外,给定情况下的二次运行时行为是不正确的:对大小为 65 的数组进行排序具有 O(1) 运行时。插入排序对于较小的数组来说更快,因为它的开销比快速排序(枢轴选取、分区等)要少。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-02
  • 1970-01-01
  • 2017-06-16
  • 1970-01-01
  • 2021-07-02
  • 2011-09-02
相关资源
最近更新 更多