std::sort 为什么那么快?STL 快排详解

深入解析std::sort的快速排序优化

一、传统快速排序算法

以下来自OI-Wiki

快速排序(英语:Quicksort),又称分区交换排序(英语:partition-exchange sort),简称「快排」,是一种被广泛运用的排序算法。

过程

快速排序的工作原理是通过 分治 的方式来将一个数组排序。

快速排序分为三个过程:

  1. 将数列划分为两部分(要求保证相对大小关系);
  2. 递归到两个子序列中分别进行快速排序;
  3. 不用合并,因为此时数列已经完全有序。

和归并排序不同,第一步并不是直接分成前后两个序列,而是在分的过程中要保证相对大小关系。具体来说,第一步要是要把数列分成两个部分,然后保证前一个子数列中的数都小于后一个子数列中的数。

快速排序的最优时间复杂度和平均时间复杂度为 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++ STLStandard 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),它是如何实现的呢?

  1. 快速排序算法最坏情况下时间复杂度退化为 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)层时,强行停止递归,转而对当前处理的子数组进行堆排序。

  2. 此外,传统的快速排序在数据量很小时,为极小的子数组产生许多的递归调用,得不偿失。为此,STL的sort()进行了优化,在小数据量的情况下改用插入排序具体做法是:当递归处理的子数组长度(子数组包含的元素个数)小于等于某个阈值threshold 时,停止处理并退出本层递归,使当前子数组停留在“接近排序但尚未完成”的状态,最后待所有递归都退出后,再对整个序列进行一次插入排序(注意不是对当前处理的子数组进行插入排序,而是在快速排序的所有递归完全退出后,对整个数组统一进行一次插入排序)。实验表明,此种策略有着良好的效率,因为插入排序在面对“接近有序”的序列时拥有良好的性能。

  3. “三数取中”选基准元素。不是选取第一个元素作为基准元素,而是在当前子数组中选取3个元素,取中间大的那个元素作为基准元素。从而保证选出的基准元素不是子数组的最小元素,也不是最大元素,避免Partition分到子数组最边上,以降低最坏情况发生的概率。

  4. 尾递归转为循环。 即将传统快速排序代码

    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时转入堆排序。

  5. 优先处理短区间。 在上述策略(4)的基础上进一步改进,不是按固定次序处理左右子区间(每次都先处理左区间、后处理右区间),而是先(通过递归)处理左右两个子区间中“较短的那个区间”,然后再(通过循环)处理两个子区间中“较长的那个区间”。从而使每次递归处理的子数组长度至少缩减一半,使最坏情况下递归深度(算法最坏情况空间复杂度)为 O ( l o g n ) O(logn) O(logn)

  6. 三路分划(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 检验

P1177 【模板】排序

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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_Equinox

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值