C++STL函数和排序算法的快排以及归并排序详解

前言


排序是算法课的第一课,也是工程里最常见的操作。C++ 标准库提供了std::sort和std::stable_sort,日常代码直接调用即可;但面试和算法练习常要求手写快速排序与归并排序,理解它们的分治结构、复杂度来源和稳定性差异,仍然是必要的基本功。


一个高频误解是「std::sort就是快速排序」。标准只规定复杂度上界,不规定具体算法;主流实现使用内省排序(introsort),即快排、堆排、插入排序的混合:递归过深时切换到堆排,保证最坏 O(N log N),小区间用插入排序减少开销。另一个误解是「比较函数写<=也能用」——比较器必须满足严格弱序(strict weak ordering),写<=是未定义行为。


本文先讲标准库排序函数的契约,再给出快速排序与归并排序的完整实现,最后对比三者的复杂度、稳定性与适用场景。示例以 C++17 为基准,所有代码按标准语义推演,具体运行时间依赖实现与数据,本文不给未测量的数字。


一、标准库的排序接口


std::sort声明在<algorithm>:


// 适用于 C++17(示意签名)
template<class RandomIt>
void sort(RandomIt first, RandomIt last);

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

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

template<class RandomIt, class Compare>
void stable_sort(RandomIt first, RandomIt last, Compare comp);

要点:



  • 要求随机访问迭代器,所以std::vector、std::deque、std::array、原生数组可以,std::list不行(它有自己的成员sort);

  • std::sort不保证稳定,相等元素的相对顺序可能改变;

  • std::stable_sort保证稳定,代价是需要额外内存;拿不到额外内存时复杂度可能退化到 O(N log² N);

  • 两者都是 O(N log N) 次比较(std::sort自 C++11 起要求最坏 O(N log N))。


C 语言遗留的std::qsort在<cstdlib>,签名为void qsort(void* base, std::size_t count, std::size_t size, int (*comp)(const void*, const void*))。它通过void*擦除类型,比较函数还要把指针转回具体类型,既容易写错,也通常比模板版本难优化。C++ 代码应优先用std::sort。


二、比较器与严格弱序


排序算法只依赖一个关系:comp(a, b)表示「a 应当排在 b 前面」。这个关系必须满足严格弱序:



  1. 非自反:comp(x, x)为假;

  2. 非对称:若comp(a, b)为真,则comp(b, a)为假;

  3. 传递:若comp(a, b)且comp(b, c),则comp(a, c);

  4. 等价传递:若 a 与 b 不可比、b 与 c 不可比,则 a 与 c 也不可比。


用<=或>=作为比较器会违反非自反性,std::sort在遇到大量相等元素时可能越界访问,属于未定义行为(undefined behavior, UB)。正确写法是严格小于:


// 适用于 C++17
#include <algorithm>
#include <string>
#include <vector>

struct Person {
std::string name;
int age = 0;
};

void sort_people(std::vector<Person>& people) {
std::sort(people.begin(), people.end(),
[](const Person& a, const Person& b) {
return a.age < b.age; // 不要写 <=
});
}

如果业务要求「年龄相同则保持原顺序」,应使用std::stable_sort,而不是在比较器里加入与输入顺序相关的判断。


三、手写快速排序与归并排序


快速排序的思路是:选一个基准值(pivot),把区间分成「小于基准」和「不小于基准」两部分,再对两部分递归。平均复杂度 O(N log N),最坏 O(N²)(例如每次选到最大或最小值作为基准),额外空间来自递归栈,平均 O(log N),最坏 O(N)。


下面的实现选取区间中点作为基准,用两个指针从两端向中间扫描并交换:


// 适用于 C++17
#include <utility>
#include <vector>

template <class T>
void quick_sort(std::vector<T>& a, int lo, int hi) {
if (lo >= hi) {
return;
}

int i = lo;
int j = hi;
const T pivot = a[lo + (hi - lo) / 2];

while (i <= j) {
while (i <= hi && a[i] < pivot) {
++i;
}
while (j >= lo && pivot < a[j]) {
--j;
}
if (i <= j) {
std::swap(a[i], a[j]);
++i;
--j;
}
}

quick_sort(a, lo, j);
quick_sort(a, i, hi);
}

几个细节决定了它是否正确:



  • 两个内层循环都带i <= hi/j >= lo的边界检查,防止基准值恰好是极值时越界;

  • 交换后++i、--j,保证区间收缩,否则遇到相等元素会死循环;

  • 递归调用的是[lo, j]和[i, hi],当j < lo或i > hi时由开头的lo >= hi兜住。


这个版本对含大量重复元素的数组仍然可能退化:所有元素相等时,每轮只把区间缩小常数个位置。工程实现通常用三路划分(小于、等于、大于)处理重复值。另一个改进是「小区间用插入排序」,减少递归调用开销。


