在C++的生态系统中,STL(Standard Template Library)算法是核心竞争力之一,它不仅仅是一组函数,更是C++标准委员会经过数十年验证的性能最优解。当我们谈论STL算法时,我们谈论的不仅是效率,更是可维护性与标准化的三位一体。
本文将从 STL 算法的底层逻辑出发,覆盖从基础用法到前沿特性的全维度知识,帮你真正掌握 "用 STL 解决问题" 的核心能力。
Part1STL 算法生态定位与价值
你是否曾为手写二分查找调试过 "边界值漏判"?
是否在实现排序时纠结 "快速排序的栈溢出风险"?
STL 算法的出现正是为了解决这些问题 ------ 它由 C++ 标准委员会主导,经过无数开发者验证,不仅封装了成熟的算法逻辑,更通过泛型设计适配几乎所有 STL 容器。
例如,对 vector<int> 排序,手写快排需要约 50 行代码,而 STL 只需:
#include <algorithm>
#include <vector>
int main() {
std::vector<int> vec = {3, 1, 4, 1, 5};
std::sort(vec.begin(), vec.end()); // 一行搞定,O(n log n) 复杂度
return 0;
}
1.1、STL 算法的演进与现代生态
STL 算法并非一成不变:从 C++98 首次引入基础算法(如 find/sort),到 C++11 加入移动语义(std::move 算法),再到 C++17 支持并行计算(std::execution::par),直至 C++20 Ranges 库重构算法调用范式,每一次迭代都紧跟开发需求。
如今,STL 算法已成为现代 C++ 生态的核心组件 ------ 它与 Ranges 库互补(Ranges 简化调用,STL 保证性能),甚至能与 GPU 加速框架(如 NVIDIA CUDA)结合,应对异构计算场景。
1.2、本文阅读指南
本文面向 C++ 进阶开发者,需具备基础语法、STL 容器(vector/list/map)与迭代器概念。阅读路径建议:
- 先掌握 "底层基石"(迭代器 / 函数对象),理解算法的设计逻辑;
- 再按 "算法类别"(非修改 / 修改 / 排序 / 数值)逐个突破,结合代码实践;
- 最后通过 "实战案例" 与 "陷阱避坑" 巩固应用能力。
Part2底层基石
STL 算法的灵活性源于 "算法 - 迭代器 - 函数对象" 的三角架构:迭代器充当算法与容器的桥梁,函数对象为算法注入自定义逻辑,三者协同实现 "与容器解耦、与逻辑适配" 的泛型设计。
2.1、迭代器:算法与容器的适配桥梁
迭代器本质是 "泛化的指针",它屏蔽了不同容器的内存布局差异(如 vector 的连续内存、list 的链表节点),让算法能统一操作容器元素。C++ 标准将迭代器分为 5 类,每类支持的操作不同,直接决定算法的适用性:
|---------|---------------------------|--------------------|--------------------------|
| 迭代器类型 | 支持操作 | 典型容器 | 适配算法示例 |
| 输入迭代器 | 只读、单向移动(++) | istream_iterator | find/count |
| 输出迭代器 | 只写、单向移动(++) | ostream_iterator | copy/fill |
| 前向迭代器 | 可读可写、单向移动、可重复访问 | forward_list | replace/generate |
| 双向迭代器 | 可读可写、双向移动(++/--) | list/map | reverse/prev_permutation |
| 随机访问迭代器 | 双向迭代器所有操作 + 随机访问([]/+-) | vector/deque/array | sort/binary_search |
关键陷阱:迭代器类型不匹配导致的错误
例如,std::sort 要求随机访问迭代器(需支持随机跳转以优化排序),若误用在 list 上(仅支持双向迭代器),编译会直接报错:
#include <algorithm>
#include <list>
int main() {
std::list<int> lst = {3, 1, 4};
// 错误:list::iterator 是双向迭代器,不支持 sort 所需的随机访问
std::sort(lst.begin(), lst.end());
return 0;
}
// 编译报错(GCC):error: no matching function for call to 'sort(std::list<int>::iterator, std::list<int>::iterator)'
正确做法是使用 list 自带的 sort 成员函数(专为双向迭代器优化):
lst.sort(); // 正确,list 成员函数支持双向迭代器
迭代器失效:算法调用中的 "隐形炸弹"
当容器修改(如 vector 扩容、list 插入节点)时,原有迭代器可能失效(指向已释放内存),若算法继续使用失效迭代器,会导致未定义行为(崩溃或脏数据)。
例如,vector 扩容后 copy 失效迭代器:
#include <algorithm>
#include <vector>
int main() {
std::vector<int> src = {1, 2, 3};
std::vector<int> dst;
// 错误:dst 为空,begin() 迭代器后续会因扩容失效
std::copy(src.begin(), src.end(), dst.begin());
return 0;
}
正确做法是用插入迭代器(back_inserter)自动处理扩容:
#include <iterator> // 包含 back_inserter
std::copy(src.begin(), src.end(), std::back_inserter(dst)); // 正确,自动调用 push_back
2.2、函数对象:算法的可定制化接口
函数对象(包括函数指针、Lambda、标准函数对象)是算法的 "逻辑扩展点"------ 它让算法摆脱固定逻辑,支持自定义判断(如 "查找符合条件的元素")或操作(如 "自定义排序规则")。
1). 谓词:算法的 "判断逻辑"
谓词是返回 bool 的函数对象,分为一元谓词(接收 1 个参数)和二元谓词(接收 2 个参数):
- 一元谓词:如 find_if 中的 "元素是否大于 10";
- 二元谓词:如 sort 中的 "元素 a 是否大于元素 b"。
示例:用 Lambda 作为谓词,查找 vector 中大于 10 的元素:
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {5, 12, 8, 15, 3};
// 一元谓词:[](int x) { return x > 10; }
auto it = std::find_if(vec.begin(), vec.end(), [](int x) {
return x > 10;
});
if (it != vec.end()) {
std::cout << "找到第一个大于 10 的元素:" << *it << std::endl; // 输出 12
}
return 0;
}
2). 标准函数对象:避免重复造轮子
STL 提供 std::less/std::greater/std::plus 等标准函数对象,可直接适配算法。例如,用 std::greater 实现降序排序:
#include <algorithm>
#include <vector>
#include <functional> // 包含 std::greater
int main() {
std::vector<int> vec = {3, 1, 4};
// 二元谓词:std::greater<int>(),表示"a > b"
std::sort(vec.begin(), vec.end(), std::greater<int>());
// 此时 vec 为 [4, 3, 1]
return 0;
}
3). Lambda 捕获的线程安全与内存开销
Lambda 捕获外部变量时需注意两点:
- 线程安全 :多线程环境下,捕获非线程安全的变量(如普通 int )可能导致数据竞争,建议用 std::mutex 保护,或捕获 const 变量(只读无竞争);
- 内存开销 :值捕获( [x] )会拷贝变量,若变量是大对象(如 std::string ),开销较高,此时建议用引用捕获( [&x] ),但需确保变量生命周期覆盖 Lambda 执行。
2.3、算法设计哲学:泛型、解耦与复杂度承诺
1). 泛型与解耦:算法不直接操作容器
STL 算法通过迭代器间接访问容器,而非直接操作容器对象 ------ 这意味着算法无需关心容器的具体类型(vector/list/array),只需依赖迭代器提供的接口。这种解耦设计让算法的复用性达到极致:同一个 std::sort 既能排序 vector<int>,也能排序 array<double>。
2). 复杂度承诺:C++ 标准的 "性能保证"
C++ 标准为每个算法明确了时间 / 空间复杂度,开发者可放心依赖:
- find / count :O (n)(线性遍历);
- sort :O (n log n)(内省排序,最坏情况也是 O (n log n));
- binary_search :O (log n)(二分查找)。
相比之下,手写算法若未优化,可能出现 O (n²) 的排序(如简单冒泡),或 O (n) 的查找(未用二分),性能差距显著。

