快速排序算法
一、概述
快速排序(Quicksort)是一种常用的高效排序算法,采用分治法的思想。其核心思想是通过选择一个基准元素,将数组划分为两部分,使得基准左边的元素都小于基准,右边的元素都大于基准,然后递归地对左右部分继续进行排序。
特点
- 时间复杂度:
- 平均时间复杂度:O(n log n)
- 最坏时间复杂度:O(n²)(当每次选的基准值是最大或最小时)
- 空间复杂度:O(log n) (递归栈的空间开销)
- 不稳定:快速排序不是一种稳定的排序算法。
二、算法步骤
- 选择基准元素。
- 通过一趟排序将待排数组分为两部分,一部分比基准小,另一部分比基准大。
- 递归地对两部分进行快速排序。
三、代码模板(Java版)
import java.util.Scanner;
public class QuickSort {
private static final int N = 1000000 + 10;
private static int[] q = new int[N];
private static int n;
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
for (int i = 0; i < n; i++) {
q[i] = scanner.nextInt();
}
quickSort(q, 0, n - 1);
for (int i = 0; i < n; i++) {
System.out.print(q[i] + " ");
}
scanner.close();
}
public static void quickSort(int[] arr, int l, int r) {
if (l < r) {
int i = l - 1;
int j = r + 1;
int x = arr[l];
while (i < j) {
do {
i++;
} while (i < r && arr[i] < x);
do {
j--;
} while (arr[j] > x);
if (i < j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
quickSort(arr, l, j);
quickSort(arr, j + 1, r);
}
}
}
四、代码讲解
- quickSort 方法:这是快速排序的主方法,递归地对数组进行分区和排序。
- partition 方法:分区操作,选择一个基准元素,并将数组划分为两部分。
- swap 方法:简单的元素交换操作,用于在分区过程中交换数组中的元素。
注意事项
- 递归终止条件:当
low >= high
时,递归结束。 - 基准选择:这里选择了数组的最后一个元素作为基准,也可以根据不同情况优化基准的选择,如随机选择基准或使用三数取中法。
五、补充
在某些快速排序的实现中,int i = l - 1; int j = r + 1;
这种初始化方式出现在特定的双指针(或左右指针)分区法中。我们来看一下它背后的逻辑和原理。
1、快速排序的分区操作
在快速排序中,分区操作的目的是将数组分成两部分:
- 一部分的元素小于基准值(pivot),
- 另一部分的元素大于基准值。
而 双指针分区法 是通过从数组的两端同时向中间移动指针来实现这个分区过程。
2、i = l - 1
和 j = r + 1
的作用
-
i = l - 1;
:- 这个指针
i
最初指向左边界l
的前一个位置。这样做的原因是,当我们开始移动指针时,i
会先增加(i++
),所以最初将其设置为l - 1
,可以确保一开始从左边界的第一个元素(即arr[l]
)开始比较。 - 如果直接初始化为
l
,那么第一次增量操作时会跳过arr[l]
,导致错误的比较顺序。
- 这个指针
-
j = r + 1;
:- 这个指针
j
最初指向右边界r
的后一个位置。因为我们在分区过程中,指针j
会向左移动(j--
),将其初始设置为r + 1
确保可以从右边界的第一个元素(即arr[r]
)开始比较。 - 如果直接初始化为
r
,第一次减量操作时会跳过arr[r]
,导致同样的逻辑错误。
- 这个指针
3、分区过程的具体操作
使用双指针的分区方法时,整个过程如下:
- 从数组的两端开始,
i
从左往右扫描,j
从右往左扫描。 - 当
i
找到一个大于等于基准值的元素时,它停止向右移动。 - 当
j
找到一个小于等于基准值的元素时,它停止向左移动。 - 这时,
i
和j
的元素满足互换条件,将它们交换位置。 - 继续上述步骤,直到
i
和j
相遇或交错,这时分区结束。
4、代码示例
这种逻辑可以用以下代码片段展示:
public class QuickSort {
public static void quickSort(int[] arr, int l, int r) {
if (l >= r) return;
int pivot = arr[(l + r) / 2]; // 选取中间的元素作为基准
int i = l - 1, j = r + 1;
while (i < j) {
// 从左到右找到一个大于或等于 pivot 的元素
do i++; while (arr[i] < pivot);
// 从右到左找到一个小于或等于 pivot 的元素
do j--; while (arr[j] > pivot);
// 如果 i 和 j 还没有相遇,交换这两个元素
if (i < j) swap(arr, i, j);
}
// 递归地对左边和右边进行排序
quickSort(arr, l, j);
quickSort(arr, j + 1, r);
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
5、总结
int i = l - 1;
和 int j = r + 1;
这种初始化方式的主要目的是为了确保在分区过程中,指针从左右边界的元素开始遍历时能够正常工作。在第一次移动指针之前,它们不会跳过边界上的元素