基准选择直接影响最坏情况。选首元素时,已排序数组会退化成 O(N²);选中点能缓解常见的顺序输入,但仍存在构造出的最坏输入。std::sort用内省排序解决这个问题:记录递归深度,超过阈值就改用堆排序,把最坏复杂度压回 O(N log N)。


归并排序把区间对半切开,分别排序后再合并。合并需要一块与区间等长的辅助空间,时间复杂度稳定在 O(N log N),且天然稳定——只要合并时「右边元素严格小于左边元素才取右边」,相等元素就会保持原有先后。


// 适用于 C++17
#include <algorithm>
#include <cstddef>
#include <vector>

template <class T>
void merge_sort_impl(std::vector<T>& a, std::vector<T>& buffer,
std::size_t lo, std::size_t hi) {
if (hi - lo < 2) {
return;
}

const std::size_t mid = lo + (hi - lo) / 2;
merge_sort_impl(a, buffer, lo, mid);
merge_sort_impl(a, buffer, mid, hi);

std::size_t i = lo;
std::size_t j = mid;
std::size_t k = lo;

while (i < mid && j < hi) {
if (a[j] < a[i]) {
buffer[k++] = a[j++]; // 严格小于才取右边,保证稳定
} else {
buffer[k++] = a[i++];
}
}
while (i < mid) {
buffer[k++] = a[i++];
}
while (j < hi) {
buffer[k++] = a[j++];
}

const auto offset = static_cast<std::ptrdiff_t>(lo);
std::copy_n(buffer.begin() + offset, static_cast<std::ptrdiff_t>(hi - lo),
a.begin() + offset);
}

template <class T>
void merge_sort(std::vector<T>& a) {
std::vector<T> buffer(a.size());
merge_sort_impl(a, buffer, 0, a.size());
}

注意merge_sort_impl的区间是左闭右开[lo, hi),所以hi可以等于a.size(),不会越界。buffer在整个递归过程中复用,避免每层都分配新数组。


归并排序的代价是 O(N) 额外空间。对std::vector这种连续存储,额外空间通常可以接受;对链表,归并反而是优势——std::list::sort就是基于归并的成员函数,不需要随机访问。


四、三者对比与选择




算法平均时间最坏时间额外空间稳定性适用场景



std::sortO(N log N)O(N log N)O(log N) 栈否通用,默认首选

std::stable_sortO(N log N)O(N log² N) 无额外内存时O(N)是需要保持相等元素顺序

手写快排O(N log N)O(N²)O(log N) 栈否教学、算法练习

手写归并O(N log N)O(N log N)O(N)是链表、外部排序、要求稳定

std::qsort取决于实现取决于实现取决于实现否维护 C 接口时



选择顺序可以简化为:默认std::sort;需要稳定用std::stable_sort;数据在链表上且不想搬移元素,用std::list::sort;数据大到内存放不下,考虑外部归并。手写快排和归并主要用于理解原理或特殊约束下的定制。


常见坑点



  1. ❌ 比较器写成return a.x <= b.x;。


✅ 必须满足严格弱序;相等时返回false,用<。



  1. ❌ 认为std::sort稳定,依赖相等元素的原有顺序。


✅std::sort不保证稳定;需要稳定时用std::stable_sort。



  1. ❌ 对std::list调用std::sort。


✅std::sort要求随机访问迭代器;链表用成员函数sort()。



  1. ❌ 快排分区后不移动指针,遇到重复元素死循环。


✅ 交换后++i、--j,确保区间每轮收缩。



  1. ❌ 快排内层扫描不检查边界,基准为极值时越界。


✅ 加i <= hi、j >= lo;或用三路划分与内省策略。



  1. ❌ 归并时用a[j] <= a[i]判断取右边。


✅ 相等时先取左边,才是稳定排序;严格小于才取右边。



  1. ❌ 手写归并每层递归都新建临时数组。


✅ 预分配一块与输入等长的 buffer,递归中复用。



  1. ❌ 用std::sort排std::vector<bool>后对其元素取地址。


✅vector<bool>是特化代理引用,不提供真实元素地址;需要地址时换vector<char>或deque<bool>。


总结




概念要点



std::sort内省排序,最坏 O(N log N),不稳定

std::stable_sort稳定,通常需要 O(N) 辅助空间

严格弱序比较器必须非自反、非对称、可传递

快排平均 O(N log N),最坏 O(N²),原地但不稳定

归并稳定 O(N log N),需要 O(N) 辅助空间

稳定性相等元素相对顺序是否保持



排序算法的选择本质是三个约束的权衡:时间上界、额外空间、稳定性。标准库已经把通用场景覆盖得很好,手写实现的价值在于理解分治与合并的边界条件——尤其是「指针怎么走、区间怎么收」这两件事,几乎所有快排 bug 都出在这里。




相关新闻

Comfyui和Wan2.1 在 Ascend NPU 上的 算子问题

