文章目录
一、传统快速排序算法
以下来自OI-Wiki
快速排序(英语:Quicksort),又称分区交换排序(英语:partition-exchange sort),简称「快排」,是一种被广泛运用的排序算法。
过程
快速排序的工作原理是通过 分治 的方式来将一个数组排序。
快速排序分为三个过程:
- 将数列划分为两部分(要求保证相对大小关系);
- 递归到两个子序列中分别进行快速排序;
- 不用合并,因为此时数列已经完全有序。
和归并排序不同,第一步并不是直接分成前后两个序列,而是在分的过程中要保证相对大小关系。具体来说,第一步要是要把数列分成两个部分,然后保证前一个子数列中的数都小于后一个子数列中的数。
快速排序的最优时间复杂度和平均时间复杂度为 O ( l o g n ) O(logn) O(logn),最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2).
对于最优情况,每一次选择的分界值都是序列的中位数,此时算法时间复杂度满足的递推式为 T ( n ) = 2 T ( n / 2 ) + Θ ( n ) T(n) = 2T(n/2) + \Theta(n) T(n)=2T(n/2)+Θ(n),由主定理, T ( n ) = Θ ( n l o g n ) T(n) = \Theta(nlogn) T(n)=Θ(nlogn)
二、std::sort
2.1 优化策略
C++ STL是Standard Template Library的简称,即标准模板库。简单来说,STL将常用的数据结构与算法进行了封装,用户需要时可以直接调用,不用重新开发。排序算法**std::sort( )**是STL包含的一个重要算法。
STL中的sort()函数基于快速排序算法实现,众所众知,快速排序是目前已知平均情况下最快的排序算法,被IEEE评选为20世纪十大算法之一,但其最坏情况下时间复杂度会退化为 O ( n 2 ) O(n^2) O(n2)。STL中的std::sort()对传统快速排序做了巧妙的改进,使其最坏情况下时间复杂度也能维持在 O ( n l o g n ) O(nlogn) O(nlogn),它是如何实现的呢?
-
快速排序算法最坏情况下时间复杂度退化为 O ( n 2 ) O(n^2) O(n2)的主要原因是,每次划分(Partition)操作时,都分在子数组的最边上,导致递归深度恶化为O(n)层。而STL的sort()在Partition操作有恶化倾向时,能够自我侦测,转而改为堆排序,使效率维持在堆排序的 O ( n l o g n ) O(nlogn) O(nlogn)。其具体方法是:侦测快速排序的递归深度,当递归深度达到⌊2log2n⌋=O(logn)层时,强行停止递归,转而对当前处理的子数组进行堆排序。
-
此外,传统的快速排序在数据量很小时,为极小的子数组产生许多的递归调用,得不偿失。为此,STL的sort()进行了优化,在小数据量的情况下改用插入排序。具体做法是:当递归处理的子数组长度(子数组包含的元素个数)小于等于某个阈值threshold 时,停止处理并退出本层递归,使当前子数组停留在“接近排序但尚未完成”的状态,最后待所有递归都退出后,再对整个序列进行一次插入排序(注意不是对当前处理的子数组进行插入排序,而是在快速排序的所有递归完全退出后,对整个数组统一进行一次插入排序)。实验表明,此种策略有着良好的效率,因为插入排序在面对“接近有序”的序列时拥有良好的性能。
-
“三数取中”选基准元素。不是选取第一个元素作为基准元素,而是在当前子数组中选取3个元素,取中间大的那个元素作为基准元素。从而保证选出的基准元素不是子数组的最小元素,也不是最大元素,避免Partition分到子数组最边上,以降低最坏情况发生的概率。
-
尾递归转为循环。 即将传统快速排序代码
void QuickSort(int R[],int m,int n){ if(n - m + 1 > threshold){ int j = Partition(R, m, n); QuickSort(R, m, j-1); //递归处理左区间 QuickSort(R, j+1, n); //递归处理右区间,尾递归 } }
转换为
void QuickSort(int R[],int m,int n){ while(n - m + 1 > threshold){ //注意此处不是if,而是while int j = Partition(R, m, n); QuickSort(R, m, j-1); //递归处理左区间 m = j+1; //通过while循环处理右区间,从而消除尾递归 } }
即先递归处理左区间,后循环处理右区间,从而消除一个尾递归,以减少递归调用带来的时空消耗。
这里需注意,尾递归转循环后,转入堆排序的时机不仅仅是递归深度达到2logn,而是递归深度和while循环迭代的次数加一起达到2logn时转入堆排序。 -
优先处理短区间。 在上述策略(4)的基础上进一步改进,不是按固定次序处理左右子区间(每次都先处理左区间、后处理右区间),而是先(通过递归)处理左右两个子区间中“较短的那个区间”,然后再(通过循环)处理两个子区间中“较长的那个区间”。从而使每次递归处理的子数组长度至少缩减一半,使最坏情况下递归深度(算法最坏情况空间复杂度)为 O ( l o g n ) O(logn) O(logn)。
-
三路分划(3-Way Partition)。 当重复元素很多时,传统快速排序效率较低。可修改Partition操作,不是把当前数组划分为两部分,而是三部分:小于基准元素K的元素放在左边,等于K的元素放在中间,大于K的元素在右边。接下来仅需对小于K的左半部分子数组和大于K的右半部分子数组进行排序。中间等于K的所有元素都已就位,无需处理。
2.2 实现代码
- 为了减轻博客编写压力以及较好的代码可读性,以下实现代码仅适用于对int[] 数组升序排序
- 堆调整算法为大根堆
- 没有采用任何模板编程
2.2.1 主代码框架
// 避免命名污染
namespace Equinox {
// 向下调整算法
void adjustDown(int *a, int p, int n) {
//...
}
// 堆排序
void heapSort(int *a, int n) {
//...
}
// 插入排序
void insertSort(int *a, int n) {
//...
}
// 三路划分
std::pair<int, int> partition(int *a, int l, int r) {
//...
}
constexpr int threshold = 10;
// 对 [l, r] 排序
void _sort(int *a, int l, int r, int dep) {
//...
}
void sort(int *a, int n) {
//...
}
}
2.2.2 快排框架
void sort(int *a, int n) {
int dep = std::__lg(n) * 2; // 约定最大递归深度
_sort(a, 0, n - 1, dep);
insertSort(a, n); // 结束后在进行一次插入排序
}
2.2.3 快排主体
constexpr int threshold = 10; // 小区间不做处理,最终会插入排序
void _sort(int *a, int l, int r, int dep) {
if (l >= r) return;
// 优化一个尾递归
while (r - l + 1 > threshold) {
// 深度过深,直接堆排
if (dep == 0) {
heapSort(a + l - 1, r - l + 1);
return;
}
-- dep;
// 三路划分,x 为左区间右端点,y 为右区间左端点
auto [x, y] = partition(a, l, r);
if (x - l <= r - y) {
_sort(a, l, x - 1, dep);
l = y + 1;
}
else {
_sort(a, y + 1, r, dep);
r = x - 1;
}
}
}
2.2.4 三路划分
// 三路划分
std::pair<int, int> partition(int *a, int l, int r) {
// 三数取中
std::swap(a[l + 1], a[(l + r) / 2]);
if (a[l + 1] > a[r]) {
std::swap(a[l + 1], a[r]);
}
if (a[l] > a[r]) {
std::swap(a[l], a[r]);
}
if (a[l + 1] > a[l]) {
std::swap(a[l], a[l + 1]);
}
// lt | eq | gt
int i = l, j = l, k = r, key = a[l];
while (j <= k) {
if (a[j] < key) {
std::swap(a[j], a[i]);
++ i;
++ j;
}
else if (a[j] > key) {
std::swap(a[j], a[k]);
-- k;
}
else {
++ j;
}
}
return std::pair(i, k);
}
2.2.5 堆排序
// 向下调整算法
void adjustDown(int *a, int p, int n) {
int lc = p * 2 + 1; // 0-based 左儿子
while (lc < n)
{
if (lc + 1 < n && a[lc] < a[lc + 1]) {
++ lc;
}
if (a[p] < a[lc]) {
std::swap(a[lc], a[p]);
p = lc;
lc = p * 2 + 1;
}
else {
break;
}
}
}
// 堆排序
void heapSort(int *a, int n) {
for (int i = (n - 1) / 2; i >= 0; -- i) {
adjustDown(a, i, n);
}
for (int i = n - 1; i > 0; -- i) {
std::swap(a[0], a[i]);
adjustDown(a, 0, i - 1);
}
}
2.2.6 插入排序
// 插入排序
void insertSort(int *a, int n) {
for (int i = 1, j, k; i < n; ++ i) {
k = a[i], j = i - 1;
for (; j >= 0 && a[j] > k; -- j) {
a[j + 1] = a[j];
}
a[j + 1] = k;
}
}
2.3 完整代码以及Online Judge 检验
AC代码[70ms]:
#include <bits/stdc++.h>
using i64 = long long;
// 避免命名污染
namespace Equinox {
// 向下调整算法
void adjustDown(int *a, int p, int n) {
int lc = p * 2 + 1; // 0-based 左儿子
while (lc < n)
{
if (lc + 1 < n && a[lc] < a[lc + 1]) {
++ lc;
}
if (a[p] < a[lc]) {
std::swap(a[lc], a[p]);
p = lc;
lc = p * 2 + 1;
}
else {
break;
}
}
}
// 堆排序
void heapSort(int *a, int n) {
for (int i = (n - 1) / 2; i >= 0; -- i) {
adjustDown(a, i, n);
}
for (int i = n - 1; i > 0; -- i) {
std::swap(a[0], a[i]);
adjustDown(a, 0, i - 1);
}
}
// 插入排序
void insertSort(int *a, int n) {
for (int i = 1, j, k; i < n; ++ i) {
k = a[i], j = i - 1;
for (; j >= 0 && a[j] > k; -- j) {
a[j + 1] = a[j];
}
a[j + 1] = k;
}
}
// 三路划分
std::pair<int, int> partition(int *a, int l, int r) {
// 三数取中
std::swap(a[l + 1], a[(l + r) / 2]);
if (a[l + 1] > a[r]) {
std::swap(a[l + 1], a[r]);
}
if (a[l] > a[r]) {
std::swap(a[l], a[r]);
}
if (a[l + 1] > a[l]) {
std::swap(a[l], a[l + 1]);
}
// lt | eq | gt
int i = l, j = l, k = r, key = a[l];
while (j <= k) {
if (a[j] < key) {
std::swap(a[j], a[i]);
++ i;
++ j;
}
else if (a[j] > key) {
std::swap(a[j], a[k]);
-- k;
}
else {
++ j;
}
}
return std::pair(i, k);
}
constexpr int threshold = 10;
void _sort(int *a, int l, int r, int dep) {
if (l >= r) return;
while (r - l + 1 > threshold) {
if (dep == 0) {
heapSort(a + l - 1, r - l + 1);
return;
}
-- dep;
auto [x, y] = partition(a, l, r);
if (x - l <= r - y) {
_sort(a, l, x - 1, dep);
l = y + 1;
}
else {
_sort(a, y + 1, r, dep);
r = x - 1;
}
}
}
void sort(int *a, int n) {
int dep = std::__lg(n) * 2; // 约定最大递归深度
_sort(a, 0, n - 1, dep);
insertSort(a, n); // 结束后在进行一次插入排序
}
}
constexpr int N = 1E5;
int a[N];
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
for (int i = 0; i < n; ++ i) {
std::cin >> a[i];
}
Equinox::sort(a, n);
for (int i = 0; i < n; ++ i) {
std::cout << a[i] << " \n"[i + 1 == n];
}
return 0;
}