我发现了许多快速排序算法的实现,但最后我决定坚持这个:
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)。
任何帮助将不胜感激!
答案 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))
现在,如果我们将数组分区为1
和n-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;
}
}
}