数组排序02|快速排序:排序算法中的闪电
快速排序是一种高效的排序算法,由英国计算机科学家托尼·霍尔在1960年提出。它的核心思想也是“分而治之”,通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程递归进行,以此达到整个数据变成有序序列。
快速排序的详细步骤
- 选择基准值:从数组中选择一个基准值(pivot)。
- 分区操作:将数组中小于基准值的元素放在基准值的左边,大于基准值的元素放在基准值的右边,基准值放在中间。
- 递归排序:对基准值左右两边的子数组按照同样的方式进行递归排序。
实现代码
分区操作是快速排序的核心,下面是分区操作的实现代码:
// Lomuto partition scheme
int partition(int arr[], int low, int high) {
// 选择数组最后一个元素作为枢轴
int pivot = arr[high];
// i 是小于枢轴元素的最后一个元素的索引
int i = low - 1;
// 遍历数组,将小于枢轴的元素放到左边
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
// 交换 arr[i] 和 arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 将枢轴元素放到正确的位置
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
// 返回枢轴元素的索引
return i + 1;
}
实现了分区操作后,我们可以使用递归的方式实现快速排序:
void quick_sort(int arr[], int low, int high) {
if (low < high) {
// 分区操作,将数组分为两部分
int mid = partition(arr, low, high);
// 递归排序左边的子数组
quick_sort(arr, low, mid - 1);
// 递归排序右边的子数组
quick_sort(arr, mid + 1, high);
}
}
算法分析
- 时间复杂度:平均情况下为
O(nlogn)
,最坏情况下为O(n^2)
。 - 空间复杂度:
O(logn)
。
快速排序的优化方案
1. Hoare 分区方案
Lomuto 分区方案在最坏情况下的时间复杂度为 O(n^2)
,主要原因是它会将所有小于枢轴的元素放在一边,而将所有大于或等于枢轴的元素放在另一边。更优的分区方案是 Hoare 分区方案,它在处理大量重复元素时性能更好。Hoare 分区方案通过两个指针从数组的两端向中间移动,交换不符合条件的元素,直到两个指针相遇。
下面是 Hoare 分区方案的实现代码:
// Hoare partition scheme
int partition(int arr[], int low, int high) {
// 选择数组中间的元素作为枢轴
int pivot = arr[(low + high) / 2];
int i = low - 1, j = high + 1;
// 交换元素直到 i >= j
while (1) {
do {
i++;
} while (arr[i] < pivot);
do {
j--;
} while (arr[j] > pivot);
if (i >= j) {
return j;
}
// 交换 arr[i] 和 arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
2. 三数取中法
快速排序的性能和枢轴的选择有很大关系,如果选择的枢轴是数组的最大值或最小值,会导致快速排序的性能下降。为了避免这种情况,可以采用三数取中法,即选择数组的第一个元素、中间元素和最后一个元素的中位数作为枢轴。
下面是三数取中法的实现代码:
// 三数取中法选择枢轴
int median_of_three(int arr[], int low, int high) {
int mid = (low + high) / 2;
// 保证 arr[low] <= arr[mid] <= arr[high]
if (arr[low] > arr[mid]) {
int temp = arr[low];
arr[low] = arr[mid];
arr[mid] = temp;
}
if (arr[mid] > arr[high]) {
int temp = arr[mid];
arr[mid] = arr[high];
arr[high] = temp;
}
if (arr[low] > arr[mid]) {
int temp = arr[low];
arr[low] = arr[mid];
arr[mid] = temp;
}
return mid;
}
使用三数取中法选择枢轴后,新的分区函数代码如下:
int partition(int arr[], int low, int high) {
// 使用三数取中法选择枢轴
int pivot = arr[median_of_three(arr, low, high)];
int i = low - 1, j = high + 1;
// 交换元素直到 i >= j
while (1) {
do {
i++;
} while (arr[i] < pivot);
do {
j--;
} while (arr[j] > pivot);
if (i >= j) {
return j;
}
// 交换 arr[i] 和 arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
3. 随机化快速排序
除了三数取中法,还可以通过随机选择枢轴的方式来优化快速排序。随机化快速排序的思路是在每次递归调用中,随机选择一个枢轴,而不是固定地选择第一个元素、最后一个元素或中间元素。
下面是随机化快速排序的实现代码:
int partition(int arr[], int low, int high) {
// 随机选择枢轴
int pivot_index = rand() % (high - low + 1) + low;
int pivot = arr[pivot_index];
// 交换 arr[pivot_index] 和 arr[high]
int temp = arr[pivot_index];
arr[pivot_index] = arr[high];
arr[high] = temp;
int i = low - 1, j = high + 1;
// 交换元素直到 i >= j
while (1) {
do {
i++;
} while (arr[i] < pivot);
do {
j--;
} while (arr[j] > pivot);
if (i >= j) {
return j;
}
// 交换 arr[i] 和 arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
4. 双枢轴快速排序(Dual-Pivot Quick Sort)
双枢轴快速排序是一种改进的快速排序算法,也被称为“双路快排”,由俄罗斯计算机科学家弗拉基米尔·约瑟福维奇·沃洛佐金在2009年提出。双枢轴快速排序通过两个枢轴将数组分为三部分,左边的部分小于第一个枢轴,中间的部分在第一个枢轴和第二个枢轴之间,右边的部分大于第二个枢轴,也就是说,双枢轴快速排序将数组分为三部分,而不是两部分。
下面是双枢轴快速排序的实现代码:
void dual_pivot_quick_sort(int arr[], int low, int high) {
if (low < high) {
// 选择两个枢轴
int pivot1 = arr[low], pivot2 = arr[high];
if (pivot1 > pivot2) {
int temp = pivot1;
pivot1 = pivot2;
pivot2 = temp;
}
int i = low + 1, k = low + 1, j = high - 1;
while (k <= j) {
if (arr[k] < pivot1) {
// 交换 arr[i] 和 arr[k]
int temp = arr[i];
arr[i] = arr[k];
arr[k] = temp;
i++;
} else if (arr[k] >= pivot2) {
while (arr[j] > pivot2 && k < j) {
j--;
}
// 交换 arr[j] 和 arr[k]
int temp = arr[j];
arr[j] = arr[k];
arr[k] = temp;
j--;
if (arr[k] < pivot1) {
// 交换 arr[i] 和 arr[k]
int temp = arr[i];
arr[i] = arr[k];
arr[k] = temp;
i++;
}
}
k++;
}
i--;
j++;
// 交换 arr[low] 和 arr[i]
int temp = arr[low];
arr[low] = arr[i];
arr[i] = temp;
// 交换 arr[high] 和 arr[j]
temp = arr[high];
arr[high] = arr[j];
arr[j] = temp;
// 递归排序左边的子数组
dual_pivot_quick_sort(arr, low, i - 1);
// 递归排序中间的子数组
dual_pivot_quick_sort(arr, i + 1, j - 1);
// 递归排序右边的子数组
dual_pivot_quick_sort(arr, j + 1, high);
}
}
总结
相比于归并排序,快速排序是一种原地排序算法,不需要额外的空间,但在最坏情况下的时间复杂度为 O(n^2)
。通过优化枢轴的选择和分区方案,可以提高快速排序的性能,使其在大多数情况下表现优异,因此快速排序是目前最常用的排序算法之一。