分享Linux、Unix、C/C++后端开发、面试题等技术知识讲解
Part3非修改序列算法
非修改序列算法仅读取容器元素,不改变元素值或容器结构,核心场景包括 "查找、计数、比较、遍历"。这类算法的共性是:均支持输入迭代器或更高,时间复杂度多为 O (n)。
3.1、元素查找与计数:从线性匹配到子序列搜索
1). find/find_if/find_if_not:精准定位元素
- find :查找等于目标值的元素(需元素支持 == 运算符);
- find_if :查找满足一元谓词的元素;
- find_if_not :查找不满足一元谓词的元素(C++11 新增)。
示例:查找自定义结构体 Person 中年龄大于 18 的对象:
#include <algorithm>
#include <vector>
#include <string>
struct Person {
std::string name;
int age;
};
int main() {
std::vector<Person> people = {
{"Alice", 16},
{"Bob", 20},
{"Charlie", 17}
};
// 查找年龄 > 18 的 Person,用 Lambda 作为谓词
auto it = std::find_if(people.begin(), people.end(), [](const Person& p) {
return p.age > 18;
});
if (it != people.end()) {
std::cout << "找到成年用户:" << it->name << std::endl; // 输出 Bob
}
return 0;
}
2). count/count_if:统计元素出现次数
count 统计等于目标值的元素个数,count_if 统计满足谓词的元素个数。需注意:在链表容器(如 list)上使用时,虽为线性遍历,但 STL 实现避免了重复计算迭代器,效率优于手写循环。
示例:统计 vector 中偶数的个数:
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5, 6};
// 统计偶数:x % 2 == 0
int even_count = std::count_if(vec.begin(), vec.end(), [](int x) {
return x % 2 == 0;
});
std::cout << "偶数个数:" << even_count << std::endl; // 输出 3
return 0;
}
3). search/search_n/find_end:子序列查找
- search :查找子序列首次出现的位置(如在字符串中找子串);
- search_n :查找连续 n 个满足条件的元素;
- find_end :查找子序列最后一次出现的位置。
示例:在字符串中查找子串 "ab":
#include <algorithm>
#include <string>
#include <iostream>
int main() {
std::string str = "aababcabcd";
std::string sub = "ab";
// 查找 sub 在 str 中首次出现的位置
auto it = std::search(str.begin(), str.end(), sub.begin(), sub.end());
if (it != str.end()) {
// 计算索引:it - str.begin()
std::cout << "子串首次出现位置:" << it - str.begin() << std::endl; // 输出 1
}
return 0;
}
3.2、序列比较与遍历:比手动循环更优雅
1). equal/mismatch/lexicographical_compare:序列对比
- equal:判断两个序列是否相等(需元素支持==);
- mismatch:查找两个序列首个不相等的元素对;
- lexicographical_compare:按字典序比较两个序列(如字符串排序)。
equal 的边界陷阱:
C++14 及之前,equal 仅比较 "第一个序列长度" 的元素,若第二个序列更长,超出部分会被忽略;
C++17 新增重载,可显式传入两个序列的 end 迭代器,避免此问题:
#include <algorithm>
#include <vector>
int main() {
std::vector<int> vec1 = {1, 2, 3};
std::vector<int> vec2 = {1, 2, 3, 4};
// C++14 行为:仅比较前 3 个元素,返回 true
bool is_equal_cpp14 = std::equal(vec1.begin(), vec1.end(), vec2.begin());
// C++17 行为:比较两个序列的全部元素,因长度不同返回 false
bool is_equal_cpp17 = std::equal(vec1.begin(), vec1.end(), vec2.begin(), vec2.end());
return 0;
}
2). for_each:泛型遍历的 "瑞士军刀"
for_each 遍历序列并对每个元素执行函数对象,支持返回函数对象(可携带遍历过程中的统计信息),灵活性优于 range-based for 循环。
示例:用 for_each 遍历并统计元素总和:
#include <algorithm>
#include <vector>
#include <iostream>
// 自定义函数对象,统计总和
struct SumCounter {
int sum = 0;
// 重载 () 运算符,接收元素
void operator()(int x) {
sum += x;
}
};
int main() {
std::vector<int> vec = {1, 2, 3, 4};
// for_each 返回函数对象,携带统计结果
SumCounter counter = std::for_each(vec.begin(), vec.end(), SumCounter());
std::cout << "元素总和:" << counter.sum << std::endl; // 输出 10
return 0;
}
for_each 与 range-based for 的对比:
- 可读性:range-based for 更直观(for (int x : vec) { ... });
- 灵活性:for_each 支持返回值(如上述统计总和)、适配迭代器适配器(如反向遍历);
- 副作用控制:两者均需避免在函数对象 / 循环体中修改外部变量(建议用捕获或函数对象成员变量)。
Part4修改序列算法
修改序列算法会改变元素值或容器结构(如移动、替换、删除元素),核心场景包括 "复制、移动、填充、替换、删除、重排"。这类算法的关键是:理解内存操作逻辑,避免迭代器失效与性能浪费。
4.1、复制与移动:避免手动循环的坑
1). copy/copy_if/copy_backward:元素复制
- copy :将源序列元素复制到目标序列;
- copy_if :复制满足谓词的元素(C++11 新增);
- copy_backward :从源序列末尾向前复制,避免覆盖未复制元素(如向量内部右移)。
copy 的核心陷阱:目标容器容量不足。copy 不自动扩容目标容器,若目标容器空间不够,会写入非法内存,导致崩溃。正确做法是提前预留容量,或用插入迭代器(back_inserter):
#include <algorithm>
#include <vector>
#include <iterator> // 包含 back_inserter
int main() {
std::vector<int> src = {1, 2, 3};
std::vector<int> dst;
// 错误:dst 为空,容量为 0,copy 会写入非法内存
// std::copy(src.begin(), src.end(), dst.begin());
// 正确方式 1:提前预留容量
dst.reserve(src.size()); // 预留 3 个元素空间
std::copy(src.begin(), src.end(), dst.begin());
// 正确方式 2:用 back_inserter 自动扩容
dst.clear(); // 清空之前的元素
std::copy(src.begin(), src.end(), std::back_inserter(dst));
return 0;
}
2). move/move_backward:移动语义的落地
C++11 引入移动语义后,move 算法可将源序列元素 "移动" 到目标序列(而非拷贝),大幅减少内存开销 ------ 尤其适用于大对象(如 std::string、自定义结构体)。
示例:移动 vector<string> 中的元素,避免拷贝开销:
#include <algorithm>
#include <vector>
#include <string>
#include <iterator>
int main() {
std::vector<std::string> src = {"a", "bb", "ccc"};
std::vector<std::string> dst;
// 移动 src 元素到 dst:src 元素会变为"有效但未定义"状态
std::move(src.begin(), src.end(), std::back_inserter(dst));
// 此时 dst 为 {"a", "bb", "ccc"},src 元素可能为空(取决于 string 实现)
return 0;
}
4.2、填充与替换:批量修改策略
1). fill/fill_n/generate/generate_n:批量填充
- fill :将序列所有元素设为指定值;
- fill_n :将序列前 n 个元素设为指定值;
- generate :用生成器函数(无参数,返回元素值)填充序列;
- generate_n :用生成器函数填充前 n 个元素。
示例:用 generate 填充随机数序列:
#include <algorithm>
#include <vector>
#include <random>
#include <iostream>
// 生成器函数:返回 [0, 100) 的随机数
int generate_random() {
static std::mt19937 gen(std::random_device{}());
static std::uniform_int_distribution<int> dist(0, 99);
return dist(gen);
}
int main() {
std::vector<int> vec(5); // 容量为 5 的空向量
// 用 generate_random 填充 vec
std::generate(vec.begin(), vec.end(), generate_random);
// 输出随机序列(示例:12 45 78 3 56)
for (int x : vec) {
std::cout << x << " ";
}
return 0;
}
2). replace/replace_if/replace_copy:元素替换
- replace:将序列中等于旧值的元素改为新值;
- replace_if:将满足谓词的元素改为新值;
- replace_copy:复制序列时,将等于旧值的元素改为新值(不修改源序列)。
示例:将 vector 中大于 10 的元素替换为 0:
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {5, 12, 8, 15, 3};
// 替换大于 10 的元素为 0
std::replace_if(vec.begin(), vec.end(), [](int x) {
return x > 10;
}, 0);
// 输出:5 0 8 0 3
for (int x : vec) {
std::cout << x << " ";
}
return 0;
}
4.3、删除与重排:伪删除与真实删除
1). remove/remove_if:理解 "伪删除" 原理
remove 算法的核心是 "移动元素覆盖待删除元素",但不改变容器大小(end () 迭代器位置不变),也不释放内存 ------ 这种 "只标记、不删除" 的行为称为 "伪删除"。
例如,vector<int> vec = {1, 2, 3, 2, 4},用 remove(vec.begin(), vec.end(), 2) 后:
- 元素移动为 {1, 3, 4, 2, 4}(待删除元素 2 被后续元素覆盖);
- 返回 "新的逻辑末尾迭代器"(指向第 3 个元素 4);
- 容器 size 仍为 5(物理大小未变)。
正确删除元素的方式:remove + erase。erase 会删除 "逻辑末尾到物理末尾" 的元素,释放内存:
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3, 2, 4};
// 步骤 1:remove 移动元素,返回逻辑末尾
auto new_end = std::remove(vec.begin(), vec.end(), 2);
// 步骤 2:erase 删除逻辑末尾后的元素,修改容器大小
vec.erase(new_end, vec.end());
// 此时 vec 为 {1, 3, 4},size 为 3
std::cout << "容器大小:" << vec.size() << std::endl; // 输出 3
return 0;
}
2). reverse/rotate:序列重排的高效实现
- reverse :反转序列(双向迭代器即可,如 list / vector );
- rotate :将序列 "旋转"(如 [a,b,c,d] 旋转后变为 [c,d,a,b] ),底层用 "三步反转法" 实现,时间复杂度 O (n),空间复杂度 O (1)。
rotate 的 "三步反转法" 原理(以 rotate(first, middle, last) 为例):
- 反转 [first, middle) 段;
- 反转 [middle, last) 段;
- 反转 [first, last) 整个段。
示例:旋转 vector 序列:
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 以 index=2 为界旋转:[1,2] 和 [3,4,5] 交换位置
std::rotate(vec.begin(), vec.begin() + 2, vec.end());
// 输出:3 4 5 1 2
for (int x : vec) {
std::cout << x << " ";
}
return 0;
}
3). unique:去重的前提与局限
unique 算法用于 "移除连续重复的元素",但需满足两个前提:
- 序列已排序(否则非连续重复元素无法去重);
- 配合 erase 使用(unique 仅做伪删除)。
示例:对 vector 排序后去重:
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {3, 1, 4, 1, 5, 1, 5};
// 步骤 1:先排序,使重复元素连续
std::sort(vec.begin(), vec.end()); // 排序后:1,1,1,3,4,5,5
// 步骤 2:unique 伪删除连续重复元素
auto new_end = std::unique(vec.begin(), vec.end());
// 步骤 3:erase 真实删除
vec.erase(new_end, vec.end());
// 输出:1 3 4 5
for (int x : vec) {
std::cout << x << " ";
}
return 0;
}
Part5排序与关联算法
排序与关联算法是 STL 中最 "硬核" 的部分,涵盖 "排序、二分查找、堆操作" 三大场景,底层实现融合了多种优化(如内省排序、堆排序、二分查找),是性能与稳定性的标杆。
5.1、排序算法全家桶:sort/stable_sort/partial_sort
1). sortvs stable_sort:稳定性与性能的权衡
sort:底层用 "内省排序"(Introsort),逻辑如下:
- 先用快速排序(分治高效);
- 当递归深度超过阈值(通常为 2*log2(n)),转堆排序(避免快排最坏情况 O (n²));
- 当子序列长度小于阈值(通常为 16),转插入排序(小序列高效);
- 不保证稳定性(相同元素的相对顺序可能改变)。
stable_sort:底层用 "归并排序",保证稳定性(相同元素相对顺序不变),但空间复杂度更高(O (n) 额外空间,若内存不足会退化为 O (n log² n) 时间复杂度)。
示例:对比 sort 与 stable_sort 的稳定性:
#include <algorithm>
#include <vector>
#include <iostream>
// 自定义结构体:包含值和索引
struct Element {
int value;
int index;
};
int main() {
std::vector<Element> vec = {
{2, 0},
{1, 1},
{2, 2}
};
// 用 value 排序,sort 不保证稳定性
std::vector<Element> vec_sort = vec;
std::sort(vec_sort.begin(), vec_sort.end(), [](const Element& a, const Element& b) {
return a.value < b.value;
});
// stable_sort 保证稳定性(value=2 的元素仍保持 index 0 < 2)
std::vector<Element> vec_stable = vec;
std::stable_sort(vec_stable.begin(), vec_stable.end(), [](const Element& a, const Element& b) {
return a.value < b.value;
});
// 输出 sort 结果(可能为:(1,1), (2,2), (2,0) ------ 不稳定)
std::cout << "sort 结果:";
for (auto& e : vec_sort) std::cout << "(" << e.value << "," << e.index << ") ";
// 输出 stable_sort 结果(一定为:(1,1), (2,0), (2,2) ------ 稳定)
std::cout << "\nstable_sort 结果:";
for (auto& e : vec_stable) std::cout << "(" << e.value << "," << e.index << ") ";
return 0;
}
2). partial_sort/nth_element:部分排序的高效选择
- partial_sort :对序列前 k 个元素排序,使前 k 个元素为 "全局最小的 k 个元素" 且有序,时间复杂度 O (n log k)------ 适用于 "取前 k 小 / 大元素" 场景(如 Top K 问题)。
- nth_element :使第 n 个元素(从 0 开始)为 "全局第 n 小的元素",左侧元素均小于等于它,右侧元素均大于等于它,时间复杂度 O (n)------ 适用于 "找中位数" 或 "分割序列" 场景。
示例:用 partial_sort 取前 3 小元素:
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {5, 3, 1, 4, 2, 6};
// 取前 3 小元素,排序后放在前 3 个位置
std::partial_sort(vec.begin(), vec.begin() + 3, vec.end());
// 输出:1 2 3 5 4 6(前 3 个元素为全局最小,且有序)
for (int x : vec) {
std::cout << x << " ";
}
return 0;
}
5.2、二分查找:有序序列的快速检索
二分查找算法仅适用于已排序的序列,核心是 "每次将搜索范围缩小一半",时间复杂度 O (log n)。STL 提供 4 个二分查找相关算法:
|---------------|---------------------|-----------------------------------|
| 算法 | 功能描述 | 返回值 |
| binary_search | 判断序列中是否存在目标值 | bool(存在返回 true) |
| lower_bound | 查找第一个 "大于等于" 目标值的元素 | 迭代器(指向该元素,若无则返回 end ()) |
| upper_bound | 查找第一个 "大于" 目标值的元素 | 迭代器(指向该元素,若无则返回 end ()) |
| equal_range | 查找 "等于目标值的所有元素" 的区间 | 迭代器对([lower_bound, upper_bound)) |
关键陷阱:无序序列用二分查找
若序列未排序,binary_search 返回 false 不代表元素不存在 ------ 二分查找的前提是 "序列有序",无序序列的查找结果未定义(可能返回 true 也可能返回 false,完全随机)。
示例:用 equal_range 查找有序序列中等于 3 的元素区间:
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3, 3, 3, 4, 5}; // 已排序
// 查找等于 3 的元素区间
auto [low_it, high_it] = std::equal_range(vec.begin(), vec.end(), 3);
// 输出区间元素:3 3 3
std::cout << "等于 3 的元素:";
for (auto it = low_it; it != high_it; ++it) {
std::cout << *it << " ";
}
// 输出区间长度:3
std::cout << "\n区间长度:" << high_it - low_it << std::endl;
return 0;
}
5.3、堆操作:优先队列的底层引擎
堆是一种 "完全二叉树" 结构,分为大根堆(父节点 >= 子节点)和小根堆(父节点 <= 子节点)。STL 堆操作基于 "大根堆" 实现,核心算法包括 make_heap/push_heap/pop_heap/sort_heap,通常与 vector 配合使用(用向量模拟堆的完全二叉树结构)。
堆操作的配合流程:
- make_heap:将向量转换为堆(大根堆);
- push_heap:向堆中插入元素(需先调用push_back将元素加入向量);
- pop_heap:删除堆顶元素(将堆顶元素移到向量末尾,需后续调用pop_back移除);
- sort_heap:将堆排序为有序序列(从小到大,因堆是大根堆,排序后为升序)。
示例:用堆操作实现优先队列功能:
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> heap_vec = {3, 1, 4, 1, 5};
// 1. 构建大根堆
std::make_heap(heap_vec.begin(), heap_vec.end());
std::cout << "堆顶元素(最大):" << heap_vec[0] << std::endl; // 输出 5
// 2. 插入元素 6
heap_vec.push_back(6);
std::push_heap(heap_vec.begin(), heap_vec.end()); // 调整堆结构
std::cout << "插入 6 后堆顶:" << heap_vec[0] << std::endl; // 输出 6
// 3. 删除堆顶元素(6)
std::pop_heap(heap_vec.begin(), heap_vec.end()); // 堆顶 6 移到末尾
heap_vec.pop_back(); // 移除末尾的 6
std::cout << "删除堆顶后堆顶:" << heap_vec[0] << std::endl; // 输出 5
// 4. 堆排序(升序)
std::sort_heap(heap_vec.begin(), heap_vec.end());
std::cout << "排序后序列:";
for (int x : heap_vec) std::cout << x << " "; // 输出 1 1 3 4 5
return 0;
}
std::priority_queue与堆操作的关系
std::priority_queue 是 STL 提供的优先队列容器,其底层正是基于堆操作实现的 ------ 默认是大根堆,可通过 std::greater 改为小根堆:
#include <queue> // 包含 priority_queue
#include <vector>
#include <iostream>
int main() {
// 小根堆:priority_queue<类型, 底层容器, 比较函数>
std::priority_queue<int, std::vector<int>, std::greater<int>> min_heap;
min_heap.push(5);
min_heap.push(3);
min_heap.push(7);
// 输出堆顶(最小元素):3
std::cout << "小根堆顶:" << min_heap.top() << std::endl;
return 0;
}
Part6集合与数值算法
6.1、集合操作:基于有序序列的高效运算
STL 集合算法用于 "两个有序序列的交集、并集、差集" 等操作,前提是两个输入序列均已排序且排序规则一致,时间复杂度均为 O (n + m)(n、m 为两序列长度)。
核心算法包括:
- set_union:求两序列的并集(元素不重复);
- set_intersection:求两序列的交集(元素在两序列中均存在);
- set_difference:求两序列的差集(元素在第一个序列中但不在第二个中);
- merge:合并两有序序列为一个有序序列(元素可重复);
- includes:判断第一个序列是否包含第二个序列(子集判断)。
示例:用 set_intersection 求两个有序数组的交集:
#include <algorithm>
#include <vector>
#include <iostream>
#include <iterator>
int main() {
// 两个已排序的序列
std::vector<int> vec1 = {1, 2, 3, 4, 5};
std::vector<int> vec2 = {3, 4, 5, 6, 7};
std::vector<int> result;
// 求交集,结果存入 result
std::set_intersection(
vec1.begin(), vec1.end(),
vec2.begin(), vec2.end(),
std::back_inserter(result) // 插入迭代器,自动扩容
);
// 输出交集:3 4 5
std::cout << "两序列交集:";
for (int x : result) {
std::cout << x << " ";
}
return 0;
}
6.2、数值算法:从基础计算到高性能
数值算法用于 "数学运算" 场景,如累加、内积、差分、前缀和
核心算法包括:accumulate/inner_product/adjacent_difference/partial_sum/iota。
1). accumulate:序列累加的 "瑞士军刀"
accumulate 不仅能求序列总和,还能通过自定义二元操作实现 "字符串拼接""元素计数" 等扩展功能。需注意:初始值类型决定结果类型,若初始值为 int,即使序列是 double,结果也会被截断为 int。
示例 1:求序列总和(注意初始值类型):
#include <algorithm>
#include <numeric> // 包含 accumulate
#include <vector>
#include <iostream>
int main() {
std::vector<double> vec = {1.5, 2.5, 3.5};
// 错误:初始值为 int,结果会截断为 7(1.5+2.5+3.5=7.5)
int sum_int = std::accumulate(vec.begin(), vec.end(), 0);
// 正确:初始值为 double,结果为 7.5
double sum_double = std::accumulate(vec.begin(), vec.end(), 0.0);
std::cout << "sum_int: " << sum_int << std::endl; // 输出 7
std::cout << "sum_double: " << sum_double << std::endl; // 输出 7.5
return 0;
}
示例 2:用 accumulate 拼接字符串:
#include <algorithm>
#include <numeric>
#include <vector>
#include <string>
#include <iostream>
int main() {
std::vector<std::string> words = {"Hello", " ", "World", "!"};
// 二元操作:字符串拼接(std::string::operator+)
std::string sentence = std::accumulate(
words.begin(), words.end(),
std::string("") // 初始值为空字符串
);
std::cout << sentence << std::endl; // 输出 "Hello World!"
return 0;
}
2). adjacent_difference/partial_sum:差分与前缀和
- adjacent_difference :计算序列中 "当前元素与前一个元素的差值"(差分序列);
- partial_sum :计算序列中 "前 n 个元素的累加和"(前缀和序列)。
示例:计算差分与前缀和:
#include <algorithm>
#include <numeric>
#include <vector>
#include <iostream>
#include <iterator>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::vector<int> diff, prefix;
// 计算差分序列:[1, 2-1=1, 3-2=1, 4-3=1, 5-4=1]
std::adjacent_difference(vec.begin(), vec.end(), std::back_inserter(diff));
// 计算前缀和序列:[1, 1+2=3, 3+3=6, 6+4=10, 10+5=15]
std::partial_sum(vec.begin(), vec.end(), std::back_inserter(prefix));
// 输出差分:1 1 1 1 1
std::cout << "差分序列:";
for (int x : diff) std::cout << x << " ";
// 输出前缀和:1 3 6 10 15
std::cout << "\n前缀和序列:";
for (int x : prefix) std::cout << x << " ";
return 0;
}
3). iota:生成连续序列
iota(发音 "艾欧塔")用于生成 "从初始值开始的连续递增序列",C++11 新增,适用于 "生成索引""填充连续整数" 场景。
示例:生成 1~10 的连续序列:
#include <algorithm>
#include <numeric>
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec(10); // 容量为 10 的空向量
// 生成 1~10 的连续序列(初始值 1,每次+1)
std::iota(vec.begin(), vec.end(), 1);
// 输出:1 2 3 4 5 6 7 8 9 10
for (int x : vec) {
std::cout << x << " ";
}
return 0;
}
Part7进阶特性
7.1、算法参数与迭代器适配
1). std::bind:调整算法的参数个数
std::bind 用于 "绑定函数对象的部分参数",将多元函数对象转为一元函数对象,适配仅支持一元谓词的算法(如 find_if)。
示例:用 bind 适配二元谓词到 find_if:
#include <algorithm>
#include <functional> // 包含 bind
#include <vector>
#include <iostream>
// 二元谓词:判断 x 是否大于 y
bool is_greater(int x, int y) {
return x > y;
}
int main() {
std::vector<int> vec = {5, 3, 8, 2, 9};
int threshold = 5;
// 用 bind 将 is_greater(x, y) 转为一元谓词:is_greater(x, threshold)
auto greater_than_threshold = std::bind(is_greater, std::placeholders::_1, threshold);
// find_if 用一元谓词查找大于 5 的元素
auto it = std::find_if(vec.begin(), vec.end(), greater_than_threshold);
if (it != vec.end()) {
std::cout << "第一个大于 5 的元素:" << *it << std::endl; // 输出 8
}
return 0;
}
2). 迭代器适配器:扩展算法的适用场景
迭代器适配器是 "包装现有迭代器" 的工具,可改变迭代器的行为,核心包括:
- 反向迭代器( reverse_iterator ) :将迭代器方向反转( rbegin() 指向末尾, rend() 指向开头),适配 sort 实现降序排序;
- 插入迭代器( inserter / back_inserter / front_inserter ) :自动调用容器的插入函数( insert / push_back / push_front ),解决 copy 目标容器容量不足问题;
- 流迭代器( istream_iterator / ostream_iterator ) :将 IO 流(如 cin / cout )视为容器,让算法直接操作流数据。
示例:用流迭代器实现 "从标准输入读取整数,排序后输出到标准输出":
#include <algorithm>
#include <iterator>
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec;
// 从 cin 读取整数(直到 EOF,如 Ctrl+D)
std::cout << "请输入整数(以 Ctrl+D 结束):";
std::copy(
std::istream_iterator<int>(std::cin), // 输入流迭代器
std::istream_iterator<int>(), // EOF 迭代器
std::back_inserter(vec)
);
// 排序
std::sort(vec.begin(), vec.end());
// 输出到 cout,元素间用空格分隔
std::cout << "排序后:";
std::copy(
vec.begin(), vec.end(),
std::ostream_iterator<int>(std::cout, " ") // 输出流迭代器,分隔符为空格
);
return 0;
}
7.2、并行算法:C++17 效率飞跃
C++17 引入 "并行算法",通过 std::execution 命名空间的执行策略,让算法自动利用多核 CPU 并行计算,大幅提升计算密集型任务的效率。
核心执行策略:
- std::execution::seq :串行执行(默认,与 C++17 前的算法行为一致);
- std::execution::par :并行执行(多线程,可能拆分任务到不同线程);
- std::execution::par_unseq :并行 + 向量化执行(允许编译器用 SIMD 指令优化,进一步提升效率)。
并行算法的关键约束:
- 谓词必须 "无副作用"(不能修改外部变量,否则会导致数据竞争);
- 迭代器必须是 "可随机访问迭代器"(如 vector/array 的迭代器);
- 数据规模需足够大(小数据量的并行开销可能超过收益)。
示例:用并行 sort 排序大向量:
#include <algorithm>
#include <execution> // 包含并行执行策略
#include <vector>
#include <random>
#include <chrono>
#include <iostream>
int main() {
// 生成 100 万个随机数
std::vector<int> vec(1'000'000);
std::mt19937 gen(std::random_device{}());
std::uniform_int_distribution<int> dist(0, 999'999);
std::generate(vec.begin(), vec.end(), [&]() { return dist(gen); });
// 串行排序计时
auto vec_seq = vec;
auto start_seq = std::chrono::high_resolution_clock::now();
std::sort(std::execution::seq, vec_seq.begin(), vec_seq.end());
auto end_seq = std::chrono::high_resolution_clock::now();
auto time_seq = std::chrono::duration_cast<std::chrono::milliseconds>(end_seq - start_seq).count();
// 并行排序计时
auto vec_par = vec;
auto start_par = std::chrono::high_resolution_clock::now();
std::sort(std::execution::par, vec_par.begin(), vec_par.end());
auto end_par = std::chrono::high_resolution_clock::now();
auto time_par = std::chrono::duration_cast<std::chrono::milliseconds>(end_par - start_par).count();
// 输出时间对比(并行通常快 2~4 倍,取决于 CPU 核心数)
std::cout << "串行排序时间:" << time_seq << "ms" << std::endl;
std::cout << "并行排序时间:" << time_par << "ms" << std::endl;
return 0;
}
7.3、新标准前沿:C++20/23 演进
1). C++20 Ranges:算法调用的范式转变
C++20 Ranges 库重构了 STL 算法的调用方式,核心改进包括:
- 无需显式传递迭代器对 :直接传递容器,算法自动使用 begin() / end() ;
- 支持链式调用 :用 | 操作符串联多个算法(如 "过滤→转换→排序");
- 惰性求值 :中间结果不存储,仅在需要时计算,减少内存开销。
示例:用 Ranges 实现 "过滤偶数→平方→排序" 的链式调用:
#include <ranges> // C++20 Ranges
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5, 6};
// 链式调用:过滤偶数 → 平方 → 排序
auto result = vec
| std::views::filter([](int x) { return x % 2 == 0; }) // 过滤:2,4,6
| std::views::transform([](int x) { return x * x; }) // 平方:4,16,36
| std::views::common; // 转为普通视图,适配 sort
// 排序(Ranges 版 sort 直接接收视图)
std::ranges::sort(result);
// 输出:4 16 36
for (int x : result) {
std::cout << x << " ";
}
return 0;
}
2). C++23 提案:ranges::views与算法的深度整合
C++23 进一步扩展 Ranges 功能,例如 ranges::views::zip(将多个序列打包为元组序列)、ranges::views::slide(滑动窗口),让算法能更灵活地处理多序列或窗口数据。
示例:用 ranges::views::zip 同时遍历两个序列:
#include <ranges> // C++23 支持 views::zip
#include <vector>
#include <iostream>
int main() {
std::vector<int> a = {1, 2, 3};
std::vector<std::string> b = {"a", "b", "c"};
// zip 打包 a 和 b,每个元素为 (a[i], b[i])
for (auto [x, s] : std::views::zip(a, b)) {
std::cout << x << " → " << s << std::endl;
}
// 输出:
// 1 → a
// 2 → b
// 3 → c
return 0;
}
3). GPU 加速:STL 算法的异构计算扩展
随着 GPU 计算的普及,部分编译器(如 NVIDIA HPC SDK)已支持将 STL 算法(如 sort/transform) offload 到 GPU 执行,通过 #pragma omp target teams distribute 等指令,实现 "CPU 控制 + GPU 计算" 的异构模式,大幅提升大规模数据处理的效率。
Part8算法实战
8.1、算法选择决策树:从需求到方案
在实际开发中,算法选型需结合 "数据规模、容器类型、需求场景" 三要素,以下是关键决策路径:
1). 数据规模维度
- 小数据集(n <100):优先选择 "实现简单" 的算法(如 for_each 遍历、 sort 排序),无需过度优化;
- 大数据集(n > 1e6):优先选择 "时间复杂度低" 的算法(如 binary_search 代替 find 、 sort 代替手动排序),若支持并行则启用 std::execution::par 。
2). 容器类型维度
- vector / array (随机访问迭代器):优先用 sort / binary_search / transform (随机访问高效);
- list (双向迭代器):用 list::sort 代替 std::sort ,配合 merge / reverse (双向迭代器适配);
- map / set (有序关联容器):用容器自带的 find (O (log n))而非 std::find (O (n)),配合 for_each 遍历。
3). 需求场景维度
|--------------|---------------------------------------|--------------------|
| 需求场景 | 推荐算法组合 | 容器适配 |
| 查找元素 | 无序序列:find_if;有序序列:lower_bound | 所有容器 |
| 排序 | 需稳定:stable_sort;无需稳定:sort | vector/deque/array |
| 去重 | sort + unique + erase | vector/deque |
| Top K 问题 | partial_sort(需有序输出);nth_element(无需有序) | vector/array |
| 两有序序列交集 / 并集 | set_intersection/set_union | 所有容器(需有序) |
| 求和 / 统计 | accumulate(基础统计);count_if(条件统计) | 所有容器 |
8.2、经典案例实战
案例 1:日志数据去重与排序
需求:处理日志文件,日志结构体包含 time(时间戳)和 content(内容),需按时间戳排序,去重相同内容的日志。
实现步骤:
- 读取日志到 vector<Log>;
- 按时间戳排序(sort);
- 按内容去重(unique + erase);
- 输出排序后的日志。
代码实现:
#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
// 日志结构体
struct Log {
long long time; // 时间戳(毫秒)
std::string content; // 日志内容
};
int main() {
// 模拟日志数据
std::vector<Log> logs = {
{1620000000000, "Server started"},
{1620000001000, "User login"},
{1620000000000, "Server started"}, // 重复日志
{1620000002000, "User logout"}
};
// 步骤 1:按时间戳排序
std::sort(logs.begin(), logs.end(), [](const Log& a, const Log& b) {
return a.time < b.time;
});
// 步骤 2:按内容去重(需先排序,使重复日志连续)
auto new_end = std::unique(logs.begin(), logs.end(), [](const Log& a, const Log& b) {
return a.content == b.content; // 按内容判断重复
});
logs.erase(new_end, logs.end());
// 步骤 3:输出结果
std::cout << "排序去重后的日志:" << std::endl;
for (const auto& log : logs) {
std::cout << "[" << log.time << "] " << log.content << std::endl;
}
return 0;
}
输出结果:
排序去重后的日志:
[1620000000000] Server started
[1620000001000] User login
[1620000002000] User logout
案例 2:两个有序数组的中位数查找
需求:给定两个有序数组 nums1 和 nums2,求它们的中位数(若总长度为奇数,取中间元素;若为偶数,取中间两个元素的平均值)。
实现思路:
- 总长度为 m + n,中位数位置为 k = (m + n - 1) / 2(0 开始);
- 用 nth_element 找到第 k 小元素(O (m + n) 时间复杂度),无需合并两个数组。
代码实现:
#include <algorithm>
#include <vector>
#include <iostream>
#include <numeric>
double findMedianSortedArrays(std::vector<int>& nums1, std::vector<int>& nums2) {
// 合并两个数组(用 reserve 避免扩容)
std::vector<int> merged;
merged.reserve(nums1.size() + nums2.size());
merged.insert(merged.end(), nums1.begin(), nums1.end());
merged.insert(merged.end(), nums2.begin(), nums2.end());
int total = merged.size();
int k = (total - 1) / 2;
// 找到第 k 小元素(0 开始)
std::nth_element(merged.begin(), merged.begin() + k, merged.end());
if (total % 2 == 1) {
// 奇数长度:中位数为第 k 小元素
return merged[k];
} else {
// 偶数长度:中位数为第 k 小和第 k+1 小元素的平均值
// 先找到第 k+1 小元素(此时 merged[k] 已确定,只需排序 k+1 位置)
std::nth_element(merged.begin() + k + 1, merged.begin() + k + 1, merged.end());
return (merged[k] + merged[k + 1]) / 2.0;
}
}
int main() {
std::vector<int> nums1 = {1, 3};
std::vector<int> nums2 = {2};
std::cout << "中位数:" << findMedianSortedArrays(nums1, nums2) << std::endl; // 输出 2.0
std::vector<int> nums3 = {1, 2};
std::vector<int> nums4 = {3, 4};
std::cout << "中位数:" << findMedianSortedArrays(nums3, nums4) << std::endl; // 输出 2.5
return 0;
}
Part9误区与调试
9.1、高频误区盘点
1). 迭代器类型错误
- 错误场景 :用 std::sort 排序 list ( list 迭代器是双向迭代器,不支持随机访问);
- 后果 :编译报错(如 GCC 提示 "无匹配的 sort 函数");
- 解决 :用 list::sort 成员函数,或先将 list 转为 vector 再排序。
2). remove后未调用 erase
- 错误场景 :仅调用 std::remove 删除元素,未调用 erase ;
- 后果 :容器大小不变,失效元素留在容器末尾,后续访问可能导致脏数据;
- 解决 :始终用 vec.erase(std::remove(...), vec.end()) 组合删除。
3). 二分查找用在无序序列
- 错误场景:对未排序的vector调用binary_search;
- 后果:返回结果未定义(可能误判元素不存在);
- 解决:先调用sort排序,再用二分查找算法。
4). 谓词有副作用
- 错误场景 : find_if 的谓词修改外部变量(如 [&count](int x) { count++; return x > 10; } );
- 后果 :多线程环境下导致数据竞争,或算法多次调用谓词时重复修改;
- 解决 :谓词仅做判断,不修改外部变量;若需统计,用 for_each 返回函数对象。
9.2、调试工具与方法
1). std::debug_iterator:捕获失效迭代器
MSVC、GCC 等编译器提供 std::debug_iterator(或 __gnu_debug::vector),可在运行时检测迭代器失效(如 erase 后继续使用迭代器),抛出明确的错误信息。
示例:用 __gnu_debug::vector 调试迭代器失效:
#include <algorithm>
#include <debug/vector> // GCC 调试向量(需链接 -D_GLIBCXX_DEBUG)
#include <iostream>
int main() {
__gnu_debug::vector<int> vec = {1, 2, 3, 4};
auto it = vec.begin();
vec.erase(it); // 删除 it 指向的元素,it 失效
// 错误:访问失效迭代器,debug 模式下抛出异常
std::cout << *it << std::endl;
return 0;
}
运行结果(GCC 调试模式):
/usr/include/c++/11/debug/vector:448:
Error: attempt to dereference a past-the-end iterator.
2). 静态断言验证迭代器类型
用 std::iterator_traits 和 static_assert,在编译期验证迭代器类型是否符合算法要求,避免运行时错误。
示例:验证 vector 迭代器是随机访问迭代器:
#include <algorithm>
#include <vector>
#include <iterator> // 包含 iterator_traits
#include <type_traits> // 包含 is_same_v
int main() {
using Iterator = std::vector<int>::iterator;
// 静态断言:验证 Iterator 的迭代器类别是随机访问迭代器
static_assert(
std::is_same_v<
typename std::iterator_traits<Iterator>::iterator_category,
std::random_access_iterator_tag
>,
"vector 迭代器必须是随机访问迭代器"
);
return 0;
}
3). 性能分析工具
用 Perf(Linux)、Visual Studio 性能探查器(Windows)等工具,分析算法的执行时间,定位性能瓶颈:
- Perf : perf record ./a.out 记录性能数据, perf report 查看热点函数(如 std::sort 的耗时);
- Visual Studio :通过 "性能探查器" 的 "CPU 使用率" 视图,查看算法函数的调用次数和耗时。
总结
STL 算法的本质是 "泛型编程思想的落地"------ 通过迭代器解耦算法与容器,通过函数对象扩展算法逻辑,最终实现 "一次编写,多容器复用" 的高效开发模式。掌握 STL 算法,不仅是学会调用 API,更是理解 "如何设计通用、高效、安全的代码"。
学习路线:
- 基础阶段 :熟练掌握常用算法( find / sort / copy / accumulate ),理解迭代器分类与函数对象;
- 进阶阶段 :深入底层实现(如 sort 的内省排序、 remove 的伪删除),掌握并行算法与 Ranges;
- 专家阶段 :阅读 STL 源码(如 GCC libstdc++、Clang libc++),实现自定义泛型算法(如适配特殊容器的排序算法)。