最糟糕的Quicksort算法案例

时间:2016-11-07 23:24:07

标签: arrays algorithm sorting time-complexity quicksort

我发现了许多快速排序算法的实现,但最后我决定坚持这个:

public static void quickSort(int array[], int start, int end)
        {
            if(end <= start || start >= end) { 

            } else {
            int pivot = array[start];
            int temp = 0 ;
            int i = start+1;

            for(int j = 1; j <= end; j++)  { 
                if(pivot > array[j]) { 
                    temp = array[j];
                    array[j] = array[i];
                    array[i] = temp;
                    i++;
                }

            }
            array[start] = array[i-1];
            array[i-1] = pivot;
            quickSort(array, start, i-2);
            quickSort(array, i, end);
        }} 

有几件我感到困惑的事情 为什么有些人建议将第一个元素作为一个支点,其他人则告诉选择中间元素,有些人会告诉你应该选择最后一个元素作为支点,不会有所不同吗? 让我们说我试图说明为什么如果数组被排序,快速排序将O(n ^ 2)作为最坏情况的增长顺序。
我有以下数组:
{1,2,3,4,5,6}。
如果我选择第一个元素作为我的枢轴元素,它是否会将它与其他所有元素进行比较,然后只是将它与自身交换,并且只是O(n)?然后它将进一步前进到两行,即O(logn)

quickSort(array, start, i-2);
quickSort(array, i, end);

所以最后,即使它是一个有序的整数列表,它仍然是O(nlogn)?

如果我决定选择我的最后一个元素作为我的枢轴元素,它会不会完全不同?它将交换6和1,因此与枢轴元素是数组中的第一个元素相比,它将执行完全不同的操作。

我只是不明白为什么最坏的情况是O(n ^ 2)。

任何帮助将不胜感激!

3 个答案:

答案 0 :(得分:6)

Quicksort的重点是找到一个将阵列分成两个大致相等的部分的枢轴。这就是你从log(n)获得的地方。

假设有一个大小为n的数组,并且在每次迭代时都可以将数组分成相等的部分。然后我们有:

T(n) = 2 * T(n / 2) + O(n)
     = 4 * T(n/4) + 2 * O(n)
.
.
(log(n) steps)
.
.
    = 2^log(n) * T(1) + log(n) * O(n)
    = n * O(1) + O(n * log(n))
    = O(n * log(n))

现在,如果我们将数组分区为1n-1,我们会得到:

T(n) = T(1) + T(n-1) + O(n) = T(n-1) + O(n)
     = T(n-2) + O(n-1) + O(n)
     = T(n-3) + O(n-2) + O(n-1) + O(n)
.
.
(n-1) steps
.
.
    = T(1) + O(2) + O(3) + ... + O(n)
    = O(1 + 2 + 3 + .... + n)
    = O(n^2)

如果您提到,以下两个内容都不会单独O(log(n))。如果数组已排序,则一个为O(1),另一个为T(n-1)。因此,您将获得O(n^2)复杂性。

quickSort(array, start, i-2); // should be constant time
quickSort(array, i, end); // should be T(n-1)

正如@MarkRansom在下面提到的,这不是排序数组所独有的。一般来说,如果你选择的方式是阵列分区非常不均匀,那么你将遇到这种最坏情况的复杂性。例如,如果数组未排序,但您始终为枢轴选择最大(或最小,具体取决于您的实现),则会遇到同样的问题。

答案 1 :(得分:3)

QuickSort首先将具有更高值的所有内容移动到列表的 end ,以及更低的所有内容 >值列表的开头

如果轴心点的值是列表中的最小值,则列表中的每个值都将移动到列表的末尾。但是,仅确定移动所有这些值的位置需要O(n)工作。然后,如果您选择第二低的值,然后选择第三低的值等,那么您最终将O(n)工作n/2次。 O(n²/2)简化为O(n²)

  

为什么有些人建议将第一个元素作为支点,其他人告诉我们选择中间元素,有些人会告诉你应该选择最后一个元素作为支点,是不是会有所区别?

所有这一切都是为了猜测(不扫描整个列表)哪个元素最有可能接近数据集的中位数,从而使您接近最佳状态 - 案件行为尽可能。

  • 如果您的数据完全是随机的,那么您选择的内容并不重要 - 您同样可能获得良好的支点,并且始终选择的机会最糟糕的支点非常渺茫。选择第一个或最后一个值是最简单的选项。
  • 如果您的数据是预先排序的(或大部分是这样),选择中间可能会让您获得最佳值之一,而选择第一个或最后一个元素将始终为您提供最差的支点。

在现实生活中,处理主要是预先排序的数据的可能性足够高,以至于代码的复杂程度可能略高。关于这个主题的The Wikipedia section可能值得一读。

答案 2 :(得分:3)

下面是一个使用3的中间值的快速排序,Hoare分区的变体排除了等于pivot的中间元素(它们已经被排序),并且仅通过使用递归将堆栈复杂性限制为O(log(n))较小的部分,然后循环回到较大的部分。最坏情况下的时间复杂度仍为O(n ^ 2),但这需要中位数为3才能重复选择小值或大值。当所有值相同时(由于排除等于pivot的中间值),最佳情况O(n)发生。通过使用中位数的中位数,时间复杂度可以限制为O(n log(n)),但是这样的开销使得平均情况要慢得多(我想知道它是否最终比堆排序慢。中位数为中位数,它肯定比合并排序慢,但合并排序需要第二个数组大小相同或原始数组大小的1/2。)

http://en.wikipedia.org/wiki/Median_of_medians

Introsort通过根据递归级别切换到堆排序来解决最坏情况下的时间复杂性。

http://en.wikipedia.org/wiki/Introsort

typedef unsigned int uint32_t;

void QuickSort(uint32_t a[], size_t lo, size_t hi) {
    while(lo < hi){
        size_t i = lo, j = (lo+hi)/2, k = hi;
        uint32_t p;
        if (a[k] < a[i])            // median of 3
            std::swap(a[k], a[i]);
        if (a[j] < a[i])
            std::swap(a[j], a[i]);
        if (a[k] < a[j])
            std::swap(a[k], a[j]);
        p = a[j];
        i--;                        // Hoare partition
        k++;
        while (1) {
            while (a[++i] < p);
            while (a[--k] > p);
            if (i >= k)
                break;
            std::swap(a[i], a[k]);
        }
        i = k++;
        while(i > lo && a[i] == p)  // exclude middle values == pivot
            i--;
        while(k < hi && a[k] == p)
            k++;
        // recurse on smaller part, loop on larger part
        if((i - lo) <= (hi - k)){
            QuickSort(a, lo, i);
            lo = k;
        } else {
            QuickSort(a, k, hi);
            hi = i;
        }
    }
}