学习目标
深刻理解 quick sort 的 partition 的过程才有可能做出很好的实现。
快速排序在历史上的地位
Quick Sort,一段对于快速排序的描述:可能是二十世纪最伟大的排序算法之一。
我们首先先写一个基本的快速排序算法,后面我们再对这个基本的快速排序算法进行优化。
快速排序第 1 版的思路
从标定点后面一个一个地比较到底,(1)如果遇到比标定元素大的,就放过;(2)如果遇到比标定元素小的,就依次放在标定元素的后面(这句话说得有点含糊其辞,应该结合代码实现去理解这句话)。
理解快速排序的 partition
我们首先先回顾一下归并排序:归并排序不管数组的内容是什么,归并排序总是一分为二地去做排序运算,然后再归并起来。而快速排序的递归过程是这样的:
例如:待排序数组是 {4,6,2,3,1,5,7,8}
我们首先选择数组的第 1 个元素 4 作为基准。第 1 轮排好序以后,我们要达到的是这样一个效果:
- 第 1 个元素 4 的前面的所有的数都比 4 小,后面所有的数都比 4 大,也即是说:运算之前的第 1 个元素 4 它放在了它应该在的地方(最终排好序以后,4 就是在这个位置,我们没有理由再去无谓地挪动 4 的位置);
- 4 前面的元素 2,3,1 的相对位置是固定的,不保持它们原来数组的位置,所以不是原地排序;
- 4 后面的元素 6,5,7,8 的相对位置是固定的,不保持它们在原来数组中的位置,所以不是原地排序。
我们把上面这一轮的步骤称之为 Partition,Partition是快速排序算法的核心,正确地写出 partition 函数是实现快速排序的关键。
先写出快速排序算法的框架:
public class QuickSortTest {
// Partition
@Test
public void test01() {
int[] arr = {9, 8, 7, 6, 5, 4, 3, 2, 1};
int len = arr.length;
quickSort(arr, 0, len - 1);
}
/**
* @param arr
* @param left 左边界,可以取到
* @param right 右边界,可以取到
*/
private void quickSort(int[] arr, int left, int right) {
// 递归到底的条件是:当区间退化成一个元素的时候,就没有必要 partition 了
if (left >= right) {
return;
}
int p = partition(arr, left, right);
quickSort(arr, left, p - 1);
quickSort(arr, p + 1, right);
}
// 返回排好序的时候原来数组的首个元素最终应该放置的位置
private int partition(int[] arr, int left, int right) {
}
}
接下来,我们就要来实现 partition 了,每一次 partition 都将后面的元素进行整理,整理以后小于 4 的元素在数组的前半部分,大于 4 的元素在数组的后半部分。
下面我们具体讲解一下快速排序的实现。
l:通常我们选取左边界
j:是分界点,也就是例子中 4 这个元素的位置
我们逐渐遍历去“比较”,当前被访问的元素是 i。
我们首先写出来的这一版,经过随机生成数组元素的比较,已经比“归并排序”效率要高了。
代码实现:
/**
* @param arr
* @param left
* @param right
* @return
*/
private int partition(int[] arr, int left, int right) {
int v = arr[left];// 基准值
int j = left; // 这是要返回的那个元素的索引
for (int i = left + 1; i <= right; i++) { // i 是循环变量,每一个元素都要和基准元素比较
if (arr[i] < v) {
swap(arr, j + 1, i);
j++;
}
}
swap(arr, left, j);
return j;
}
private static void swap(int[] arr, int index1, int index2) {
int temp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = temp;
}
下面的图帮助我们理解 partition 的过程和边界值的选取。
理解递归思想的应用
快速排序的实现也利用了“递归”的思想。快速排序也是另一个O(nlogn) 级别的算法。
测试
对于近乎有序的数组而言,我们这一版的快速排序在 100 万这个级别的时候栈溢出了。
测试用例:1000000(100万),用于比较的排序方法:归并排序、快速排序
思考
接下来我们来谈一下,关于快速排序的优化措施。
1、底层使用插入排序,同样,我们使用 16 作为临界值
2、我们在脑子里想象一些比较极端的情况,也就是每一次作为基准的值迭代以后不落在中间那个地方。
快速排序最差的情况就是在数组近乎有序的时候,深度不是固定,最差的时候会退化成 O(n^2)。
思考一下快速排序的时间复杂度 O(nlogn) 是如何计算出来的,快速排序对于最糟糕的情况(逆序数组的排序)是可以达到 O(n^2) 这个级别的复杂度的。