Comfyui和Wan2.1 在 Ascend NPU 上的 算子问题

日期&#xff1a;2026-10-18 环境&#xff1a;B 机&#xff08;Atlas 800IA2, Ascend 910B4, CANN torch_npu 26.x&#xff09; 目标&#xff1a;让 WanVideoWrapper&#xff08;kijai/ComfyUI-WanVideoWrapper&#xff09;在 NPU 上跑文生视频一、问题在华为Asecnd NPU 800A2…

2026/10/20 2:25:29 阅读更多 →
Linux 上的交叉编译

Linux 上的交叉编译

1. C/C 编译的四个步骤 从源代码到可执行文件&#xff0c;通常分为&#xff1a;预处理 → 编译 → 汇编 → 链接。 1.1 预处理&#xff08;Preprocessing&#xff09; 处理以 # 开头的预处理指令&#xff0c;例如展开 #include 引入的头文件、替换宏、处理条件编译&#xff0…

2026/10/20 2:24:28 阅读更多 →
彻底禁用Chrome和Edge自动更新的三步实操指南

彻底禁用Chrome和Edge自动更新的三步实操指南

最近好几个朋友跑来问我&#xff0c;说系统里的浏览器老是自己偷偷升级&#xff0c;明明是个小版本更新&#xff0c;结果重启之后一堆插件不能用、界面换了、甚至还出现过打不开网页的情况。网上一搜“禁用更新”能找到一堆教程&#xff0c;照着改完服务、关掉任务计划&#xf…

2026/10/20 2:24:28 阅读更多 →

最新新闻

模型量化如何影响推理性能?实战解析原理、选型与问题排查

模型量化如何影响推理性能?实战解析原理、选型与问题排查

在AI推理优化这条路上&#xff0c;量化是我见过投入产出比最高的手段。很多人以为量化就是把模型从FP32变成INT8&#xff0c;体积缩小、推理变快&#xff0c;但实操后才发现&#xff0c;AI模型量化对推理性能的影响并不只有正向&#xff1a;有时速度反而下降&#xff0c;精度却…

2026/10/20 20:20:51 阅读更多 →
基于Simulink的23自由度分布式电驱动整车动力学模型搭建与仿真

基于Simulink的23自由度分布式电驱动整车动力学模型搭建与仿真

做分布式电驱动车辆的控制&#xff0c;最头疼的问题就是你手上的整车模型到底该建多细。我这次在Matlab Simulink里完整搭了一套23自由度整车动力学模型&#xff0c;把车身六个方向的运动、四个车轮的垂向跳动、车轮旋转、转向系统、以及分布式驱动链条的扭转自由度全部拉进来了…

2026/10/20 20:20:51 阅读更多 →
用JSON和Python命令行构建个人技能管理工具:从数据建模到实践

用JSON和Python命令行构建个人技能管理工具:从数据建模到实践

1. 为什么需要技能管理工具&#xff1a;需求拆解与设计取舍上周末想更新简历&#xff0c;结果发现我根本说不清“现在到底会什么”。十几项技能分散在旧简历、脑图、收藏夹和一摞笔记本里&#xff0c;有的早已过期&#xff0c;有的只停留在“看过教程”的阶段&#xff0c;还有的…

2026/10/20 20:20:51 阅读更多 →
企业微信回调验证与AES加解密:.NET开发者接入指南

企业微信回调验证与AES加解密:.NET开发者接入指南

接手过几个企业微信自建应用的项目&#xff0c;每次新人对接时几乎都会卡在同一个地方&#xff1a;管理后台保存“接收消息服务器”配置时提示URL验证失败&#xff0c;或者明明验证过了&#xff0c;消息却一直推送不进来。这个环节看着只是填几个参数&#xff0c;背后实际上是一…

2026/10/20 20:20:51 阅读更多 →
vLLM Prefix Caching 原理:如何用 Paged KV 消除重复计算

vLLM Prefix Caching 原理:如何用 Paged KV 消除重复计算

先说一个我遇到过的情况。某个线上推理服务&#xff0c;所有请求都带着同一段两千多字的 system prompt&#xff0c;并发一上来&#xff0c;GPU 利用率看着很高&#xff0c;但真正对用户有用的计算没多少——大部分算力在一遍遍重复计算同一段内容的 K 和 V。很长一段时间里&am…

2026/10/20 20:20:51 阅读更多 →
用Design.md构建符合产品风格的原型设计:设计令牌实战指南

用Design.md构建符合产品风格的原型设计:设计令牌实战指南

上周跟某团队聊原型评审&#xff0c;对方把三个页面的设计稿拍在我面前&#xff0c;主色蓝就出现了五种&#xff0c;没有两个颜色值相同。评审会开了一个小时&#xff0c;快一半时间在争论“这个蓝到底是不是我们品牌那个蓝”。原型阶段最让人心累的从来不是画得快不快&#xf…

2026/10/20 20:19:47 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →