
前置题目:可先学习相对更简单的快速选择算法,重点掌握划分(partition)的原理,题目见hot100 215.数组中的第K个最大元素。
思路:手写快排。
1.快速排序的核心思想:
(1)定义一个递归函数sort(left,right),表示把子数组[left,right]升序排列。
(2)划分子数组[left,right],划分方法同hot100 215.数组中的第K个最大元素。设划分后,基准元素pivot的下标为i。
(a)在pivot左侧的元素,继续递归排序,即递归调用sort(left,i - 1)。
(b)在pivot右侧的元素,继续递归排序,即递归调用sort(i + 1,right)。
(3)递归边界:如果left >= right,说明子数组为空或者只剩一个元素,此时无需排序,直接返回。
(4)递归入口:sort(0,n - 1)。
2.优化:如果子数组已是升序数组,直接返回。
3.疑问:
(1)如果不随机选择基准元素pivot,会发生什么?
答:
如果子数组是降序的,且我们每次都选子数组的第一个(或者最后一个)元素作为pivot,那么按照算法,j不变或者移动到最左边,划分是最不均匀的,算法会退化至O(n^2)。随机选择pivot的目的是能使划分在期望意义上是均匀的(j移动到子数组的中间),保证算法的期望时间复杂度为O(nlogn)。
(2)代码中的nums[i] < pivot和nums[i] > pivot,能否改成nums[i] <= pivot和nums[i] >= pivot?
答:
这个做法会在子数组所有元素相同时,划分后的j是子数组的最后一个元素的下标,是最不均匀的划分,算法也会退化至O(n^2)。
(3)代码中的i <= j能否改成i < j?
答:
这样的话会算错。来看一个例子nums = [2,1,3],pivot = 2。
(a)左指针i = 1移动到i = 2,右指针j = 2因为不满足i < j的条件,无法移动。此时我们交换2和nums[j] = 3,得到[3,1,2],返回j = 2。然而j = 2的左侧有大于pivot = 2的元素,划分失败。
(b)如果写成i <= j,那么最终i = 2,j = 1。此时我们交换2和nums[j] = 1,得到[1,2,3],返回j = 1。这样的划分就是正确的。
4.复杂度分析:
(1)期望时间复杂度:O(nlogn),其中n是nums的长度。在平均情况下,可以视作均匀划分(partition)。递归深度为O(logn),每一层都会涉及O(n)个元素的比较和交换,所以时间复杂度等于一个高为O(logn),底边长为O(n)的矩形的面积,即O(nlogn)。
(2)期望空间复杂度:O(logn)。递归需要期望O(logn)的栈开销。
5.总结:

