C++中的sort与自定义排序详解

 更新时间:2025年08月27日 09:02:05   作者:oymaster  
std::sort是C++模板函数,采用内省排序(混合快速、堆、插入排序)动态优化性能,确保O(nlogn)效率与鲁棒性,通过比较器、Lambda或重载运算符实现自定义排序逻辑

基本使用与原理

std::sort 是一个模板函数,常见签名如下:

template<class RandomIt>
void sort(RandomIt first, RandomIt last);

template<class RandomIt, class Compare>
void sort(RandomIt first, RandomIt last, Compare comp);
  • first,last:指定排序范围的随机访问迭代器。
  • comp:可选的比较器,定义元素顺序,默认为 std::less(基于 operator< 的升序排序)。
  • 时间复杂度:平均和最坏情况均为 O(n log n)。
  • 空间复杂度:通常为 O(log n),用于递归栈或临时缓冲区。
  • 要求:比较器必须满足严格弱序(strict weak ordering),否则可能导致未定义行为。

核心算法:内省排序(Introsort)

C++ 标准未强制指定 std::sort 的实现算法,但大多数现代标准库(如 libstdc++、libc++)采用 内省排序(Introsort)

Introsort 由 David Musser 于 1997 年提出,是一种混合排序算法,结合了快速排序(Quicksort)、堆排序(Heapsort)和插入排序(Insertion Sort)的优点,以兼顾效率和鲁棒性。

1. 快速排序

快速排序是 Introsort 的主要算法,步骤如下:

  • 选择基准(pivot):通常使用三数取中(median-of-three,选取首、尾、中间元素的中位数)或随机选择。
  • 分区(partition):将元素分为小于等于基准和大于基准的两部分。
  • 递归:对两个子区间递归排序。

特点

  • 平均时间复杂度:O(n log n)。
  • 最坏情况(如已排序或逆序数组):O(n²)。
  • 优化:三数取中或随机化基准减少最坏情况概率。

实现细节

  • 分区方案:常用 Lomuto 或 Hoare 分区算法。
  • 基准选择:三数取中避免极端情况(如已排序输入)。

2. 堆排序

当快速排序的递归深度超过阈值(通常为 2 * log n),Introsort 切换到堆排序,以避免快速排序的最坏情况。

步骤

  • 构建最大堆(或最小堆,取决于排序顺序)。
  • 反复将堆顶元素移到末尾并调整堆。

特点

  • 时间复杂度:始终为 O(n log n)。
  • 空间复杂度:O(1)(原地排序)。
  • 适合处理快速排序退化的场景。

实现细节

  • 使用数组表示堆,通过下标计算父子节点关系。
  • 堆调整(sift-down)确保堆性质。

3. 插入排序

当子区间大小较小时(通常 < 16 或 32 个元素,具体阈值依实现而定),Introsort 切换到插入排序。

步骤

  • 逐个将元素插入到已排序的子序列中。

特点

  • 时间复杂度:O(n²),但在小数组上常数开销低。
  • 适合部分有序或小规模数据。

实现细节

  • 通常通过循环实现,避免递归。
  • 常用于优化小规模子区间的排序。

原因:

  • 快速排序:平均性能优异,但在最坏情况下(如已排序或重复元素)退化为 O(n²)。
  • 堆排序:保证最坏情况 O(n log n),但平均性能稍逊,且堆调整开销较高。
  • 插入排序:在小规模数据上高效,减少递归开销。 Introsort 动态切换算法,确保:
  • 高效性:利用快速排序的平均性能。
  • 鲁棒性:通过堆排序避免最坏情况。
  • 优化性:插入排序处理小数组。

自定义排序

1. 函数指针

定义一个独立的比较函数,签名需为bool(T, T)

bool compare(int a, int b) {
    return a > b; // 降序排序
}

std::vector<int> vec = {5, 2, 9, 1, 5};
std::sort(vec.begin(), vec.end(), compare); // 结果为 {9, 5, 5, 2, 1}

2. 静态成员函数

在类中定义静态比较函数,避免依赖对象实例:

class Sorter {
public:
    static bool compare(int a, int b) {
        return a > b; // 降序排序
    }
};

std::vector<int> vec = {5, 2, 9, 1, 5};
std::sort(vec.begin(), vec.end(), Sorter::compare);

注意:非静态成员函数因隐含this指针,签名不匹配std::sort的要求,因此无法直接使用。

3. 使用函数对象

通过定义一个类并重载operator(),可以在比较器中存储状态:

