【算法基础】快速排序算法 - JAVA
一、算法基础
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 基本实现
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):
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个元素),插入排序比快速排序更高效:
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 尾递归优化
通过将递归调用替换为迭代,可以避免栈溢出:
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 解法实现
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 应用到快速排序
使用三向切分可以优化快速排序,特别是处理有大量重复元素的数组:
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);
}
五、完整实现与示例
以下是一个包含各种优化的完整快速排序实现:
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²)
- 不稳定排序,无法保持相等元素的相对顺序
- 对于小数组,可能不如插入排序高效
适用场景
- 需要高效排序大数组时
- 内存受限、不能使用额外空间时
- 当平均性能比最坏情况性能更重要时
- 不要求排序稳定性时
快速排序是一种高效且实用的排序算法,在大多数情况下都表现出色。通过本文介绍的各种优化技巧,可以使快速排序在实际应用中获得最佳性能。掌握快速排序是每位程序员的基本功,也是理解分治算法思想的重要一步。