快速排序法
从冒泡排序谈起
冒泡排序法体现了数学归纳法的一种朴素应用。以递增(非递减)排序为例,每一轮冒泡将当前数列[a0,a1…an-1]的最大元素交换至数列最右端,从而将数列的无序部分的长度收缩至n-1。经过n-1次冒泡,数列的无序部分长度收缩至1,排序完成。java实现代码如下:
for(int i = 0; i < array.length; i++){
for(int j = 0; j < array.length - i - 1; j++){
if(array[j] < array[j + 1]){
int t = array[j];
array[j] = array[j + 1];
array[j + 1] = t;
}
}
}
冒泡排序的每轮冒泡(即代码中的外层循环)中,数列整个无序部分的所有元素均参与比较,比较运算次数A(n)与数列无序程度无关,交换次数B(n)则受数列无序程度影响且收敛于比较次数。因此,选择比较次数为时间复杂度的计算基准,易得时间复杂度为O(n^2)。
快速排序法:分治策略的应用
由于数学归纳法的特性,问题规模的缩小速度为常数速度(问题规模以n的正比例函数收敛,至1结束)。为了进一步提高排序算法的速度,需要考虑以新的数学思路进行处理,分治法的思路是将一个问题规模为n的大问题分割成两个问题规模为k:n-k的小问题,再继续递归的分割,最终有概率在lg(n)的分割深度下解决问题。
对于排序问题来说,在理想的分治情况下对数列进行分割(即分割点在数列中点),分治效率最高,问题规模的收敛速度最快,需log2(n)趟排序结束。在最坏情况下,按1:n-1分割数列,分治效率最低,需n-1趟结束,事实上快排在这种情况下的时间复杂度将退化为O(n^2)。
下面是快速排序法的数学描述:
(1)从A[0,n-1]中选取枢轴元素
(2)重排A中元素,并将其划分为左右两部分,使得数组中所有比枢轴元素小的元素在左半部分,比枢轴元素大的在右半部分
(3)对左右两部分递归地执行(1)(2)直至子数组长度为1
基于上述思路进行排序,能否有效地将平均情况下的时间复杂度控制到O(nlogn)有两个问题,一是前面提到的分割深度问题,二是处理每一层的2^k(k为分割的深度)个子数组的程序步可否控制在O(n)。可喜的是,问题二的答案是肯定的,而问题一涉及到枢轴元素的选取,优秀的选取策略将能更好地对数列进行分割。
为了减少额外空间的消耗,多数语言对步骤(2)的代码实现都是基于交换的,通过左右哨兵向中间移动,遍历整个数列,直至相遇,是一种很巧妙的元素交换手段。一般情况下,枢轴元素会选择待排序数组的首个元素。
一趟快速排序的算法是(来自百度百科):
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j–),找到第一个小于key的值A[j],将A[j]的值赋给A[i];
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]的值赋给A[j];
5)重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。
案例演示
待排序的数组如下:
下标 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
元素 | 5 | 1 | 8 | 4 | 7 | 9 |
哨兵 | i | j |
初始状态下,哨兵i位于数组的最左端,哨兵j位于数组的最右端,枢轴元素a0=5。首先,哨兵j自右向左移动,直至寻找到第一个小于key的元素4,然后与key进行交换,交换后的结果如下所示
下标 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
元素 | 4 | 1 | 8 | 5 | 7 | 9 |
哨兵 | i | j | - | - |
下标 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
元素 | 4 | 1 | 5 | 8 | 7 | 9 |
哨兵 | i | j |
下标 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
元素 | 4 | 1 | 5 | 8 | 7 | 9 |
哨兵 | i,j |
public void quicksort(int[] a, int left, int right){
if(right <= left){
return;
}
int l = left; //左哨兵
int r = right; //右哨兵
int k = left; //枢轴
while(l != r){
int t = 0;
while((a[r] >= a[k])&&(r > l)){ //当枢轴右方发现第一个小于枢轴的元素,交换
r--;
}
t = a[r];
a[r] = a[k];
a[l] = t;
k = r;
while((a[l] <= a[k])&&(r > l)){ //当枢轴左方发现第一个大于枢轴的元素,交换
l++;
}
t = a[l];
a[l] = a[k];
a[k] = t;
k = l;
}
//完成一趟枢轴分割后递归地进行分割
quicksort(a, left, k - 1);
quicksort(a, k + 1, right);
}