前言
我们通常习惯把快速排序和归并排序放在一起对比,它们有许多相似之处,也有不同的地方。学习完之后,你会对分治以及递归有更深的理解。
一、算法思路
下面是快速排序的思路:
(1)如果有一个或者更少的数字需要排序,则不做任何事情。
(2)否则,把区域分成“较小”数字和“较大”数字两部分,把较小的数字移到左边,把较大的数字移到右边。递归地对每个区域进行排序。整个数组现在是有序的。
二、算法实现
/** Arrange the numbers in data from smallest to largest. */
public static void quickSort(int[] data){
quickSortHelper(data, 0, data.length-1);
}
/**
* Arrange the numbers in data between indices bottom and top,
* inclusive, from smallest to largest.
*/
protected static void quickSortHelper(int[] data, int bottom, int top){
if (bottom <= top){
int midpoint = partition(data,bottom,top);
quickSortHelper(data,bottom, midpoint-1);
quickSortHelper(data, midpoint + 1, top);
}
}
/**
* Choose one element of data in the region between bottom and top,
* inclusive, as the pivot. Arrange the numbers so that those less
* than or equal to the pivot are to the left of it and those
* greater than the pivot are to the right of it. Return the final
* position of the pivot.
*/
protected static int partition(int[] data, int bottom, int top){
int pivot = data[top];
int firstAfterSmall = bottom;
for (int i = bottom; i < top; i++){
if (data[i] < pivot){
swap(data,i,firstAfterSmall);
firstAfterSmall++;
}
}
swap(data,firstAfterSmall,top);
return firstAfterSmall;
}
/** Swap the element of data at indices i and j. */
protected static void swap(int[] data, int i, int j){
int temp = data[i];
data[i] = data[j];
data[j] = temp;
}
三、算法图示
四、算法步骤
1.输入输出
输入
待排序数组
输出
因为快速排序是一个原位排序的方法,直接在原数组操作,所以不需要返回值。
public static void quickSort(int[] data){
quickSortHelper(data, 0, data.length-1);
}
2.递归分组
调用一个Helper
方法,实现数组的分组。值得注意的是,这里的分组应该与下面的分区区别开。分组是每次以分区方法partition()
得到的midpoint
为界,将数组分成两个子序列。分区是在当前序列内,将元素划分为较小区域和较大区域,以pivot
为界。还有就是,虽然分组的分割点叫做midpoint
,但是却不是在正中间的位置,这个位置是根据partition
得到的基准元素的最终位置来确定的,不要看到midpoint
就以为是(bottom+top)/2
。
protected static void quickSortHelper(int[] data, int bottom, int top){
if (bottom <= top){
int midpoint = partition(data, bottom, top);
quickSortHelper(data, bottom, midpoint-1);
quickSortHelper(data, midpoint + 1, top);
}
}
该处使用的url网络请求的数据。
3.分区
分区方法的作用是在当前数组内,将元素划分为较小区域和较大区域,以pivot
为界。那么如何确认pivot
的位置呢。这里我们使用当前数组中下标最大的一个元素作为基准pivot
,那么可不可以选择其他位置呢,这个留给读者思考。我们的目的是完成元素的分区,所以选择基准的位置也要考虑后续操作是否方便。firstAfterSmall
顾名思义,较小分区后的第一个元素(的下标)。所以它的左侧始终是已知较小区域。循环的作用是把所有比基准元素小的元素通过swap()
方法依次交换到当前数组的左侧。循环结束之后,firstAfterSmall
左侧都是比pivot
小的元素。此时,交换firstAfterSmall
所在位置的元素和pivot(即data[top])
。最终得到的结果就是:基准元素左侧的元素都小于它,右侧的元素都大于它。换个思路来想,我们相当于确定了基准元素在数组中的位置。
protected static int partition(int[] data, int bottom, int top){
int pivot = data[top];
int firstAfterSmall = bottom;
for (int i = bottom; i < top; i++){
if (data[i] < pivot){
swap(data,i,firstAfterSmall);
firstAfterSmall++;
}
}
swap(data,firstAfterSmall,top);
return firstAfterSmall;
}
protected static void swap(int[] data, int i, int j){
int temp = data[i];
data[i] = data[j];
data[j] = temp;
}
总结
最后,简单对比一下归并排序与快速排序。虽然都是采用了递归方法,使用分治的思想完成排序。但是归并排序是先分组,在归并的过程中完成排序,也就是说归并时的工作量会比较大。而快速排序也是先分组,但是是在分组的时候就逐步排序,归并的操作就十分简单了。当然还有诸如稳定性等问题,后面我们再深入讨论。