4-6 比较排序:快速排序

快速排序

快速排序是一种基于分治法的排序算法,它选择一个元素作为枢轴,并将给定数组围绕该枢轴进行划分,最终将枢轴放置在排序数组中的正确位置。

该算法主要包含三个步骤:

  1. 选择枢轴元素:从数组中选择一个元素作为枢轴元素。枢轴元素的选择可以是多种多样的(例如,第一个元素、最后一个元素、随机元素或中位数)。
  2. 对数组进行分区:围绕中心元素重新排列数组。分区后,所有小于中心元素的元素都将位于中心元素的左侧,所有大于中心元素的元素都将位于中心元素的右侧。
  3. 递归调用:递归地对两个分区子数组应用相同的过程。
  4. 基本情况:当子数组中只剩下一个元素时,递归停止,因为单个元素已经排序。

image

选择枢轴点
选择枢轴点有很多不同的方法。

  • 始终选择第一个(或最后一个)元素作为枢轴。下面的实现选择最后一个元素作为枢轴。这种方法的缺点是,当数组已经排序时,最终结果会非常糟糕。
  • 随机选择一个元素作为枢轴点。这种方法更可取,因为它不存在导致最坏情况发生的固定模式。
  • 选择中位数作为枢轴元素。就时间复杂度而言,这是一种理想的方法,因为我们可以在线性时间内找到中位数,并且配分函数总是会将输入数组分成两半。但由于中位数查找的常数较大,因此平均耗时更长。

分区算法
快速排序的关键步骤是分区(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;
}

输出:
image


快速排序的复杂度分析、优缺点、以及应用

快速排序算法的复杂度分析
时间复杂度:

  • 最佳情况: (Ω(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 个最小/最大的元素。
  • 根据多个键(自定义比较器)对对象数组进行排序。
  • 数据压缩算法(如霍夫曼编码预处理)。
  • 图形学和计算几何(例如,凸包算法)。
posted @ 2026-03-30 17:59  游翔  阅读(0)  评论(0)    收藏  举报