4-6 比较排序:快速排序
快速排序
快速排序是一种基于分治法的排序算法,它选择一个元素作为枢轴,并将给定数组围绕该枢轴进行划分,最终将枢轴放置在排序数组中的正确位置。
该算法主要包含三个步骤:
- 选择枢轴元素:从数组中选择一个元素作为枢轴元素。枢轴元素的选择可以是多种多样的(例如,第一个元素、最后一个元素、随机元素或中位数)。
- 对数组进行分区:围绕中心元素重新排列数组。分区后,所有小于中心元素的元素都将位于中心元素的左侧,所有大于中心元素的元素都将位于中心元素的右侧。
- 递归调用:递归地对两个分区子数组应用相同的过程。
- 基本情况:当子数组中只剩下一个元素时,递归停止,因为单个元素已经排序。

选择枢轴点
选择枢轴点有很多不同的方法。
- 始终选择第一个(或最后一个)元素作为枢轴。下面的实现选择最后一个元素作为枢轴。这种方法的缺点是,当数组已经排序时,最终结果会非常糟糕。
- 随机选择一个元素作为枢轴点。这种方法更可取,因为它不存在导致最坏情况发生的固定模式。
- 选择中位数作为枢轴元素。就时间复杂度而言,这是一种理想的方法,因为我们可以在线性时间内找到中位数,并且配分函数总是会将输入数组分成两半。但由于中位数查找的常数较大,因此平均耗时更长。
分区算法
快速排序的关键步骤是分区(partition())。分区有三种常用的算法。所有这些算法的时间复杂度都是 O(n)。
- 朴素分区:这里我们创建数组的副本。首先放入所有较小的元素,然后放入所有较大的元素。最后将临时数组复制回原始数组。这需要 O(n) 的额外空间。
- Lomuto分区:(逻辑很简单,我们从最左边的元素开始,并将小于(或等于)当前元素的索引记为i。遍历过程中,如果找到更小的元素,则将当前元素与arr[i]交换。否则,忽略当前元素。)本文使用了这种分区方法。这是一个简单的算法,它跟踪较小元素的索引并不断交换索引。之所以选择它,是因为它简单易用。
- 霍尔分割法:这是所有分割方法中最快的。它从数组的两侧遍历,不断将左侧较大的元素与右侧较小的元素交换,但数组本身并未被分割。详情请
代码实现
#include <iostream>
#include <vector>
int partition(std::vector<int>& arr, int low, int high)
{
// choose the pivot
int pivot = arr[high];
// undex of smaller element and indicates
// the right position of pivot found so far
int i = low - 1;
// Traverse arr[low..high] and move all smaller
// elements on left side. Elements from low to
// i are smaller after every iteration
for (int j = low; j <= high - 1; j++)
{
if (arr[j] < pivot)
{
i++;
std::swap(arr[i], arr[j]);
}
}
// move pivot after smaller elements and
// return its position
std::swap(arr[i + 1], arr[high]);
return i + 1;
}
// the QuickSort function implementation
void quickSort(std::vector<int>& arr, int low, int high)
{
if (low < high)
{
// pi is the partition return index of pivot
int pi = partition(arr, low, high);
// recursion calls for smaller elements
// and greater or equals elements
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
int main()
{
std::vector<int> arr = {10, 7, 8, 9, 1, 5};
int n = arr.size();
quickSort(arr, 0, n - 1);
for (int i = 0; i < n; i++)
{
std::cout << arr[i] << " ";
}
return 0;
}
输出:

快速排序的复杂度分析、优缺点、以及应用
快速排序算法的复杂度分析
时间复杂度:
- 最佳情况: (Ω(n log n)),当枢轴元素将数组分成两个相等的部分时发生。
- 平均情况(θ(n log n)),平均而言,枢轴将数组分成两部分,但不一定相等。
- 最坏情况: (O(n²)),当总是选择最小或最大的元素作为枢轴时发生(例如,已排序的数组)。
辅助空间:
- 最坏情况:由于不平衡的分区导致倾斜的递归树,需要大小为 O( n ) 的调用栈,因此复杂度为 O(n)。
- 最佳情况: 由于均衡划分,得到一个均衡的递归树,其调用栈大小为 O(log n),因此时间复杂度为 O(log n)。
快速排序的优势
- 它是一种分治算法,可以更轻松地解决问题。
- 它处理大型数据集非常高效。
- 它占用资源少,运行只需要少量内存。
- 由于我们对同一个数组进行排序,并且不会将数据复制到任何辅助数组,因此它对缓存友好。
- 当对稳定性要求不高时,适用于大数据的最快通用算法。
- 它是尾递归的,因此可以进行所有尾调用优化。
快速排序的缺点
- 它的最坏情况时间复杂度为 O(n 2 ),这种情况发生在枢轴选择不当时。
- 对于小型数据集来说,这不是一个好的选择。
- 它不是稳定排序,这意味着如果两个元素具有相同的键,则在快速排序的情况下,它们的相对顺序不会在排序后的输出中保留,因为在这里我们根据枢轴的位置交换元素(而不考虑它们的原始位置)。
快速排序的应用
- 在内存中高效地对大型数据集进行排序。
- 用于库排序函数(如 C++ std::sort 和 Java Arrays.sort 用于基本数据类型)。
- 对数据库中的记录进行排序,以便更快地进行搜索。
- 需要排序输入的算法的预处理步骤(例如,二分查找、双指针技术)。
- 使用快速选择(快速排序的一种变体)查找第 k 个最小/最大的元素。
- 根据多个键(自定义比较器)对对象数组进行排序。
- 数据压缩算法(如霍夫曼编码预处理)。
- 图形学和计算几何(例如,凸包算法)。

浙公网安备 33010602011771号