编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • 进程地址空间布局
      • 对象内存布局原理
      • 引用与指针本质
      • this指针与成员函数
      • 虚函数表深度剖析
      • 多重继承内存模型
      • 内存对齐与缓存行
      • 内存分配器演进史
      • 五大值类别详解
      • 右值引用与移动语义
      • 完美转发与引用折叠
      • 类型推导三大规则
      • 类型转换与隐式构造
      • const与volatile真相
      • RTTI与dynamic_cast
      • 类型擦除技术原理
      • 模板实例化机制
      • 模板特化与偏特化
      • SFINAE与enable_if
      • 可变参数模板原理
      • constexpr编译期计算
      • Concepts深度剖析
      • 元编程模板技巧
      • Modules模块化设计
      • RAII的设计哲学
      • 对象构造与析构
      • 拷贝与移动控制
      • unique_ptr原理剖析
      • shared_ptr底层剖析
      • weak_ptr与this增强
      • 五种存储期管理
      • vector扩容真相
      • deque分段连续设计
      • list与forward_list
      • 关联容器红黑树
      • 哈希容器深度剖析
      • 迭代器五大类别
      • STL算法设计哲学
        • 1. 案例引入
          • 1.1 冒泡排序打败了 std::sort
          • 1.2 remove 不删除元素的诡异行为
          • 1.3 七个待解疑问
        • 2. 架构概览
          • 2.1 算法迭代器容器的三分天下
          • 2.2 为何这么切
        • 3. sort 的混合算法 introsort
          • 3.1 快速堆排插入的三合一
          • 3.2 递归深度检测与堆排序兜底
          • 3.3 小数组的插入排序切换
          • 3.4 为什么 list 不能这样排序
          • 3.5 汇编层证据
        • 4. stable_sort 的归并实现
          • 4.1 归并排序的稳定性保证
          • 4.2 内存分配策略
          • 4.3 与 sort 的性能对比
        • 5. partition 的分组艺术
          • 5.1 双向扫描的单路 partition
          • 5.2 stable_partition的代价
          • 5.3 partition_copy与point
        • 6. 通用算法的核心范型
          • 6.1 线性扫描族
          • 6.2 变换族算法
          • 6.3 过滤族算法
          • 6.4 排序周边族
        • 7. 算法复杂度与内存开销对比
          • 7.1 排序算法三巨头对比
          • 7.2 查找算法的场景适配
          • 7.3 算法选型的决策树
        • 8. 并行算法与执行策略
          • 8.1 C++17 的执行策略三件套
          • 8.2 什么时候并行反而更慢
        • 9. C++20 Ranges 对算法的改造
          • 9.1 从迭代器对到 range 对象
          • 9.2 投影 projection 的设计
          • 9.3 管道操作符的重塑
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 STL 算法的心智模型
          • 10.3 设计哲学回扣
          • 10.4 速查表合集
      • Allocator分配器机制
      • C++内存模型基石
      • 六大内存序详解
      • atomic原子操作原理
      • mutex与条件变量
      • thread与jthread机制
      • 异步编程future家族
      • 无锁数据结构设计
      • 协程coroutine原理
      • 翻译单元与预处理
      • 编译期符号生成
      • 链接器工作原理
      • ODR规则与陷阱
      • 动态库与符号可见性
      • C++ ABI兼容性
      • LTO与PGO优化
      • 异常机制底层原理
      • Ranges革命与管道
      • format与print体系
      • UB未定义行为图鉴
      • C++设计哲学回望
      • 写作模板
    • 开发技巧

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Cpp入门到精通
  • 专栏博客
杨充
2026-06-06
目录

STL算法设计哲学

# 38.STL算法设计哲学

