快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

快速排序算法(quick sort)——较优的算法_子序列

快速排序(QuickSort)算法是一种十分高效的排序算法,具有平均运行时间为O(nlogn)的特性。该算法的核心思想是分治策略,即将待排数据序列不断划分成两个子序列,并对每个子序列递归地应用快速排序算法,直到每个子序列只剩下一个元素时,整个序列也就自然完成了排序。

快速排序算法的实现过程相对简单,但需要注意一些细节。本文将从以下4个方面介绍快速排序算法:分割操作、递归过程、性能优化和稳定性分析。

分割操作

快速排序的核心操作就是分割,这个操作将待排数据序列分成两个子序列,一个子序列小于等于基准值(pivot),另一个子序列大于基准值。具体实现如下:

首先,我们需要选取一个基准值。通常情况下,我们采用序列的中间位置作为基准值;也可以随机选取一个元素作为基准值,以避免某些特殊情况下的不良影响。

然后,我们从两端向中间扫描数据序列,找到左侧第一个大于基准值的元素和右侧第一个小于基准值的元素,将它们交换位置。如下图所示:

  1. 1 3 5 7 9 4 2 8 基准值为5
  2. ↑ ↓
  3. 1 3 2 7 9 4 5 8
  4. ↑ ↓
  5. 1 3 2 4 9 7 5 8
  6. ↑ ↓
  7. 1 3 2 4 5 7 9 8

最后,我们将基准值与左侧子序列的最后一个元素(即小于等于基准值的元素的最后一个元素)交换位置,以保证左侧子序列小于等于基准值,右侧子序列大于基准值。如下图所示:

  1. 1 3 2 4 5 7 9 8
  2. 1 3 2 4 5 7 8 9

分割操作结束后,我们可以得到一个新的基准值位置index(即原始基准值所在的位置),并且保证index左侧子序列小于等于基准值,index右侧子序列大于基准值。

快速排序算法(quick sort)——较优的算法_子序列_02

递归过程

快速排序算法通过不断的分割操作,将待排数据序列划分成两个子序列,并对每个子序列递归地应用快速排序算法,直到每个子序列只剩下一个元素时,整个序列也就自然完成了排序。

具体来说,递归过程可以通过以下伪代码实现:

  1. function quicksort(array, left, right)
  2. if (left < right)
  3. // 对当前子序列进行分割操作,并得到新基准值位置
  4. pivotIndex = partition(array, left, right)
  5. // 对左侧子序列递归排序
  6. quicksort(array, left, pivotIndex - 1)
  7. // 对右侧子序列递归排序
  8. quicksort(array, pivotIndex + 1, right)

在上面的伪代码中,quicksort()是快速排序函数的入口,它接受三个参数:待排数据序列array、左侧子序列的起始位置left、右侧子序列的起始位置right。如果子序列中存在多个元素,即left < right,则继续进行下一步操作。否则,直接返回。

接着,我们调用partition()函数对当前子序列进行分割操作,得到新的基准值位置pivotIndex。

然后,我们对基准值左侧的子序列(即左侧子序列)进行递归调用,即:

quicksort(array, left, pivotIndex - 1)

这里的left是原始子序列的左侧位置,pivotIndex - 1是新的右侧位置。递归调用quickSort()函数时,我们将子序列的左侧位置设为left,右侧位置设为pivotIndex - 1,以保证每次递归的子序列规模都比原来小。

然后,我们对基准值右侧的子序列(即右侧子序列)进行递归调用,即:

quicksort(array, pivotIndex + 1, right)

这里的pivotIndex + 1是新的左侧位置,right是原始子序列的右侧位置。递归调用quickSort()函数时,我们将子序列的左侧位置设为pivotIndex + 1,右侧位置设为right,以保证每次递归的子序列规模都比原来小。

最终,当每个子序列只剩下一个元素时,整个序列也自然完成了排序。

