快排的思想是这样的:
- 如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。
- 遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。
根据分治、递归的处理思想,我们可以用递归排序下标从 p 到 q-1 之间的数据和下标从 q+1 到 r 之间的数据,直到区间缩小为 1,就说明所有的数据都有序了。
eg:快排递归实现:
private static void quickSortInternal(int[] array,int l,int r){
if(l >= r){
return;
}
int q = partition(array,l,r);
quickSortInternal(array,l,q - 1);
quickSortInternal(array,q + 1 ,r);
}
partition() 分区函数就是随机选择一个元素作为 pivot(一般情况下,可以选择 p 到 r 区间的最后一个元素或第一个元素),然后对 A[p…r] 分区,函数返回 pivot 的下标。
如上图所示,我们每次选取待排序数组的第一个元素作为分区点,然后从第二个元素开始从前向后遍历,i指向当前正在遍历的元素,从array[l+1]…array[j]的元素均小于v,从array[j+1]…array[i-1]的元素均大于v。每当碰到比分区元素小的节点就与array[j+1]节点进行交换然后橙色区域的范围就扩大1个长度,这样走下来当i走完整个待排序分区后整个待排序数组如下:
最后只需要将l指向的元素与j指向的元素交换即可达到j之前的元素全部小于分区点,j之后的元素全部大于分区点,如图所示:
eg:原地分区partition实现
/**
* 对array[l...r]部分进行partition操作
* 返回p, 使得array[l...p-1] < array[p] ; array[p+1...r] > array[p]
* @param array 待排序数组
* @param l 数组开始点
* @param r 数组结束点
* @return 分区点下标
*/
private static int partition(int[] array,int l,int r) {
// 默认比较元素为待排序数组的第一个元素
int v = array[l];
int j = l;
for (int i = l + 1;i <= r;i++) {
// 每当碰到小于比较元素的值与j+1位置交换,小于区间长度加一
if (array[i] < v) {swap(array,j+1,i);
j++;
}
}
// 当for循环⾛走完只需要将array[l]位置与array[j]位置交换即可保证索引小于j的元素均小于v,
// 索引大于j的元素均大于j
swap(array,l,j);
return j;
}
/**
* 交换数组中两个索引下标的元素
*/
private static void swap(int[] array,int x,int y) {
int temp = array[x];
array[x] = array[y];
array[y] = temp;
}
算法总结:
- 快排是一种原地、不稳定的排序算法。
- 快排的时间复杂度是 O(nlogn)。
但是,如果数组中的数据原来已经是有序的了,比如 1,3,5,6,8。如果我们每次选择最后一个元素作为 pivot,那每次分区得到的两个区间都是不均等的。我们需要进行大约 n 次分区操作,才能完成快排的整个过程。每次分区我们平均要扫描大约 n/2 个元素,这种情况下,快排的时间复杂度就从 O(nlogn) 退化成了 O(n^2)。
因此我们可以每次在整个待排序数组中随机选取⼀个元素作为分区点来优化的分区问题,代码如下:
// 随机选取待排序数组中的任意一个元素
int randomIndex = (int) (Math.random()*(r-l+1) + l);
swap(array,l,randomIndex);
int v = array[l];