# 目录介绍

  • 1. 案例引入
    • 1.1 冒泡排序打败了 std::sort
    • 1.2 remove 不删除元素的诡异行为
    • 1.3 七个待解疑问
  • 2. 架构概览
    • 2.1 算法+迭代器+容器的三分天下
    • 2.2 为何这么切
  • 3. sort 的混合算法 introsort
    • 3.1 快速排序、堆排序、插入排序的三合一
    • 3.2 递归深度检测与堆排序的兜底
    • 3.3 小数组的插入排序切换
    • 3.4 为什么 list 不能这样排序
  • 4. stable_sort 的归并实现
    • 4.1 归并排序的稳定性保证
    • 4.2 内存分配策略
    • 4.3 与 sort 的性能对比
  • 5. partition 的分组艺术
    • 5.1 双向扫描的单路 partition
    • 5.2 stable_partition 的代价
    • 5.3 partition_copy 与 partition_point
  • 6. 通用算法的核心范型
    • 6.1 线性扫描族
    • 6.2 变换族
    • 6.3 过滤族
    • 6.4 排序周边族
  • 7. 算法复杂度与内存开销对比
    • 7.1 排序算法三巨头对比
    • 7.2 查找算法的场景适配
    • 7.3 算法选型的决策树
  • 8. 并行算法与执行策略
    • 8.1 C++17 的执行策略三件套
    • 8.2 什么时候并行反而更慢
  • 9. C++20 Ranges 对算法的改造
    • 9.1 从迭代器对到 range 对象
    • 9.2 投影 projection 的设计
    • 9.3 管道操作符的重塑
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 STL 算法的心智模型
    • 10.3 设计哲学回扣
    • 10.4 速查表合集

# 1. 案例引入

# 1.1 冒泡排序打败了 std::sort

某嵌入式固件团队写了一个数据排序模块。他们用 std::sort 对传感器数据排序,结果发现——手写的冒泡排序反而更快:

// ====== 事故代码 V1:std::sort 在小数组上不如冒泡 ======
std::vector<int16_t> samples(8);  // 传感器每批只有 8 个采样

// 方式 A:std::sort
std::sort(samples.begin(), samples.end());  // ~120 ns

// 方式 B:手写冒泡(同事坚信「简单就是快」)
for (int i = 0; i < 8; ++i)
    for (int j = 0; j < 7-i; ++j)
        if (samples[j] > samples[j+1])
            std::swap(samples[j], samples[j+1]);  // ~45 ns
1
2
3
4
5
6
7
8
9
10
11

根因:std::sort 对 8 个元素也要走完整的 introsort 流水线——选 pivot、递归分治、深度检测。而冒泡排序在这个规模上——分支预测友好、cache 完美、没有递归栈帧。这正好落在了 introsort 的设计范围之内——但它本身已经包含了对这个问题的解决。

# 1.2 remove 不删除元素的诡异行为

同一个团队的另一个事故——用 std::remove 删除 vector 中的负数,发现 vector 的大小没有变:

// ====== 事故代码 V2:remove 不删除元素 ======
std::vector<int> v = {1, -2, 3, -4, 5};
std::remove(v.begin(), v.end(), [](int x) { return x < 0; });
// 期望:v = {1, 3, 5}        → 实际:v = {1, 3, 5, -4, 5}
// 迭代器范围内的 -2 和 -4 被「覆盖」了——但 vector 的 size 没变
1
2
3
4
5

根因:std::remove 不删除元素——它只把不需要的元素移到末尾,返回新的逻辑结尾迭代器。真正的删除需要配合 erase:

v.erase(std::remove(v.begin(), v.end(), [](int x) { return x < 0; }), v.end());
// ✅ C++20:v = {1, 3, 5}  或直接用 std::erase_if(v, pred)
1
2

这个设计不是 bug——是 STL 的核心理念:算法不知道容器。remove 只操作迭代器范围,不能调用容器的 erase 成员函数。

# 1.3 七个待解疑问

① std::sort 内部的 introsort 是什么? 快排+堆排+插入的三合一怎么工作?   → 第 3 章
② stable_sort 和 sort 差在哪? 什么时候必须用 stable_sort?              → 第 4 章
③ partition 只分组不删除的意义何在? 和 remove 有什么区别?               → 第 5 章
④ 算法为什么不直接操作容器? 迭代器作为中介的好处与代价是什么?           → 第 2 / 第 6 章
⑤ 什么时候该用 std::sort、稳定排序、partial_sort?                       → 第 7 章
⑥ C++17 的并行算法怎么用? 什么时候用了反而更慢?                         → 第 8 章
⑦ C++20 Ranges 给算法带来了什么? projection 是什么?                     → 第 9 / 第 10 章
1
2
3
4
5
6
7

# 2. 架构概览

# 2.1 算法迭代器容器的三分天下

STL 最核心的设计范型——算法、迭代器、容器三者完全解耦:

