快速排序QuickSort
快速排序
快速排序算法有两个核心点,分别为 哨兵划分
和 递归
。
哨兵划分
:
以数组某个元素(一般选取首元素)为 基准数 ,将所有小于基准数的元素移动至其左边,大于基准数的元素移动至其右边。
如下图所示,为哨兵划分操作流程。通过一轮 哨兵划分 ,可将数组排序问题拆分为 两个较短数组的排序问题 (本文称之为左(右)子数组)。
递归
:
对 左子数组 和 右子数组 分别递归执行 哨兵划分,直至子数组长度为 1 时终止递归,即可完成对整个数组的排序。
如下图所示,为示例数组 [2,4,1,0,3,5] 的快速排序流程。观察发现,快速排序和 二分法 的原理类似,都是以
log
时间复杂度实现搜索区间缩小。
java
void quickSort(int[] nums, int l, int r) {
// 子数组长度为 1 时终止递归
if (l >= r) return;
// 哨兵划分操作
int i = partition(nums, l, r);
// 递归左(右)子数组执行哨兵划分
quickSort(nums, l, i - 1);
quickSort(nums, i + 1, r);
}
int partition(int[] nums, int l, int r) {
// 以 nums[l] 作为基准数
int i = l, j = r;
while (i < j) {
while (i < j && nums[j] >= nums[l]) j--;
while (i < j && nums[i] <= nums[l]) i++;
swap(nums, i, j);
}
swap(nums, i, l);
return i;
}
void swap(int[] nums, int i, int j) {
// 交换 nums[i] 和 nums[j]
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
// 调用
int[] nums = { 4, 1, 3, 2, 5 };
quickSort(nums, 0, nums.length - 1);
优化
java
package 快速排序;
/**
* Created with IntelliJ IDEA.
*
* @Author: AlenXu
* @Date: 2024/03/19/13:26
* @Description:
*/
public class QuickSort {
static void quickSort1(int[] nums, int l, int r) {
// 子数组长度为 1 时终止递归
if (l >= r) return;
//哨兵划分操作
int i = partition(nums, l, r);
// 递归左(右)子数组执行哨兵划分
quickSort(nums, l, i - 1);
quickSort(nums, i + 1, r);
}
// 由于普通快速排序每轮选取「子数组最左元素」作为「基准数」,因此在输入数组
// 完全倒序 时, partition() 的递归深度会达到N ,即 最差空间复杂度 为 O(N) 。
// 每轮递归时,仅对 较短的子数组 执行哨兵划分 partition() ,
// 就可将最差的递归深度控制在O(logN) (每轮递归的子数组长度都 ≤ 当前数组长度),
// 即实现最差空间复杂度O(logN) 。
/**
* Tail Call 优化
* @param nums
* @param l
* @param r
*/
static void quickSort(int[] nums, int l, int r) {
// 子数组长度为 1 时终止递归
while (l < r) {
// 哨兵划分操作
int i = partition(nums, l, r);
// 仅递归至较短子数组,控制递归深度
if (i - l < r - i) {
quickSort(nums, l, i - 1);
l = i + 1;
} else {
quickSort(nums, i + 1, r);
r = i - 1;
}
}
}
static private int partition1(int[] nums, int l, int r) {
以 nums[l] 作为基准数
int i = l, j = r;
while (i < j) {
while (i < j && nums[j] >= nums[l]) j--; //从右向左 找到小于基数的元素
while (i < j && nums[i] <= nums[l]) i++; //从左向右 找到大于基数的元素
//交换 i 和 j 未知的元素
swap(nums, i, j);
}
//交换基数的位置
swap(nums, i, l);
return i;
}
/**
* 随机基准数 优化
* @param nums
* @param l
* @param r
* @return
*/
// 同样地,由于快速排序每轮选取「子数组最左元素」作为「基准数」,
// 因此在输入数组 完全有序 或 完全倒序 时, partition() 每轮只划分一个元素,
// 达到最差时间复杂度O(N²) 。
// 可使用 随机函数 ,每轮在子数组中随机选择一个元素作为基准数,
// 这样就可以极大概率避免以上劣化情况
static int partition(int[] nums, int l, int r) {
// 在闭区间 [l, r] 随机选取任意索引,并与 nums[l] 交换
int ra = (int) (l + Math.random() * (r - l + 1));
swap(nums, l, ra);
// 以 nums[l] 作为基准数
int i = l, j = r;
while (i < j) {
while (i < j && nums[j] >= nums[l]) j--;
while (i < j && nums[i] <= nums[l]) i++;
swap(nums, i, j);
}
swap(nums, i, l);
return i;
}
static private void swap(int[] nums, int i, int j) {
//交换
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
public static void main(String[] args) {
// 调用
int[] nums = {4, 1, 3, 2, 5};
quickSort(nums, 0, nums.length - 1);
for (int num : nums) {
System.out.print(num + " ");
}
}
}