Quicksort 算法的最坏情况

Worst case of the Quicksort algorithm

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

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)。

任何帮助将不胜感激!

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 在下面提到的,这并不是排序数组独有的。通常,如果您以数组分区非常不均匀的方式选择枢轴,您将 运行 陷入这种最坏情况的复杂性。例如,如果数组未排序,但您始终选择 maximum(或 minimum,具体取决于您的实现),您将运行 陷入同样的​​问题。

QuickSort 首先将所有比主元值的值移动到列表的末尾,以及所有得到的值较低的值到列表的开头

如果您的轴心点处的值是列表中的最低值,则列表中的每个值都将移至列表末尾。但是,仅仅确定将所有这些值移动到哪里就需要 O(n) 工作。如果您随后选择第二低的值,然后选择第三低的值,依此类推,那么您最终将完成 O(n) 工作 n/2 次。 O(n²/2) 简化为 O(n²).

Why some people suggest taking the first element as a pivot point, others tell to pick the middle element and some will tell that you should pick the last element as your pivot point, wouldn't it be different?

这完全是尝试猜测(无需扫描整个列表)哪个元素最有可能接近数据集的中位数,从而为您提供尽可能接近最佳情况的行为。

  • 如果您的数据是完全随机的,那么您选择什么都没有关系——您同样有可能获得一个好的轴心点,并且始终如一的机会选择 最差 枢轴点非常渺茫。选择第一个或最后一个值是最简单的选项。
  • 如果您的数据已预排序(或大部分情况下已预排序),选择中间值可能会为您提供最佳值之一,而选择第一个或最后一个元素将始终为您提供最差的枢轴点。

在现实生活中,处理大部分预排序数据的可能性非常高,因此代码稍微复杂一些可能是值得的。 The Wikipedia section 关于这个主题可能值得一读。

下面是一个使用中位数 3 的快速排序,通过仅对较小的部分使用递归,然后对较大的部分进行循环,将堆栈复杂度限制为 O(log(n))。最坏情况下的时间复杂度仍然是 O(n^2),但这需要中位数为 3 才能重复选择较小或较大的值。通过使用中位数的中位数可以将时间复杂度限制为 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++;
        // 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;
        }
    }
}