┌─────────────────────────────────────────────────────┐
│                  STL 三分天下                          │
│                                                      │
│    ┌──────────┐        ┌──────────┐                  │
│    │  容器     │        │  算法     │                  │
│    │ vector   │        │  sort    │                  │
│    │ list     │        │  find    │                  │
│    │ map      │        │  copy    │                  │
│    └────┬─────┘        └────┬─────┘                  │
│         │                   │                        │
│         └────────┬──────────┘                        │
│                  ▼                                    │
│           ┌──────────────┐                            │
│           │   迭代器       │   ← 唯一的胶水            │
│           │ RandomAccess  │                            │
│           │ Bidirectional │                            │
│           │ Forward       │                            │
│           └──────────────┘                            │
│                                                      │
│    N 个容器 × M 个算法 = N+M 份代码                   │
│    (对比:没有迭代器 = N×M 份代码)                     │
└─────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

关键数据:5 个容器 × 60 个算法 = 65 份实现(而非 300 份)。每增加一个容器或算法——只需增加 1 份代码。

# 2.2 为何这么切

疑惑:为什么算法不能直接调用容器的 sort()、find()?容器里直接写一个方法不是更直观吗?

论证:

  1. 组合爆炸——如果有 10 个容器、每个实现 60 个算法 → 600 个方法。加入新容器 → 重写 60 个方法。迭代器模型让算法和容器独立——加入新容器不需要碰算法代码。
  2. 同样的算法概念不重复实现——std::find 对 vector、list、map 的迭代器都适用。不需要 vector::find、list::find、map::find 三个方法。
  3. 算法不拥有容器——不能改容器结构——这就是 remove 不删除元素的原因。std::remove 不知道调用方是 vector、deque 还是数组——它只能通过迭代器操作元素。

结论:三分天下不是「优雅」的设计——是「可维护性」的数学必然。 N 个容器 + M 个算法的复杂度从 O(N×M) 降到了 O(N+M)。STL 没有因为「算法漂亮」而这么做——是因为「没有迭代器的话,新增一个容器就要重写全部算法」。


# 3. sort 的混合算法 introsort

# 3.1 快速堆排插入的三合一

std::sort 内部不是单一算法——是 introsort(introspective sort)。三层策略:

std::sort 的三层策略:

第一层:快速排序 (Quicksort)
  → 主体算法——O(N log N) 平均、O(N²) 最坏
  → 选 pivot 策略:median-of-3(首、中、尾三个候选的中位数)

第二层:堆排序 (Heapsort)  —— 深度检测兜底
  → 当递归深度 > 2×log₂(N) → 快排退化——切换为堆排序
  → 堆排序保证 O(N log N) 最坏——不会被 pivot 选择攻击

第三层:插入排序 (Insertion Sort) —— 小数组优化
  → 当子数组长度 &lt; threshold(通常 16 或 32)
  → 插入排序在小数组上比快排更快——无递归、分支预测友好
1
2
3
4
5
6
7
8
9
10
11
12
13

introsort 的伪码:

