一、两者对比
快速排序
- 当两个子数组都有序时,整个数组也就有序
- 递归调用发生在处理整个数组之后
- 切分的位置取决于数组的内容
归并排序
- 将数组分为两个子数组分别排序,并将有序的子数组归并,以将整个数组排序
- 递归调用发生在处理整个数组之前
- 一个数组被等分为两半
二、快速排序详解
源码解析
public void QuickSort(int[] a) {
shuffle(a);
sort(a, 0, a.length - 1);
}
private void sort(int[] a, int lo, int hi) {
if (hi <= lo) return;
int j = partition(a, lo, hi);
sort(a, lo, j - 1);
sort(a, j + 1, hi);
}
private int partition(int[] a, int lo, int hi) {
int i = lo; // 左指针开始位置
int j = hi + 1; // 右指针开始位置
int v = a[lo]; // 轴点
while (true) {
while (i < hi && a[++i] < v) {} // 从左向右扫描,直到找到一个大于等于轴点的元素
while (lo < j && a[--j] > v) {} // 从右向左扫描,直到找到一个小于等于轴点的元素
if (i >= j) break;
exch(a, i, j); // 交换这两个没有排好的元素
}
// 轴点和左子数组最右元素交换
// from v <=v >=v
// to <=v v >=v
exch(a, lo, j);
return j; // 返回轴点位置
}
private void exch(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
// Fisher–Yates shuffle algorithm
private void shuffle(int[] a) {
Random random = new Random();
for (int i = a.length - 1; i >= 0; i--) {
// 0 <= r <= i
int r = random.nextInt(i + 1);
exch(a, i, r);
}
}
快速排序最好和最坏的情况
最好情况:
- 轴点(pivot)总是落在子数组的中位数上
最坏情况:
- 坏排序:原数组已经有序
- 坏元素:所有元素都是重复的
在快速排序中,一个数组的partition的递归函数调用树看上去就像二叉树一样,如果pivot每次都接近子数组的中位数,那么整棵函数调用二叉树也会更接近平衡。
三、归并排序详解
源码解析
public void MergeSort(int[] a) {
int[] aux = new int[a.length];
sort(a, 0, a.length - 1);
}
private void sort(int[] a, int lo, int hi) {
if (hi <= lo) return;
int mid = lo + (lo + hi) / 2;
sort(a, lo, mid); // 排序左半边
sort(a, mid + 1, hi); // 排序右半边
merge(a, lo, mid, hi);
}
private void merge(int[] a, int lo, int mid, int hi) {
for (int k = lo; k <= hi; k++) {
aux[k] = a[k];
}
int i = lo; // 左半边起始位置
int j = mid + 1; // 右半边起始位置
for (int k = lo; k <= hi; k++) {
if (i > mid) a[k] = aux[j++]; // 左半边用完,取右半边元素
else if (j > hi) a[k] = aux[i++]; // 右半边用完,取左半边元素
else if (aux[j] < aux[i]) a[k] = aux[j++]; // 右半边元素更小,取右半边元素
else a[k] = aux[i++]; // 左半边元素更小,取左半边元素
}
}
四、参考资料
Robert Sedgewick & Kevin Wayne《算法(第四版)》
UC Berkeley CS 61B