快速排序
快排思想:
如果要排序数组中下标从 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,就说明所有的数据都有序了。
快排和归并一样,利用的也是分治的思想
归并排序的处理过程是由下到上的,先处理子问题,然后再合并。
而快排正好相反,它的处理过程是由上到下的,先分区,然后再处理子问题。
快速排序常用递归来实现,可以推出快速排序排序的递推公式和中止条件为:
递推公式:
quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1… r)
终止条件:
p >= r
这里和归并排序的终止条件不同,归并排序终止条件也可以写为p>=r,但是实际上p==r就够了。
但是在快排中若写成p==r,则会栈溢出,因为快排中q+1,也就是p,会大于r。
归并排序中有一个merge函数,而快速排序中有一个 partition() 分区函数。partition() 分区函数实际上我们就是随机选择一个元素作为 pivot,然后对 A[p…r]分区,函数返回 pivot 的下标。(一般情况下,可以选择 p 到 r 区间的最后一个元素作为pivot)
分区函数的处理有点类似选择排序,通过游标 i 把 A[p…r-1]分成两部分。
A[p…i-1]的元素都是小于 pivot 的,我们暂且叫它“已处理区间”
A[i…r-1]是“未处理区间”。我们每次都从未处理的区间 A[i…r-1]中取一个元素 A[j],与 pivot 对比,如果小于 pivot,则将其加入到已处理区间的尾部,也就是 A[i]的位置。
在数组某个位置插入元素,需要搬移数据,非常耗时。但是在这里,我们可以交换。
只需要将 A[i]与 A[j]交换,就可以在 O(1) 时间复杂度内将 A[j]放到下标为 i 的位置。
快速排序的执行效率
最好情况下时间复杂度
O(nlogn)
最坏情况下时间复杂度
O(n²)
平均情况下时间复杂度
O(nlogn)
快速排序的内存消耗
快速排序算法的运行并不需要额外的存储空间,所以空间复杂度是 O(1),是一个原地排序算法
快速排序的稳定性
因为快速排序涉及数据的交换,所以快速排序是不稳定的排序算法。
比如序列 6,8,7,6,3,5,9,4,在经过第一次分区操作之后,第一个6会与3交换位置,两个 6 的相对先后顺序就会改变。
快速排序的代码实现
public class QuickSort {
public void quickSort(int[] array, int n) {
quickSortInternally(array, 0, n - 1);
}
private void quickSortInternally(int[] array, int p, int r) {
if (p >= r) return;
int q = partition(array, p, r);
quickSortInternally(array, p, q - 1);
quickSortInternally(array, q + 1, r);
}
private static int partition(int[] a, int p, int r) {
int pivot = a[r];
int i = p;
for(int j = p; j < r; ++j) {
if (a[j] < pivot) {
if (i == j) {
++i;
} else {
int tmp = a[i];
a[i++] = a[j];
a[j] = tmp;
}
}
}
int tmp = a[i];
a[i] = a[r];
a[r] = tmp;
return i;
}
}