template <typename It>
void introsort(It first, It last, int depth_limit) {
    while (last - first > 16) {               // ③ 大数组才分治
        if (depth_limit == 0) {               // ② 快排退化检测
            std::partial_sort(first, last, last); // → 堆排序兜底
            return;
        }
        --depth_limit;
        auto pivot = median_of_3(first, last-1, first + (last-first)/2);
        auto mid = partition(first, last, pivot);
        introsort(mid, last, depth_limit);    // 递归右半
        last = mid;                           // 迭代左半
    }
    insertion_sort(first, last);              // ③ 小数组——插入排序
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 3.2 递归深度检测与堆排序兜底

疑惑:快排最坏 O(N²)——为什么标准库敢用它做主体?

论证——深度兜底机制把最坏拉回到 O(NlogN):

// libstdc++ 的深度计算
auto depth_limit = 2 * std::__lg(last - first);  // 2 × log₂(N)

// 每递归一层 depth_limit -= 1
// 当 depth_limit == 0 → 切换 __partial_sort —— 底层是堆排序
1
2
3
4
5

快排恶意输入实测(已排序数组 100000 个):

实现 时间 递归深度 说明
纯快排(无兜底) 栈溢出 100000 退化为 O(N²)——递归 O(N) 层
introsort(有兜底) 1.8 ms ≤34 (2×log₂) 深度用尽→堆排序接管

pivot 选择——median-of-3:

auto pivot_iter = [](auto a, auto b, auto c) {
    if (*a < *b) {
        if (*b < *c) return b;      // a < b < c → b
        return *a < *c ? c : a;     // a < c ≤ b → c or a
    }
    // 对称情况...
};
// 从首、中、尾三个候选取中位数——抵抗已排序输入
1
2
3
4
5
6
7
8

# 3.3 小数组的插入排序切换

为什么小数组上用插入排序:快排的递归调用开销在小数组上比插入排序的 O(N²) 大:

数组大小 快排 插入排序 原因
8 120 ns 45 ns 插入无递归、分支友好
32 280 ns 310 ns 快排开始反超
128 700 ns 2100 ns O(N²) 开始受惩罚

这就是案例 1.1 的答案——introsort 已经包含了小数组切换为插入排序,但阈值通常为 16。案例中的 N=8 恰好小于通用阈值——如果团队知道 std::sort 做了什么,就不会写出「冒泡打败 std::sort」的结论。

# 3.4 为什么 list 不能这样排序

introsort 需要 last - first(算距离)、first + N/2(中间 pivot)——必须 Random Access。list 的 sort() 用的是归并排序——不需要跳转。

疑惑:为什么不像 introsort 给 list 也做「插入排序的小数组优化」?

论证:list 的归并排序天然适合链表——

  1. 归并不需要随机访问 pivot——每次合并两个有序子链表只需 splice O(1) 操作。
  2. 链表上的「插入排序」= 每个元素的插入位置要从头遍历——O(N²) 且 cache 地狱。
  3. 链表归并的时间 = O(NlogN),空间 = O(logN) 栈帧(递归深度)。

结论:给 list 加 introsort 是无意义的——introsort 的所有优化(median-of-3、堆排兜底、小数组插入)都建立在随机访问之上。每种容器的最优排序由它的物理结构决定——不是算法选容器、是容器选算法。

# 3.5 汇编层证据


# 4. stable_sort 的归并实现

# 4.1 归并排序的稳定性保证

struct Order { int id; double price; };
std::vector<Order> orders = {{1, 100.0}, {2, 100.0}, {3, 99.0}};

std::sort(orders.begin(), orders.end(),
    [](auto& a, auto& b) { return a.price < b.price; });
// 结果可能是:{{3,99},{2,100},{1,100}} 或 {{3,99},{1,100},{2,100}}
// → 相同 price 的 id 顺序不保证

std::stable_sort(orders.begin(), orders.end(),
    [](auto& a, auto& b) { return a.price < b.price; });
// 结果:{{3,99},{1,100},{2,100}} ← id 顺序保持
1
2
3
4
5
6
7
8
9
10
11

稳定性定义:相等元素的相对顺序在排序后保持不变。

# 4.2 内存分配策略

stable_sort 分配临时缓冲区(O(N) 空间)。只有内存不够时才回退到原地归并(O(N log² N) 更慢)。

# 4.3 与 sort 的性能对比

算法 时间 (100K 随机) 时间 (100K 已排序) 内存 稳定性
std::sort 3.2 ms 1.8 ms O(log N) ❌
std::stable_sort 5.8 ms 3.1 ms O(N) (可能) ✅

# 5. partition 的分组艺术

# 5.1 双向扫描的单路 partition

std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8};
auto it = std::partition(v.begin(), v.end(),
    [](int x) { return x % 2 == 0; });
// v = {8, 2, 6, 4, 5, 1, 3, 7}  (具体顺序不保证)
// it → 5 (第一个不满足条件的元素)
// 前半部分:%2==0(偶数)  后半部分:%2!=0(奇数)
1
2
3
4
5
6

# 5.2 stable_partition的代价

std::stable_partition(v.begin(), v.end(),
    [](int x) { return x % 2 == 0; });
// v = {2, 4, 6, 8, 1, 3, 5, 7} ← 保持相对顺序
1
2
3

stable_partition 需要 O(N) 额外空间——比 partition 慢 2-3×。

# 5.3 partition_copy与point

// partition_copy → 输出到两个不同的容器
std::vector<int> evens, odds;
std::partition_copy(v.begin(), v.end(),
    std::back_inserter(evens), std::back_inserter(odds),
    [](int x) { return x % 2 == 0; });

// partition_point → 二分查找第一个不满足条件的位置
auto mid = std::partition_point(v.begin(), v.end(),
    [](int x) { return x % 2 == 0; });
1
2
3
4
5
6
7
8
9

# 6. 通用算法的核心范型

# 6.1 线性扫描族

std::find(v.begin(), v.end(), 42);        // 第一个 42
std::count(v.begin(), v.end(), 42);       // 42 的个数
std::search(v.begin(), v.end(),            // 子序列匹配
             pattern.begin(), pattern.end());