需要注意的是,在递归过程中可能存在深度较深的情况,而每次递归都要开辟新的函数栈,因此快速排序算法在处理大规模数据时需要注意栈溢出等问题。我们可以采用尾递归优化或者迭代方式实现快速排序算法,以避免这个问题。

以下是基于C语言实现的快速排序算法代码:

  1. // 快速排序函数
  2. void quicksort(int array[], int left, int right) {
  3. if (left >= right) return; // 递归终止条件:子序列为空或只有一个元素
  4. int i = left, j = right, pivot = array[left]; // 选择第一个元素作为基准值(pivot)
  5. while (i < j) {
  6. while (i < j && array[j] >= pivot) j--; // 从右往左找到第一个小于pivot的元素
  7. if (i < j) array[i++] = array[j]; // 将该元素移到左边
  8. while (i < j && array[i] < pivot) i++; // 从左往右找到第一个大于等于pivot的元素
  9. if (i < j) array[j--] = array[i]; // 将该元素移到右边
  10. }
  11. array[i] = pivot; // 将基准值放入最终位置
  12. quicksort(array, left, i - 1); // 对基准值左侧的子序列递归调用
  13. quicksort(array, i + 1, right); // 对基准值右侧的子序列递归调用
  14. }
  15. // 测试代码
  16. int main() {
  17. int array[] = {5, 1, 3, 4, 2};
  18. int n = sizeof(array) / sizeof(int);
  19. quicksort(array, 0, n - 1);
  20. for (int i = 0; i < n; i++) {
  21. printf("%d ", array[i]);
  22. }
  23. return 0;
  24. }

在上面的代码中,我们首先实现了快速排序函数quicksort(),该函数接受三个参数:待排数据序列array、左侧子序列的起始位置left、右侧子序列的起始位置right。在函数内部,我们通过while循环对当前子序列进行分割操作,将小于基准值的元素移到左边,大于等于基准值的元素移到右边,并将基准值放入最终位置。

然后,我们对基准值左侧和右侧的子序列分别递归调用quicksort()函数,直到每个子序列只剩下一个元素时,整个序列也就自然完成了排序。最后,我们通过main函数测试了quicksort()函数的正确性,输出了排好序的结果。需要注意的是,在实际使用中,我们需要根据具体的应用场景选择合适的数据类型,并注意避免数组越界等问题。

性能优化

尽管快速排序算法已经相当高效,但仍有一些性能问题需要注意:

(1)最坏情况下的时间复杂度是O(n2):当待排数据序列已经有序或接近有序时,每次分割操作只能将待排序列划分成一个元素和其它所有元素两个序列,此时快速排序的时间复杂度将退化成O(n2)。这个问题可以通过随机选取基准值或者采用三数中值法等方式进行缓解。

(2)递归深度过深:递归过程中可能存在深度较深的情况,导致函数栈溢出或者性能下降。一个简单的解决方案是使用尾递归优化或迭代方式实现快速排序算法。

(3)数据重复率高:当待排数据序列中存在大量相同元素时,分割操作可能会使得子序列的规模很不平衡,导致快速排序算法的性能下降。这个问题可以通过使用双路快排(Two-Way QuickSort)或三路快排(Three-Way QuickSort)等方式进行缓解。

稳定性分析

快速排序算法不具备稳定性,即数据序列中相同元素的相对位置可能在排序过程中发生变化。这是因为快速排序算法不对相同元素的顺序进行特殊考虑,只保证小于等于基准值和大于基准值两个子序列之间的顺序关系。如果需要保证排序结果的稳定性,建议采用其他稳定排序算法,如归并排序(MergeSort)等。

综上所述,快速排序算法是一种高效快捷的排序算法,其核心思想是利用分治策略将序列不断划分成两个子序列,并对每个子序列递归地应用快速排序算法,直到每个子序列只剩下一个元素时,整个序列也就自然完成了排序。虽然快速排序算法具有平均运行时间为O(nlogn)的特性,但仍需要注意分割操作、递归过程、性能优化和稳定性分析等问题。

图片来源:https://www.runoob.com/w3cnote/quick-sort-2.html