一、算法基础
1.1 什么是快速排序
快速排序(Quick Sort)是一种高效的分治排序算法,由英国计算机科学家Tony Hoare于1960年提出。它的核心思想是:
- 选择一个基准元素(pivot)
- 将数组分成两部分:小于基准的元素和大于基准的元素
- 递归地对这两部分进行排序
快速排序是实际应用中最常用的排序算法之一,平均情况下时间复杂度为O(n log n) ,空间复杂度为O(log n)。
1.2 快速排序的基本思想
- 选择基准:从数组中选择一个元素作为基准(通常是第一个元素、最后一个元素或中间元素)
- 分区操作:将数组中小于基准的元素放在基准的左边,大于基准的元素放在右边
- 递归排序:对基准左右两部分分别进行递归排序
这个过程被称为分区(Partition),是快速排序的核心操作。
1.3 时间复杂度分析
- 最佳情况:O(n log n) ------ 每次分区操作都将数组均匀地分成两部分
- 平均情况:O(n log n) ------ 大多数情况下的性能表现
- 最差情况:O(n²) ------ 当数组已经有序或几乎有序时,选择第一个或最后一个元素作为基准
二、快速排序的实现
2.1 基本实现
java
public class QuickSort {
public static void sort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
quickSort(arr, 0, arr.length - 1);
}
private static void quickSort(int[] arr, int left, int right) {
if (left < right) {
// 获取分区点
int pivotIndex = partition(arr, left, right);
// 递归排序左半部分
quickSort(arr, left, pivotIndex - 1);
// 递归排序右半部分
quickSort(arr, pivotIndex + 1, right);
}
}
private static int partition(int[] arr, int left, int right) {
// 选择最右边的元素作为基准
int pivot = arr[right];
// i是小于基准区域的边界
int i = left - 1;
// 遍历区间,将小于基准的元素放到左边
for (int j = left; j < right; j++) {
if (arr[j] <= pivot) {
i++;
// 交换元素
swap(arr, i, j);
}
}
// 将基准元素放到正确的位置
swap(arr, i + 1, right);
// 返回基准元素的索引
return i + 1;
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
2.2 递归过程分析
假设有数组:[8, 4, 2, 9, 5, 7, 6, 1, 3]
-
第一次分区:
- 选择基准值 3(最后一个元素)
- 分区后:[1, 2, 3, 9, 5, 7, 6, 8, 4]
- 基准索引:2
-
左子数组递归:[1, 2]
- 选择基准值 2
- 分区后:[1, 2]
- 基准索引:1
-
右子数组递归:[9, 5, 7, 6, 8, 4]
- 选择基准值 4
- 分区后:[4, 5, 7, 6, 8, 9]
- 基准索引:0
-
继续递归...
最终得到排序结果:[1, 2, 3, 4, 5, 6, 7, 8, 9]
三、快速排序的优化
3.1 基准选择优化
选择好的基准可以显著提高快速排序的性能。最常用的优化方法是三数取中(Median-of-Three):
java
private static int selectPivot(int[] arr, int left, int right) {
int mid = left + (right - left) / 2;
// 将三个元素排序
if (arr[left] > arr[mid]) {
swap(arr, left, mid);
}
if (arr[left] > arr[right]) {
swap(arr, left, right);
}
if (arr[mid] > arr[right]) {
swap(arr, mid, right);
}
// 将中间值(基准)交换到right-1位置
swap(arr, mid, right - 1);
return arr[right - 1];
}
3.2 小数组使用插入排序
对于小规模数组(通常小于10个元素),插入排序比快速排序更高效:
java
private static void quickSort(int[] arr, int left, int right) {
// 小数组使用插入排序
if (right - left <= 10) {
insertionSort(arr, left, right);
return;
}
// 正常的快速排序过程
if (left < right) {
int pivotIndex = partition(arr, left, right);
quickSort(arr, left, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, right);
}
}
private static void insertionSort(int[] arr, int left, int right) {
for (int i = left + 1; i <= right; i++) {
int key = arr[i];
int j = i - 1;
while (j >= left && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
3.3 尾递归优化
通过将递归调用替换为迭代,可以避免栈溢出:
java
private static void quickSort(int[] arr, int left, int right) {
while (left < right) {
int pivotIndex = partition(arr, left, right);
// 递归处理较小的子数组,迭代处理较大的子数组
if (pivotIndex - left < right - pivotIndex) {
quickSort(arr, left, pivotIndex - 1);
left = pivotIndex + 1;
} else {
quickSort(arr, pivotIndex + 1, right);
right = pivotIndex - 1;
}
}
}
四、典型应用:荷兰国旗问题
荷兰国旗问题是快速排序的一个典型应用,它要求将数组分成三个部分:小于、等于、大于某个值的元素。这种分区方法也称为三向切分。
4.1 问题描述
给定一个数组和一个基准值,将数组重新排列,使得所有小于基准的元素在左边,等于基准的元素在中间,大于基准的元素在右边。
4.2 解法实现
java
public static void dutchFlagPartition(int[] arr, int pivot) {
int left = 0; // 小于pivot的区域右边界
int current = 0; // 当前处理的元素
int right = arr.length - 1; // 大于pivot的区域左边界
while (current <= right) {
if (arr[current] < pivot) {
// 小于pivot的元素放左边
swap(arr, left, current);
left++;
current++;
} else if (arr[current] > pivot) {
// 大于pivot的元素放右边
swap(arr, current, right);
right--;
// 注意:此时不增加current,因为交换来的元素还未处理
} else {
// 等于pivot的元素保持原位
current++;
}
}
}
4.3 应用到快速排序
使用三向切分可以优化快速排序,特别是处理有大量重复元素的数组:
java
private static void quickSort3Way(int[] arr, int left, int right) {
if (left >= right) {
return;
}
// 记录小于、等于、大于pivot的三个区域边界
int lt = left; // 小于pivot的区域右边界
int gt = right; // 大于pivot的区域左边界
int i = left + 1; // 当前处理的元素
int pivot = arr[left];
while (i <= gt) {
if (arr[i] < pivot) {
swap(arr, lt++, i++);
} else if (arr[i] > pivot) {
swap(arr, i, gt--);
} else {
i++;
}
}
// 递归处理小于和大于的部分
quickSort3Way(arr, left, lt - 1);
quickSort3Way(arr, gt + 1, right);
}
五、完整实现与示例
以下是一个包含各种优化的完整快速排序实现:
java
public class OptimizedQuickSort {
private static final int INSERTION_SORT_THRESHOLD = 10;
public static void sort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
quickSort(arr, 0, arr.length - 1);
}
private static void quickSort(int[] arr, int left, int right) {
// 小数组使用插入排序
if (right - left <= INSERTION_SORT_THRESHOLD) {
insertionSort(arr, left, right);
return;
}
// 使用三数取中法选择基准
medianOfThree(arr, left, right);
int pivot = arr[right - 1];
// 分区
int i = left;
int j = right - 1;
for (;;) {
while (arr[++i] < pivot) {}
while (arr[--j] > pivot) {}
if (i >= j) break;
swap(arr, i, j);
}
// 将基准放回正确位置
swap(arr, i, right - 1);
// 递归排序
quickSort(arr, left, i - 1);
quickSort(arr, i + 1, right);
}
private static void medianOfThree(int[] arr, int left, int right) {
int mid = left + (right - left) / 2;
if (arr[left] > arr[mid]) {
swap(arr, left, mid);
}
if (arr[left] > arr[right]) {
swap(arr, left, right);
}
if (arr[mid] > arr[right]) {
swap(arr, mid, right);
}
// 将基准(中间值)放到right-1位置
swap(arr, mid, right - 1);
}
private static void insertionSort(int[] arr, int left, int right) {
for (int i = left + 1; i <= right; i++) {
int key = arr[i];
int j = i - 1;
while (j >= left && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
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 = {8, 4, 2, 9, 5, 7, 6, 1, 3};
System.out.println("原始数组: " + Arrays.toString(arr));
sort(arr);
System.out.println("排序后数组: " + Arrays.toString(arr));
}
}
六、总结
核心要点
- 分治思想:快速排序采用分治策略,通过分区操作将问题分解为子问题
- 基准选择:基准元素的选择直接影响算法效率,好的选择可以避免最坏情况
- 就地排序:快速排序是一种原地排序算法,不需要额外的数组空间
- 不稳定性:快速排序不是稳定排序算法,相同元素的相对顺序可能改变
优缺点
优点:
- 平均情况下,性能优于其他 O(n log n) 排序算法
- 不需要额外的存储空间(原地排序)
- 对缓存友好,局部性好
缺点:
- 最坏情况下,时间复杂度为 O(n²)
- 不稳定排序,无法保持相等元素的相对顺序
- 对于小数组,可能不如插入排序高效
适用场景
- 需要高效排序大数组时
- 内存受限、不能使用额外空间时
- 当平均性能比最坏情况性能更重要时
- 不要求排序稳定性时
快速排序是一种高效且实用的排序算法,在大多数情况下都表现出色。通过本文介绍的各种优化技巧,可以使快速排序在实际应用中获得最佳性能。掌握快速排序是每位程序员的基本功,也是理解分治算法思想的重要一步。