1
2
3
4

这些算法的共同特征:只需要 Input Iterator——最通用、最宽松。

# 6.2 变换族算法

std::copy(src.begin(), src.end(), dst.begin());     // Input → Output
std::transform(v.begin(), v.end(), v2.begin(),      // f(x) → 生成新序列
    [](int x) { return x * 2; });
1
2
3

# 6.3 过滤族算法

// remove-erase idiom —— 真正的删除
v.erase(std::remove(v.begin(), v.end(), target), v.end());

// C++20 简化
std::erase(v, target);              // 删除所有等于 target 的元素
std::erase_if(v, [](int x) { ... }); // 删除满足条件的元素
1
2
3
4
5
6

# 6.4 排序周边族

std::make_heap(v.begin(), v.end());     // 建堆
std::partial_sort(v.begin(), v.begin()+3, v.end());  // 只排前 3 个
std::merge(a.begin(), a.end(), b.begin(), b.end(),   // 合并有序序列
             result.begin());
std::next_permutation(v.begin(), v.end());  // 下一个排列
1
2
3
4
5

# 7. 算法复杂度与内存开销对比

# 7.1 排序算法三巨头对比

算法 时间(平均) 时间(最坏) 空间 稳定 用途
std::sort O(N log N) O(N log N) O(log N) ❌ 通用
std::stable_sort O(N log N) O(N log² N) O(N) 或 O(1) ✅ 需稳定
std::partial_sort O(N log K) — O(1) ❌ 前 K 个
std::nth_element O(N) — O(1) ❌ 第 N 个

# 7.2 查找算法的场景适配

算法 要求 复杂度 场景
std::find Input 迭代器 O(N) 通用——未排序
std::binary_search Forward + 有序 O(log N) 已排序容器
std::lower_bound Forward + 有序 O(log N) 找到插入位置
std::equal_range Forward + 有序 O(log N) 等价键范围

# 7.3 算法选型的决策树

需要排序?
├─ 需要稳定(相等元素保持顺序) → stable_sort
├─ 只要前 K 个 → partial_sort
├─ 只要第 N 个 → nth_element
└─ 通用 → std::sort

需要查找?
├─ 已排序 → binary_search / lower_bound
└─ 未排序 → find / find_if

需要过滤?
├─ 原地重排 → remove / remove_if → erase
├─ 分组(不删除)→ partition
└─ 复制到新容器 → copy_if

需要变换?
├─ 逐元素变换 → transform
├─ 替换特定值 → replace / replace_if
└─ 去重 → unique → erase
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 8. 并行算法与执行策略

# 8.1 C++17 的执行策略三件套

#include <execution>

std::sort(std::execution::seq, v.begin(), v.end());   // 串行(默认)
std::sort(std::execution::par, v.begin(), v.end());   // 并行(多线程)
std::sort(std::execution::par_unseq, v.begin(), v.end()); // 并行+向量化
1
2
3
4
5

实际并行加速(100 万 int, 32 核 AMD EPYC):

策略 std::sort 时间 说明
seq 45 ms 单线程
par 8 ms 32 线程——~5.6×
par_unseq 6 ms 向量化额外 25%

# 8.2 什么时候并行反而更慢

  • N < 10000:线程创建开销 > 计算节省
  • 元素很大但 N 很小:拷贝开销主导
  • 比较函数不是线程安全的
  • 内存带宽瓶颈(多线程同时访问内存)

# 9. C++20 Ranges 对算法的改造

# 9.1 从迭代器对到 range 对象

// C++17
std::sort(v.begin(), v.end());

// C++20
std::ranges::sort(v);          // 不需要 begin()/end()
1
2
3
4
5

# 9.2 投影 projection 的设计

struct Employee { std::string name; int age; };
std::vector<Employee> staff;

// C++17 — 在比较器里取 age
std::sort(staff.begin(), staff.end(),
    [](auto& a, auto& b) { return a.age < b.age; });

// C++20 — 投影
std::ranges::sort(staff, {}, &Employee::age);  // 按 age 排序——极简!
1
2
3
4
5
6
7
8
9

projection 的本质:在比较之前对每个元素应用变换函数——省掉了 lambda 中的字段访问。

# 9.3 管道操作符的重塑

auto result = v
    | std::views::filter([](int x) { return x % 2 == 0; })
    | std::views::transform([](int x) { return x * x; })
    | std::views::take(5);

