基本思想
快速排序也是一个典型的分治思想(divide and conquer)的运用。对于分治法需要了解如下的一个流程:
1.将当前的大问题分解成若干个子问题
2.解决每一个子问题
3.合并子问题的解
对于运用到数组的排序来说,很显然可以将数组一分为二,分别将其左右排序完成后,再对整个数组进行排序。上面描述的便是归并排序的一般思路。
而对于快速排序则略有不同:先选取一个枢轴量pivot
,将数组分为三块:[L][pivot][R]
,其中[L]
表示当前数组比枢轴量值都小的元素,而[R]
表示比枢轴量都大的元素。这就完成了第一步分解的过程。然后分别对这L
,R
两个子数组进行快速排序,这就是第二部解决的过程。由于是原址排序,最后一步合并子问题解的过程可以省略。(原址的问题下面回叙述。)
因此,快速排序涉及到的核心问题在于如何选取一个恰当的枢轴量和如何对数组进行划分。在教材的实现中,往往取
A[1]
也就是第一个元素作为枢轴。当然作为改进的办法是,每次随机从数组中选取一个元素作为枢轴。选取完枢轴以后,需要进行划分,也就是将枢轴放到一个恰当的位置,使枢轴左边的元素都比枢轴小,而右边的元素都比其大。划分的过程在2.2节顺序表的一些算法设计中也有叙述,这里略去了。
实现
这里使用了Hoare的划分方法实现了快速排序。Hoare方法的思想在于两边往中间夹,遇到不满足预定条件的就交换位置。在两边夹的过程中要确保两头指针
i
和
/**
* 快速排序
* @param std::vector<_Ty> & a 待排序的数组a
* @param int begin 需要排序的子数组的起始元素的下标
* @param int end 需要排序的子数组的末尾元素的下标
*/
template<typename _Ty>
void quick_sort(std::vector<_Ty> & a, int begin, int end){
if (end - begin <= 1)
return;
//下面是划分的过程,这里使用了Hoare划分方法,也就是
//国内大多数教材采用的划分方式。
_Ty pivot = a[begin];
int i = begin , j = end;
while (i < j){
while (j > i && a[j] > pivot) //不要忘记条件j>i
j--;
a[i] = a[j];
while (i < j && a[i] < pivot)
i++;
a[j] = a[i];
}
a[i] = pivot;
output_list(a);
//递归进行子数组排序
quick_sort(a, begin, i - 1);
quick_sort(a, i + 1, end);
}
对于递归的算法很难直接输出每一次迭代的情况,因此只在每次划分完成后进行一次输出,观察其排序情况。对于测试数据5 4 11 18 1 70 35 90 100 2
可以得到如下输出:
2 4 1 5 18 70 35 90 100 11
1 2 4 5 18 70 35 90 100 11
1 2 4 5 11 18 35 90 100 70
1 2 4 5 11 18 35 90 100 70
1 2 4 5 11 18 35 70 90 100
1 2 4 5 11 18 35 70 90 100
时间与空间复杂度
一般情况下认为快速排序的平均时间复杂度是
O(nlogn)
,最坏时间复杂度
O(n2)
。由于递归要考虑栈深度问题,因此平均的空间复杂度是
O(logn)
,最坏空间复杂度是
O(n)
。
不难理解平均的情况。每次选取枢轴量恰好将数组等分,因此就得到了一个高度为
logn
的递归树。最坏情况则发生在,每次选取枢轴量后得到的划分,枢轴量恰好位于数组边缘位置(
A[1]
或
A[n]
),这样这棵递归树就会退化为一边倒的一棵二叉树。对于每次选取第一个元素或者最后一个元素作为枢轴进行划分的情况下,在数据几乎有序的情况下,快速排序会退化的非常低效。所以枢轴的选择一般采取更好的方法。而对于随机选取的枢轴来说,期望时间也是
O(nlgn)
,最坏情况同样是
O(n2)