class Comparator {
    int threshold;
public:
    Comparator(int t) : threshold(t) {}
    bool operator()(int a, int b) const {
        return a > threshold && a < b; // 仅对大于threshold的元素降序排序
    }
};

std::vector<int> vec = {5, 2, 9, 1, 5};
std::sort(vec.begin(), vec.end(), Comparator(3));

4. 使用Lambda表达式(C++11及以上)

Lambda表达式提供了一种简洁的方式定义比较器,并可捕获外部变量:

int threshold = 3;
std::vector<int> vec = {5, 2, 9, 1, 5};
std::sort(vec.begin(), vec.end(), [threshold](int a, int b) {
    return a > threshold && a < b;
});

5.使用std::function

C++11引入的std::function可以包装任何可调用对象(包括函数指针、Lambda、函数对象等),提供更灵活的方式:

#include <functional>
std::function<bool(int, int)> comp = [](int a, int b) {
    return a > b; // 降序排序
};

std::vector<int> vec = {5, 2, 9, 1, 5};
std::sort(vec.begin(), vec.end(), comp);

适用场景:当需要在运行时动态选择比较器时,std::function非常有用,但会引入少量性能开销。

6.使用标准比较器(如std::greater、std::less)

C++标准库提供了预定义的比较器(如<functional>中的std::greaterstd::less),可直接用于简单排序需求:

#include <functional>
std::vector<int> vec = {5, 2, 9, 1, 5};
std::sort(vec.begin(), vec.end(), std::greater<int>()); // 降序排序
std::sort(vec.begin(), vec.end(), std::less<int>());   // 升序排序

优点:无需手动定义比较器,代码简洁,适合常见升序或降序需求。

7.重载operator<

对于自定义结构体或类,可以通过重载operator<来定义默认排序规则,省去显式比较器:

struct Person {
    std::string name;
    int age;
    bool operator<(const Person& other) const {
        return age < other.age; // 按年龄升序
    }
};

std::vector<Person> people = {{"Alice", 25}, {"Bob", 30}, {"Charlie", 20}};
std::sort(people.begin(), people.end()); // 使用operator<,按年龄升序

适用场景:当类型有自然的排序规则且不需要多种排序方式时,重载operator<是最简洁的方案。

自定义结构体或类的排序

当排序对象是自定义结构体或类时,可以结合上述方法。

例如,使用Lambda:

std::vector<Person> people = {{"Alice", 25}, {"Bob", 30}, {"Charlie", 20}};

// 按名字字典序排序
std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) {
    return a.name < b.name;
});

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • C++运算符重载规则详解

    C++运算符重载规则详解

    这篇文章主要介绍了C++运算符重载规则详解,是C++入门学习中的基础知识,需要的朋友可以参考下
    2015-09-09
  • 原码, 反码与补码基础知识详细介绍

    原码, 反码与补码基础知识详细介绍

    这篇文章讲解了计算机的原码, 反码和补码. 并且进行了深入探求了为何要使用反码和补码, 以及更进一步的论证了为何可以用反码, 补码的加法计算原码的减法,需要的朋友可以参考下
    2016-12-12
  • 给C语言初学者的学习建议

    给C语言初学者的学习建议

    在本篇文章里小编给大家分享的是关于C语言学习建议的相关内容,有兴趣的朋友们可以学习参考下。
    2020-06-06
  • c++中的set容器介绍及操作大全

    c++中的set容器介绍及操作大全

    这篇文章主要介绍了c++中的set容器介绍及操作大全,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2025-06-06
  • C语言寻找无向图两点间的最短路径

    C语言寻找无向图两点间的最短路径

    这篇文章主要为大家详细介绍了C语言寻找无向图两点间的最短路径,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-01-01
  • Qt基础开发之QString与QByteArray详细用法与区别及QString QByteArray互转

    Qt基础开发之QString与QByteArray详细用法与区别及QString QByteArray互转

    这篇文章主要介绍了Qt基础开发之QString与QByteArray详细用法与区别及QString QByteArray互转,需要的朋友可以参考下
    2020-03-03
  • C语言实现任意进制转换器

    C语言实现任意进制转换器

    这篇文章主要为大家详细介绍了C语言实现任意进制转换器,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • 详解C++-二阶构造模式、友元

    详解C++-二阶构造模式、友元

    这篇文章主要介绍了C++-二阶构造模式、友元,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-03-03
  • C++关于size_t的bug解决案例

    C++关于size_t的bug解决案例

    这篇文章主要为大家介绍了C++关于size_t的bug解决案例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-10-10
  • C++实现String与UF8互转

    C++实现String与UF8互转

    这篇文章介绍了C++实现String与UF8互转的方法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-05-05

最新评论