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语言入门篇--关键字static详解

    C语言入门篇--关键字static详解

    本篇文章是C语言系列基础篇,C语言中,static是用来修饰变量和函数:1.修饰局部变量–>静态局部变量2.修饰全局变量–>静态全局变量3.修饰函数–>静态函数
    2021-08-08
  • C++调用C#的DLL实现方法

    C++调用C#的DLL实现方法

    这篇文章主要介绍了C++调用C#的DLL实现方法,很有实用价值,需要的朋友可以参考下
    2014-07-07
  • 一文带你了解C++中queue的使用

    一文带你了解C++中queue的使用

    C++中的queue是一种容器,用于在FIFO(先进先出)原则下存储和管理元素。本篇文章将深入探讨C++中的queue,包括它的定义、使用、原理和示例,感兴趣的可以了解一下
    2023-04-04
  • C++using声明和using编译指令

    C++using声明和using编译指令

    这篇文章主要介绍了C++using声明和using编译指令,C++当中提供了两种机制来简化对名称空间中名称的使用。using声明使特定的标识符keys,using编译指令使整个名称空间可用。下面我们就来看看这两种机制的相关资料吧,需要的小伙伴可以参考一下
    2021-12-12
  • C语言实现简单的五子棋小游戏

    C语言实现简单的五子棋小游戏

    这篇文章主要为大家详细介绍了C语言实现简单的五子棋小游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-05-05
  • C语言三种函数调用约定_cdecl与_stdcall及_fastcall详细讲解

    C语言三种函数调用约定_cdecl与_stdcall及_fastcall详细讲解

    本篇文章使用的工具是vs2010,内容可能涉及到汇编的知识,建议有一些汇编基础的再来看,不过没有汇编基础也没有关系,了解一下这三种调用约定即可
    2022-10-10
  • 从使用角度解读c++20 协程示例

    从使用角度解读c++20 协程示例

    类比线程,线程是个函数,把这个函数交给 创建线程的api,然后这个函数就变成线程了,这个函数本身没有任何特殊的地方,就是普通函数,这篇文章主要介绍了从使用角度解读c++20 协程示例,需要的朋友可以参考下
    2023-01-01
  • C++之WSAAsyncSelect模型实例

    C++之WSAAsyncSelect模型实例

    这篇文章主要介绍了C++的WSAAsyncSelect模型,实例讲述了socket与Windows消息机制的用法,需要的朋友可以参考下
    2014-10-10
  • C语言fgetc和fputc函数用法详解(以字符形式读写文件)

    C语言fgetc和fputc函数用法详解(以字符形式读写文件)

    这篇文章主要介绍了C语言fgetc和fputc函数用法详解(以字符形式读写文件),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • C++实现ETW进行进程变动监控详解

    C++实现ETW进行进程变动监控详解

    ETW提供了一种对用户层应用程序和内核层驱动创建的事件对象的跟踪记录机制。为开发者提供了一套快速、可靠、通用的一系列事件跟踪特性。本文将利用ETW进行进程变动监控,需要的可以参考一下
    2022-07-07

最新评论