文章目录
- 快速排序的多种实现方式
-
- [1. 基本快速排序(Lomuto 分区方案)](#1. 基本快速排序(Lomuto 分区方案))
-
- [1.1 基本原理](#1.1 基本原理)
- [1.2 步骤](#1.2 步骤)
- [1.3 Java 实现示例](#1.3 Java 实现示例)
- [2. Hoare 分区方案](#2. Hoare 分区方案)
-
- [2.1 基本原理](#2.1 基本原理)
- [2.2 步骤](#2.2 步骤)
- [2.3 Java 实现示例](#2.3 Java 实现示例)
- [3. 三数取中法](#3. 三数取中法)
-
- [3.1 基本原理](#3.1 基本原理)
- [3.2 步骤](#3.2 步骤)
- [3.3 Java 实现示例](#3.3 Java 实现示例)
- [4. 尾递归优化](#4. 尾递归优化)
-
- [4.1 基本原理](#4.1 基本原理)
- [4.2 步骤](#4.2 步骤)
- [4.3 Java 实现示例](#4.3 Java 实现示例)
- 总结
快速排序的多种实现方式
快速排序(Quick Sort)是一种高效的排序算法,采用分治法策略。它通过选择一个"基准"元素,将数组分割成两个子数组,并递归地对这两个子数组进行排序。本文将详细介绍几种常见的快速排序实现方式,并讨论它们的特点和适用场景。
1. 基本快速排序(Lomuto 分区方案)
1.1 基本原理
Lomuto 分区方案是最常见的快速排序实现之一。它选择数组的最后一个元素作为基准(pivot),然后重新排列数组,使得所有小于基准的元素位于基准的左侧,所有大于基准的元素位于基准的右侧。
1.2 步骤
- 选择基准:通常选择数组的最后一个元素作为基准。
- 初始化指针 :设置一个指针
i
,用于追踪当前小于基准的最后一个元素的位置。 - 遍历数组 :
- 遍历数组中的每个元素(除了基准元素),如果当前元素小于等于基准,则将该元素与
i
指针所指向的元素交换,并将i
向右移动一位。
- 遍历数组中的每个元素(除了基准元素),如果当前元素小于等于基准,则将该元素与
- 放置基准 :最后将基准元素与
i + 1
位置的元素交换,使得基准元素处于正确的位置。 - 递归排序:对基准两侧的子数组分别递归执行上述过程,直到每个子数组只剩下一个元素或为空。
1.3 Java 实现示例
java
public static void quickSort(int[] arr) {
quickSort(arr, 0, arr.length - 1);
}
private static void quickSort(int[] arr, int low, int high) {
if(low >= high) {
return;
}
int pivotIndex = lomuto(arr, low, high);
quickSort(arr, low, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, high);
}
private static int lomuto(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 ++;
if(i != j) {
swap(arr, i, j);
System.out.println(low + "|" + high + " " + i + "|" + j + " " + Arrays.toString(arr));
}
}
}
// 将基准元素放回数组正确位置
swap(arr, i + 1, high);
System.out.println(low + "|" + high + "\t \t" + Arrays.toString(arr));
return i + 1;
}
/**
* 交换数组索引为i和j的两个元素
* @param arr
* @param i
* @param j
*/
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
2. Hoare 分区方案
2.1 基本原理
Hoare 分区方案由 C.A.R. Hoare 提出,是另一种常见的快速排序实现。它通过选择一个基准元素(通常是第一个或最后一个元素),然后重新排列数组,使得所有小于基准的元素位于基准的左侧,所有大于基准的元素位于基准的右侧。
2.2 步骤
- 选择基准:通常选择数组的第一个元素作为基准。
- 初始化双指针:设置两个指针,分别指向数组的起始位置和结束位置。
- 移动指针:
- 左指针向右移动,直到找到一个大于基准的元素。
- 右指针向左移动,直到找到一个小于基准的元素。
- 交换元素:当左右指针都停止时,交换这两个元素的位置。
- 重复步骤3和4:继续移动指针并交换元素,直到左右指针相遇。
- 放置基准:最后将基准元素与右指针的位置交换,使得基准元素处于正确的位置。
2.3 Java 实现示例
java
public static void quickSort(int[] arr) {
quickSort(arr, 0, arr.length - 1);
}
private static void quickSort(int[] arr, int low, int high) {
if(low >= high) {
return;
}
int pivotIndex = hoare(arr, low, high);
quickSort(arr, low, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, high);
}
private static int hoare(int[] arr, int low, int high) {
int pivot = arr[low]; // 选择一个元素作为基准
int l = low; // 左索引
int r = high; // 右索引
while(l < r) {
while(l < r && arr[r] > pivot) {
r --;
}
while(l < r && arr[l] < pivot) {
l ++;
}
if(l < r) {
swap(arr, l, r);
System.out.println(l + "|" + r + "\t\t" + Arrays.toString(arr));
}
}
return l;
}
/**
* 交换数组索引为i和j的两个元素
* @param arr
* @param i
* @param j
*/
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
3. 三数取中法
3.1 基本原理
为了减少快速排序在最坏情况下的时间复杂度(即 O(n^2)),可以使用"三数取中法"来选择基准元素。这种方法通过选择数组的第一个、中间和最后一个元素中的中位数作为基准,从而减少最坏情况的发生概率。
3.2 步骤
- 选择基准:选择数组的第一个、中间和最后一个元素中的中位数作为基准。
- 分区操作:根据选择的基准进行分区操作,可以使用 Lomuto 或 Hoare 分区方案。
- 递归排序:对基准两侧的子数组分别递归执行上述过程,直到每个子数组只剩下一个元素或为空。
3.3 Java 实现示例
java
public static void quickSort(int[] arr) {
quickSort(arr, 0, arr.length - 1);
}
private static void quickSort(int[] arr, int low, int high) {
if (low >= high) {
return;
}
int pivotIndex = lomuto(arr, low, high);
quickSort(arr, low, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, high);
}
private static int lomuto(int[] arr, int low, int high) {
int pivotIndex = medianOfThree(arr, low, high); // 使用三数取中法选择基准
int pivot = arr[pivotIndex];
int i = low - 1; // 指向当前小于基准的最后一个元素
if (pivotIndex != high) {
swap(arr, pivotIndex, high); // 将基准元素移到最后
System.out.println("pivot:" + pivotIndex + "|" + high + "\t" + Arrays.toString(arr));
}
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
if (i != j) {
swap(arr, i, j);
System.out.println(low + "|" + high + " \t" + i + "|" + j + "\t" + Arrays.toString(arr));
}
}
}
// 将基准元素放回数组正确位置
if (i + 1 != high) {
swap(arr, i + 1, high);
System.out.println("back:" + (i + 1) + "|" + high + "\t" + Arrays.toString(arr));
}
return i + 1;
}
/**
* 选择数组的第一个、中间和最后一个元素中的中位数作为基准,返回其下标
*
* @param arr
* @param low
* @param high
* @return
*/
private static int medianOfThree(int[] arr, int low, int high) {
int mid = (low + high) / 2;
if ((arr[low] <= arr[mid] && arr[mid] <= arr[high]) || (arr[low] >= arr[mid] && arr[mid] >= arr[high])) {
return mid;
}
if ((arr[mid] <= arr[low] && arr[low] <= arr[high]) || (arr[mid] >= arr[low] && arr[low] >= arr[high])) {
return low;
}
return high;
}
/**
* 交换数组索引为i和j的两个元素
*
* @param arr
* @param i
* @param j
*/
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
4. 尾递归优化
4.1 基本原理
快速排序的递归实现可能导致栈溢出问题,特别是在处理大规模数据集时。为了避免这种情况,可以使用尾递归优化技术来减少递归调用栈的深度。
4.2 步骤
- 选择基准:选择数组的某个元素作为基准。
- 分区操作:根据选择的基准进行分区操作,可以使用 Lomuto 或 Hoare 分区方案。
- 尾递归优化:每次递归只对较小的子数组进行递归调用,较大的子数组则通过循环继续处理,从而减少递归调用栈的深度。
4.3 Java 实现示例
java
public static void quickSort(int[] arr) {
quickSort(arr, 0, arr.length - 1);
}
private static void quickSort(int[] arr, int low, int high) {
// 使用循环代替递归
while (low < high) {
int pivotIndex = hoare(arr, low, high);
// 对较小的分区进行递归调用
if (pivotIndex - low < high - pivotIndex) {
quickSort(arr, low, pivotIndex - 1);
low = pivotIndex + 1;
} else {
quickSort(arr, pivotIndex + 1, high);
high = pivotIndex - 1;
}
}
}
private static int hoare(int[] arr, int low, int high) {
int pivot = arr[low]; // 选择一个元素作为基准
int l = low; // 左索引
int r = high; // 右索引
while (l < r) {
while (l < r && arr[r] > pivot) {
r--;
}
while (l < r && arr[l] < pivot) {
l++;
}
if (l < r) {
swap(arr, l, r);
System.out.println(l + "|" + r + "\t\t" + Arrays.toString(arr));
}
}
return l;
}
/**
* 交换数组索引为i和j的两个元素
*
* @param arr
* @param i
* @param j
*/
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
总结
快速排序有多种实现方式,每种实现方式在不同的应用场景下可能有不同的性能表现:
- Lomuto 分区方案:简单易懂,但相比 Hoare 分区方案,在某些情况下可能会有更多的交换操作,导致效率稍低。
- Hoare 分区方案:通常比 Lomuto 分区更高效,因为它减少了不必要的交换操作,从而减少了时间复杂度。
- 三数取中法:通过选择数组的第一个、中间和最后一个元素中的中位数作为基准,减少最坏情况的发生概率。
- 尾递归优化:通过优化递归调用栈的深度,避免栈溢出问题,特别适合处理大规模数据集。