深入理解 Java 中的快速排序算法
一、引言
排序算法是计算机科学中的基础,它在众多领域有着广泛的应用,从数据处理到算法竞赛,一个高效的排序算法能大大提高程序的运行效率。快速排序(Quick Sort)作为一种高效的排序算法,以其平均时间复杂度为 以及原地排序(不需要额外大量辅助空间)的特性,成为众多编程语言中内置排序函数的实现基础,在 Java 的 Arrays.sort()
方法底层对于基本数据类型的排序中也有它的身影。本文将深入剖析如何用 Java 实现快速排序算法,带您领略其精妙之处。
二、快速排序的基本思想
快速排序采用了分治策略(Divide and Conquer)。它的基本思路是:选择一个基准值(pivot),通过一趟排序将待排序序列分割成两部分,使得左边部分的所有元素都小于等于基准值,右边部分的所有元素都大于等于基准值。然后分别对左右两部分递归地进行排序,直到整个序列有序。
形象地说,就好比在一群学生中,先选一个 “中等身高” 的学生作为基准,把比他矮的同学都排在左边,比他高的同学都排在右边,然后再分别在矮个子群体和高个子群体中重复这个 “选基准、分左右” 的过程,最终所有人就按身高排好了序。
三、Java 代码实现
public class QuickSort {
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
// 分区操作,获取基准值的最终位置
int pivotIndex = partition(arr, low, high);
// 对基准值左边的子数组进行递归排序
quickSort(arr, low, pivotIndex - 1);
// 对基准值右边的子数组进行递归排序
quickSort(arr, pivotIndex + 1, high);
}
}
private static int partition(int[] arr, int low, int high) {
// 选择最右边的元素作为基准值
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
// 交换 arr[i] 和 arr[j],使得小于等于 pivot 的元素都移到左边
swap(arr, i, j);
}
}
// 将基准值放到正确的位置(即 i + 1 处),此时左边都小于等于它,右边都大于等于它
swap(arr, i + 1, high);
return i + 1;
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = {5, 3, 8, 4, 2, 7, 1, 10, 6};
quickSort(arr, 0, arr.length - 1);
for (int num : arr) {
System.out.print(num + " ");
}
}
}
在上述代码中:
quickSort
方法是快速排序的入口,它首先检查子数组的长度是否大于 1,如果是,则进行分区操作,然后递归地对左右子数组排序。partition
方法负责具体的分区过程。它选择最右边的元素作为基准值,通过双指针法(i
和j
),将小于等于基准值的元素移到左边,大于等于基准值的元素留在右边,最后将基准值放到正确的分割位置,返回该位置索引。swap
方法简单地交换数组中两个指定位置的元素。
在 main
方法中,我们创建了一个测试数组,调用 quickSort
方法进行排序,并打印排序后的结果,这里将会输出 1 2 3 4 5 6 7 8 10
。
四、算法分析
(一)时间复杂度
- 最好情况:每次分区都能正好将数组分成两个等长的子数组,此时时间复杂度为 。这是因为一共需要 次分区操作,每次分区遍历 个元素,所以总的时间复杂度是 乘以 。
- 最坏情况:数组已经有序或者接近有序,每次选择的基准值都是最大或最小值,这样每次分区只会得到一个子数组,另一个为空,时间复杂度退化为 。例如数组
1, 2, 3, 4, 5
,若选 5 为基准,一趟排序后左边为空,右边是1, 2, 3, 4
,后续递归树严重不平衡。 - 平均情况:虽然存在最坏情况,但快速排序在随机化选择基准值的情况下,平均时间复杂度接近最好情况,为 ,这使得它在实践中表现极为出色。
(二)空间复杂度
快速排序是原地排序算法,它的空间复杂度主要取决于递归调用栈的深度。在最好和平均情况下,空间复杂度为 ,因为每次分区大致将数组一分为二,递归树的深度为 。但在最坏情况下,空间复杂度会退化为 ,因为递归需要 层栈空间。
(三)稳定性
快速排序是不稳定的排序算法。例如对于数组 [2, 2, 1]
,若选择第一个 2
作为基准,排序后两个 2
的相对顺序可能改变,变为 [1, 2, 2]
,所以它不保证相同元素的原始相对顺序不变。
五、优化策略
(一)随机化基准值
为了尽量避免最坏情况,我们可以在每次分区前,随机选择一个元素作为基准值,而不是固定选择最左边或最右边。这能大大降低出现退化情况的概率,使得算法在各种数据分布下都有较好的性能表现。修改 partition
方法如下:
private static int partition(int[] arr, int low, int high) {
// 随机选择一个索引作为基准值的索引
int randomIndex = new Random().nextInt(high - low + 1) + low;
swap(arr, randomIndex, high);
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
swap(arr, i, j);
}
}
swap(arr, i + 1, high);
return i + 1;
}
(二)三数取中
除了随机化,还可以采用三数取中策略。即从数组的左端、右端和中间位置选取三个数,取中间大小的值作为基准值。这能在一定程度上适应有序或部分有序的数据,进一步优化算法性能。代码示例如下:
private static int medianOfThree(int[] arr, int low, int high) {
int mid = low + (high - low) / 2;
if (arr[low] > arr[mid]) {
swap(arr, low, mid);
}
if (arr[low] > arr[high]) {
swap(arr, low, high);
}
if (arr[mid] > arr[high]) {
swap(arr, mid, high);
}
// 此时 arr[mid] 是三数中的中值,将它与最右边交换,后续 partition 逻辑不变
swap(arr, mid, high);
return high;
}
private static int partition(int[] arr, int low, int high) {
int pivotIndex = medianOfThree(arr, low, high);
int pivot = arr[pivotIndex];
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
swap(arr, i, j);
}
}
swap(arr, i + 1, pivotIndex);
return i + 1;
}
六、总结
快速排序以其简洁而高效的设计,成为排序领域的经典算法。通过对其原理、Java 实现、复杂度分析以及优化策略的学习,我们不仅掌握了一种强大的排序工具,更深入理解了分治思想在算法设计中的应用。在实际编程中,根据数据特点合理运用快速排序及其优化版本,能够显著提升程序处理数据的效率,助力我们解决各种复杂的编程挑战。希望本文能帮助您扎实掌握快速排序算法,在 Java 编程之路上更进一步。