快速排序(quick sort)是目前应用最广泛的排序算法,它的平均复杂度为O(NlogN),但因其内循环较小,所以速度很快,而且不需要太多额外的空间(主要是递归调用所需的栈空间,对于随机文件不大于logN)。关于算法的基础介绍,网上已有很多讲解得很好的资料,如July同学的《快速排序算法》,我就不拾人牙慧了,切入正题。
快速排序有点像二分搜索(binary search):思想很简单,算法很高效,大家都知道,实现很困难。快速排序的一大特点就是效率不稳定,运行时间与输入数据密切相关,原因在于,快排是基于划分(partition)的排序算法,划分操作决定了整个算法的效率。许多简单实现中,划分的基准(pivot)都取输入序列中的第一个或最后一个元素。对于随机数据而言,这是足够有效的;但对于已经有序或者逆序的数据,这种划分将导致快速排序的时间效率退化成O(N2),空间使用也会达到O(N)。可见一个好的划分过程对快速排序多么重要,因此第一个加速就从划分开始。
划分(partition)
划分操作中有两个值得关注的方面:
- 划分的过程
- 划分的基准
快排中移动元素的实际操作由划分完成,因此划分过程必须快速。目前已知的一个非常高效的算法,通过从数组两端向中间扫描的方法来完全划分。
令划分的基准v放在数组最右边,指针i从left开始向右扫描,遇到大于v的值则停住;同时令指针j从right-1开始向左扫描,遇到小于v的值时停下来,交换i和j指向的值,然后重复上述过程,直至i与j相遇。
该算法维护了一个不变式:i左边的元素都小于v;j右边的元素都大于v。这个策略是线性时间地原地划分,只扫描数组一次就能完成,C++实现如下(请仔细斟酌):
template <typename Item>
int partition(Item a[], int left, int right) {
int i = left-1, j = right;
Item v = a[right];
for (;;) {
while (a[++i] < v) {}
while (a[--j] > v)
if (j == left)
break;
if (i >= j)
break;
exchange(a[i], a[j]);
}
exchange(a[i], a[right]);
return i;
}