附代码:
java
class Solution {
private static final Random rand = new Random();
public int[] sortArray(int[] nums) {
quickSort(nums,0,nums.length - 1);
return nums;
}
// 快速排序子数组[left,right]
private void quickSort(int[] nums,int left,int right){
// 优化:如果子数组已经升序,直接返回
boolean ordered = true;
for(int i = left;i < right;i++){
if(nums[i] > nums[i + 1]){
ordered = false;
break;
}
}
if(ordered){
return;
}
// 接收pivot最终的位置,满足pivot前面的元素都小于pivot,pivot后面的元素都大于pivot
int i = partition(nums,left,right);
quickSort(nums,left,i - 1); // 排序在 pivot 左侧的元素
quickSort(nums,i + 1,right); // 排序在 pivot 右侧的元素
}
// 在子数组[left,right]中随机选择一个基准元素pivot
// 根据pivot重新排列子数组[left,right]
// 重新排列后,<= pivot的元素都在pivot的左侧,>= pivot的元素都在pivot的右侧
// 返回pivot在重新排列后的nums中的下标
// 特别地,如果子数组的所有元素都等于pivot,我们会返回子数组的中心下标,避免退化
private int partition(int[] nums,int left,int right){
// 1.在子数组[left,right]中随机选择一个基准元素pivot
// rand.nextInt(int bound)表示生成一个范围在[0,bound)之间的随机整数
int i = left + rand.nextInt(right - left + 1);
int pivot = nums[i];
// 把pivot与子数组第一个元素交换,避免pivot干扰后续划分,从而简化实现逻辑
swap(nums,i,left);
// 2. 相向双指针遍历子数组 [left + 1, right]
// 循环不变量:在循环过程中,子数组的数据分布始终如下图
// [ pivot | <=pivot | 尚未遍历 | >=pivot ]
// ^ ^ ^ ^
// left i j right
i = left + 1;
int j = right;
// 每走一步都会:i++或者j--
// 数组长度有限,因此也不会无限循环下去
while(true){
while(i <= j && nums[i] < pivot){
i++;
}
// 此时nums[i] >= pivot
while(i <= j && nums[j] > pivot){
j--;
}
// 此时nums[j] <= pivot
if(i >= j){
break;
}
// 维持循环不变量
swap(nums,i,j);
i++;
j--;
}
// 循环结束后
// [ pivot | <=pivot | >=pivot ]
// ^ ^ ^ ^
// left j i right
// 3. 把 pivot 与 nums[j] 交换,完成划分(partition)
// 为什么与 j 交换?
// 如果与 i 交换,可能会出现 i = right + 1 的情况,已经下标越界了,无法交换
// 另一个原因是如果 nums[i] > pivot,交换会导致一个大于 pivot 的数出现在子数组最左边,不是有效划分
// 与 j 交换,即使 j = left,交换也不会出错
swap(nums,left,j);
// 交换后
// [ <=pivot | pivot | >=pivot ]
// ^
// j
// 返回 pivot 的下标
return j;
}
// 交换 nums[i] 与 nums[j],即索引位置对应的元素值
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
ACM模式:
java
import java.util.Random;
import java.util.Scanner;
class Solution {
private static final Random rand = new Random();
public int[] sortArray(int[] nums) {
quickSort(nums, 0, nums.length - 1);
return nums;
}
// 快速排序子数组[left,right]
private void quickSort(int[] nums, int left, int right) {
// 优化:如果子数组已经升序,直接返回
boolean ordered = true;
for (int i = left; i < right; i++) {
if (nums[i] > nums[i + 1]) {
ordered = false;
break;
}
}
if (ordered) {
return;
}
// 接收pivot最终的位置,满足pivot前面的元素都小于pivot,pivot后面的元素都大于pivot
int i = partition(nums, left, right);
quickSort(nums, left, i - 1);
quickSort(nums, i + 1, right);
}
// 在子数组[left,right]中随机选择一个基准元素pivot
// 根据pivot重新排列子数组[left,right]
// 重新排列后,<= pivot的元素都在pivot的左侧,>= pivot的元素都在pivot的右侧
// 返回pivot在重新排列后的nums中的下标
private int partition(int[] nums, int left, int right) {
// 1.在子数组[left,right]中随机选择一个基准元素pivot
int i = left + rand.nextInt(right - left + 1);
int pivot = nums[i];
// 把pivot与子数组第一个元素交换
swap(nums, i, left);
// 2. 相向双指针遍历子数组 [left + 1, right]
i = left + 1;
int j = right;
while (true) {
while (i <= j && nums[i] < pivot) {
i++;
}
while (i <= j && nums[j] > pivot) {
j--;
}
if (i >= j) {
break;
}
swap(nums, i, j);
i++;
j--;
}
// 3. 把 pivot 与 nums[j] 交换,完成划分
swap(nums, left, j);
return j;
}
// 交换 nums[i] 与 nums[j]
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 读取数组长度
int n = scanner.nextInt();
// 读取数组元素
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = scanner.nextInt();
}
// 排序数组
Solution solution = new Solution();
int[] result = solution.sortArray(nums);
// 输出排序后的数组
for (int i = 0; i < result.length; i++) {
System.out.print(result[i]);
if (i < result.length - 1) {
System.out.print(" ");
}
}
scanner.close();
}
}