一、算法概述
快速排序是一种不稳定的原地排序算法,其核心特点是"分治+分区":通过选择一个"基准元素"(pivot),将待排序序列分割成两个子序列,其中左子序列的所有元素均小于等于基准元素,右子序列的所有元素均大于等于基准元素(具体分区规则可灵活调整),然后分别对这两个子序列递归执行快速排序,直到所有子序列长度为1(此时子序列已天然有序),排序完成。
二、核心原理与分区逻辑
快速排序的整个执行流程可分为"分区"和"递归排序"两个核心步骤,其中"分区"是快速排序的灵魂,直接决定了算法的效率;"递归排序"则是对分区后的子序列重复执行相同的逻辑,直至排序完成。
1. 核心思想(分而治之)
快速排序的分而治之思想可拆解为以下三个步骤,形成一个递归闭环:
分解:选择一个基准元素,将待排序序列 arr[low...high] 分区为左子序列 arr[low...pivotIndex−1] 、基准元素 arr[pivotIndex] 和右子序列 arr[pivotIndex+1...high] ,满足左子序列所有元素≤基准元素,右子序列所有元素≥基准元素;
解决:递归调用快速排序,分别对左子序列 arr[low...pivotIndex−1] 和右子序列arr[pivotIndex+1...high] 进行排序;
合并:由于左、右子序列的排序是在原数组上进行的,且基准元素已处于最终的正确位置,因此无需额外的合并操作,递归结束后整个序列即有序。
2. 分区操作(Partition)
分区操作是快速排序的核心,其目的是找到基准元素的最终位置,并将序列分割为符合要求的左右两个子序列。常用的分区方式有"霍尔分区法"(Hoare Partition Scheme)和" Lomuto 分区法"(Lomuto Partition Scheme),其中霍尔分区法是快速排序的原始分区方式,效率更高; Lomuto 分区法逻辑更简单,易于实现,但效率略低。本文重点讲解霍尔分区法,并简要介绍 Lomuto 分区法。
(1)霍尔分区法
霍尔分区法的核心逻辑的是:选择一个基准元素(通常选择序列的第一个元素、最后一个元素或中间元素),设置两个指针(left和right)分别指向序列的起始位置和终止位置,然后通过指针移动,将小于基准元素的元素移到左侧,大于基准元素的元素移到右侧,最终确定基准元素的位置。具体步骤如下:
-
选择基准元素pivot,本文暂选序列的第一个元素(arr[low]);
-
初始化指针left = low + 1(指向基准元素的下一个位置),right = high(指向序列的最后一个位置);
-
移动left指针,直到找到第一个大于pivot的元素(arr[left] > pivot),停止移动;
-
移动right指针,直到找到第一个小于pivot的元素(arr[right] < pivot),停止移动;
-
若left < right,交换arr[left]和arr[right],然后继续执行步骤3和步骤4;
-
若left ≥ right,交换arr[low](基准元素)和arr[right],此时基准元素pivot的位置即为right,分区完成。
需要注意的是,霍尔分区法中,基准元素最终会与right指针指向的元素交换,因为right指针停止时,指向的必然是小于基准元素的元素,交换后可保证基准元素左侧均为小于它的元素,右侧均为大于它的元素。
(2)Lomuto 分区法
Lomuto 分区法的逻辑更简洁,核心是选择一个基准元素(通常选择序列的最后一个元素),设置一个指针i(指向小于基准元素的区域的右边界),然后遍历序列从low到high-1,将小于基准元素的元素与i指针指向的元素交换,并移动i指针,遍历结束后,将基准元素与i指针指向的元素交换,确定基准元素的位置。具体步骤如下:
-
选择序列的最后一个元素作为基准元素pivot = arr[high];
-
初始化指针i = low - 1(表示小于基准元素的区域初始为空);
-
遍历j从low到high-1:
- 若arr[j] ≤ pivot,将i指针加1,交换arr[i]和arr[j];
-
遍历结束后,将i指针加1,交换arr[i]和arr[high](基准元素),此时基准元素的位置即为i,分区完成。
Lomuto 分区法的优势是逻辑简单,易于理解和实现,但由于每次都选择最后一个元素作为基准,在序列有序时会出现最坏情况,且交换次数略多于霍尔分区法。
三、算法实现(以Java为例)
本文分别实现霍尔分区法和 Lomuto 分区法的快速排序,均采用递归方式实现,同时补充非递归实现(避免递归栈溢出),并附带详细注释,便于读者理解和调试。
(1)霍尔分区法
java
/**
* 快速排序(霍尔分区法)
* @param arr 待排序数组
* @param low 排序起始索引
* @param high 排序终止索引
*/
public static void quickSortHoare(int[] arr, int low, int high) {
// 递归终止条件:当起始索引大于等于终止索引时,子序列长度为1或0,已有序
if (low < high) {
// 执行分区操作,获取基准元素的最终位置
int pivotIndex = partitionHoare(arr, low, high);
// 递归排序左子序列(基准元素左侧)
quickSortHoare(arr, low, pivotIndex - 1);
// 递归排序右子序列(基准元素右侧)
quickSortHoare(arr, pivotIndex + 1, high);
}
}
/**
* 霍尔分区法:找到基准元素的最终位置,分割数组
* @param arr 待分区数组
* @param low 分区起始索引
* @param high 分区终止索引
* @return 基准元素的最终索引
*/
private static int partitionHoare(int[] arr, int low, int high) {
// 选择序列第一个元素作为基准元素(可优化,后续讲解)
int pivot = arr[low];
int left = low + 1; // 左指针,从基准元素下一个位置开始
int right = high; // 右指针,从序列末尾开始
while (true) {
// 移动左指针,找到第一个大于基准元素的元素
while (left <= right && arr[left] <= pivot) {
left++;
}
// 移动右指针,找到第一个小于基准元素的元素
while (left <= right && arr[right] > pivot) {
right--;
}
// 若左指针大于右指针,分区结束
if (left > right) {
break;
}
// 交换左、右指针指向的元素
swap(arr, left, right);
// 交换后,移动指针继续遍历
left++;
right--;
}
// 交换基准元素和右指针指向的元素,确定基准元素最终位置
swap(arr, low, right);
return right; // 返回基准元素索引
}
/**
* 辅助方法:交换数组中两个索引位置的元素
* @param arr 数组
* @param i 索引1
* @param j 索引2
*/
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// 快速排序调用入口(简化用户调用)
public static void quickSortHoare(int[] arr) {
// 边界校验:数组为空或长度小于2,无需排序
if (arr == null || arr.length < 2) {
return;
}
quickSortHoare(arr, 0, arr.length - 1);
}
(2)Lomuto 分区法
java
/**
* 快速排序(Lomuto 分区法)
* @param arr 待排序数组
* @param low 排序起始索引
* @param high 排序终止索引
*/
public static void quickSortLomuto(int[] arr, int low, int high) {
if (low < high) {
int pivotIndex = partitionLomuto(arr, low, high);
quickSortLomuto(arr, low, pivotIndex - 1);
quickSortLomuto(arr, pivotIndex + 1, high);
}
}
/**
* Lomuto 分区法:找到基准元素的最终位置,分割数组
* @param arr 待分区数组
* @param low 分区起始索引
* @param high 分区终止索引
* @return 基准元素的最终索引
*/
private static int partitionLomuto(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 quickSortLomuto(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
quickSortLomuto(arr, 0, arr.length - 1);
}