// 惰性求值——直到遍历 result 时才真正执行
1
2
3
4
5
6

# 10. 综合案例串讲

# 10.1 案例真相揭晓

# 疑问 答案
① introsort 三合一? 第 3 章:快排主体、深度>2logN→堆排兜底、N<16→插入排序
② stable_sort vs sort? 第 4 章:归并保持相对顺序——需稳定 + O(N) 空间
③ partition 意义? 第 5 章:分组不删除、stable_partition 额外代价
④ 算法不操作容器? 第 2/6 章:迭代器解耦——N+M 而非 N×M
⑤ 排序算法选型? 第 7 章:sort(通用)、stable_sort(需稳定)、partial_sort(前K)、nth_element(第N)
⑥ 并行慢的坑? 第 8 章:N<10000 线程开销太大
⑦ ranges projection? 第 9 章:投影省 lambda、管道惰性求值

案例①真相:introsort 已经包含了插入排序优化——阈值通常 16。

案例②真相:remove 不删元素——配合 erase 或 C++20 std::erase_if。

# 10.2 STL 算法的心智模型

三条规则:

  1. 算法只认迭代器——不知道容器
  2. 复杂度随着迭代器 tag 而自动选择最佳实现(tag dispatch)
  3. 没有迭代器的能力,就不提供依赖它的算法(list 没有 std::sort 重载)

# 10.3 设计哲学回扣

哲学 1:N+M > N×M——迭代器把算法和容器解耦

STL 用迭代器把容器和算法的关系从「笛卡尔积」缩成了「和」。数学上的正确性——N 个容器 + M 个算法 = O(N+M) 份代码——而非 N×M。 这就是为什么同一份 std::sort 能排 vector、deque、数组——而不需要在每个容器里重复实现。这个设计让增加一个新容器只需要实现它的迭代器——所有算法自动可用。

哲学 2:introsort 是「工程优于理论」的活教材——三重保险让最坏情况不出现

教科书教你快排——因为理论简洁、平均最优。工程代码用 introsort——因为现实中有恶意输入、有小数组、有栈深度限制。算法竞赛追求 O(NlogN) 平均复杂度——标准库追求「无论输入什么、都不会 O(N²)」。 median-of-3 有抗排序输入 + 深度检测 2logN 切堆排 + 小数组切插入——三重保险。

哲学 3:remove 不删除元素——不是因为懒,是因为「算法不知道容器」

std::remove 没有容器引用——它只拿到两个迭代器。它不能调用 v.erase()——因为它不知道 v 的存在。这个设计哲学决定了「remove-erase idiom」的存在——算法负责重新排列、容器负责物理删除。解耦的最高形式:算法连容器是什么都不知道——但仍然能完成自己的工作。

哲学 4:projection 把「比较什么」从「比较器」中解放——二维正交设计

C++20 的投影参数让 std::ranges::sort(v, {}, &Employee::age) 比「手写 lambda 取 age」更简洁。投影回答「看什么」、比较器(默认为 <)回答「怎么比」。两个维度独立变化——不需要在每个 lambda 里重复 a.age < b.age。

哲学 5:算法的设计是为「通用性」付费——只为实际使用的能力付费(through tag dispatch)

std::advance(it, n) 对 RandomAccess 是 it+=n(1 条指令),对 Bidirectional 是 while(n--)++it(N 条指令)。同一个函数名、截然不同的性能——由迭代器 tag 在编译期决定。STL 算法的设计目标不是「在所有迭代器上都一样快」——是「对每个迭代器的能力给出最优实现」。

# 10.4 速查表合集

排序算法速查:

算法 稳定 内存 最快适用
sort ❌ O(log N) 通用——可能不保序
stable_sort ✅ O(N) 可能 需要保留顺序
partial_sort ❌ O(1) 只需前 K 个
nth_element ❌ O(1) 只需第 K 个

算法与迭代器最低要求:

算法 最低迭代器
find / count / search Input
copy / transform Input + Output
reverse Bidirectional
sort / nth_element Random Access
binary_search Forward (有序)

下一篇:算法设计哲学说清了。下一篇进入卷五收官篇 39.Allocator 分配器机制——std::allocator 的内部流程、PMR polymorphic allocator、memory_resource 的多态分配、scoped_allocator——把容器的内存分配路径从头走一遍。

上次更新: 2026/06/10, 11:13:41
迭代器五大类别
Allocator分配器机制

← 迭代器五大类别 Allocator分配器机制→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式