Java中的快速排序是一种高效的排序算法,采用了分治法的策略。快速排序的基本思路是通过一个"基准"元素将数组分成两部分:左侧是比基准元素小的元素,右侧是比基准元素大的元素,然后递归地对这两部分进行排序。
以下是笔者提供的详细的快速排序算法实现及其几种不同的思路和方法。
1. 快速排序的基本原理
快速排序的核心思想如下:
- 选择一个基准元素:可以从数组中选择任意一个元素作为基准元素,通常选择数组的第一个元素、最后一个元素、或者随机选择。
- 分区:通过一轮扫描将数组分为两部分,左侧部分的元素都比基准元素小,右侧部分的元素都比基准元素大。
- 递归排序:对基准元素左侧和右侧的部分分别进行递归排序。
2. 基本实现
下面是最简单的快速排序实现,采用了分治法的递归方式:
public class QuickSort {
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
// 找到分区点
int pi = partition(arr, low, high);
// 对分区的左右部分进行递归排序
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
// 分区操作,返回基准元素的索引
public static int partition(int[] arr, int low, int high) {
int pivot = arr[high]; // 选择最后一个元素作为基准
int i = low - 1; // i指向小于基准的元素的最后一个位置
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
swap(arr, i, j);
}
}
swap(arr, i + 1, high); // 将基准元素放到正确的位置
return i + 1; // 返回基准元素的索引
}
// 交换数组中的两个元素
public 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 = {10, 80, 30, 90, 40, 50, 70};
quickSort(arr, 0, arr.length - 1);
System.out.println("Sorted array: " + Arrays.toString(arr));
}
}
3. 快速排序的时间复杂度
- 平均时间复杂度:O(n log n),当数组大致均匀地分割成两部分时,快速排序的时间复杂度为O(n log n)。
- 最坏时间复杂度:O(n²),当数组已经是有序的或逆序排列时,每次分区操作会退化成一条链式结构,导致递归深度达到n。
- 最佳时间复杂度:O(n log n),当每次分区操作都能将数组均匀分割时,递归深度是log n。
为了避免最坏情况,可以通过随机化基准元素的选择来优化,或者使用“三数取中”策略。
4. 优化:随机基准
为了减少最坏情况的发生,可以随机选择基准元素,避免每次都选取数组的最左边或最右边作为基准。这样可以有效避免在有序或逆序数组中的最坏情况。
import java.util.Random;
public class RandomizedQuickSort {
private static Random rand = new Random();
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pi = randomizedPartition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
private static int randomizedPartition(int[] arr, int low, int high) {
// 随机选择基准元素
int pivotIndex = low + rand.nextInt(high - low + 1);
swap(arr, pivotIndex, high);
return partition(arr, low, high);
}
// 分区操作
public 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++;
swap(arr, i, j);
}
}
swap(arr, i + 1, high);
return i + 1;
}
// 交换操作
public 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 = {10, 80, 30, 90, 40, 50, 70};
quickSort(arr, 0, arr.length - 1);
System.out.println("Sorted array: " + Arrays.toString(arr));
}
}
5. 优化:三数取中法
三数取中法(Median of Three)是为了避免在已排序的数组上退化为O(n²)的最坏情况。在这种方法中,基准元素不再选择数组的第一个、最后一个或随机元素,而是选择数组的第一个元素、最后一个元素和中间元素中位数作为基准元素。
public class MedianOfThreeQuickSort {
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pi = medianOfThreePartition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
public static int medianOfThreePartition(int[] arr, int low, int high) {
// 选择第一个、最后一个和中间的元素
int mid = low + (high - low) / 2;
int[] candidates = {arr[low], arr[mid], arr[high]};
int pivot = median(candidates);
// 将基准元素交换到最后一个位置
if (pivot == arr[low]) {
swap(arr, low, high);
} else if (pivot == arr[mid]) {
swap(arr, mid, high);
}
return partition(arr, low, high);
}
// 找到三数中的中位数
public static int median(int[] candidates) {
int a = candidates[0], b = candidates[1], c = candidates[2];
if ((a > b) != (a > c)) {
return a;
} else if ((b > a) != (b > c)) {
return b;
} else {
return c;
}
}
// 分区操作
public 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++;
swap(arr, i, j);
}
}
swap(arr, i + 1, high);
return i + 1;
}
// 交换操作
public 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 = {10, 80, 30, 90, 40, 50, 70};
quickSort(arr, 0, arr.length - 1);
System.out.println("Sorted array: " + Arrays.toString(arr));
}
}
6. 非递归实现(栈模拟)
在快速排序的递归实现中,每次递归调用会使用栈存储左右子数组的索引。可以改用显式的栈来模拟递归,从而避免递归调用带来的栈溢出问题。
import java.util.Stack;
public class IterativeQuickSort {
public static void quickSort(int[] arr, int low, int high) {
Stack<int[]> stack = new Stack<>();
stack.push(new int[]{low, high});
while (!stack.isEmpty()) {
int[] range = stack.pop();
low = range[0];
high = range[1];
if (low < high) {
int pi = partition(arr, low, high);
stack.push(new int[]{low, pi - 1});
stack.push(new int[]{pi + 1, high});
}
}
}
public 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++;
swap(arr, i, j);
}
}
swap(arr, i + 1, high); // 将基准元素放到正确的位置
return i + 1; // 返回基准元素的索引
}
// 交换操作
public 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 = {10, 80, 30, 90, 40, 50, 70};
quickSort(arr, 0, arr.length - 1);
System.out.println("Sorted array: " + Arrays.toString(arr));
}
}
总结
以上代码展示了如何实现快速排序的非递归版本。在这种实现中,使用栈来模拟递归的过程,避免了递归深度过大导致栈溢出的情况。通过不断将子数组的索引压入栈中,并执行分区操作,直到数组完全排序。这个实现与传统的递归版本具有相同的时间复杂度,但可以在一些极端情况下避免递归调用过多导致的性能问题。