---------------呵呵哒---------------
家人们真的很感谢你们的支持,有幸刷到我的文章也是一种不可磨灭的缘分,我还只是个命苦的学生,如果你的手指还没有残废的话麻烦点一下点赞+收藏+关注。我的专栏里还有很多有趣的内容,呃如果不想买的话可以看里面的试读文章,我会不断更新,当然买下来我会大大滴感谢泥,真的想赚点零花钱呜呜呜T_T
家人们好呀!!!
你的数据住在宽敞的容器里,可你对它做的绝大多数操作,依然像个一丝不苟的实习生------亲自写下一个个for循环,检查每个元素,算求和、找最大值、删掉不想要的。这种感觉就像是你明明有一台智能手机,却仍然在用计算器、手写日记、纸质地图------不是不行,但总觉得缺点效率。

C++早就为你准备了一套不用手写循环的"自动化工具包"------算法库。它藏在<algorithm>和<numeric>两个头文件中,提供了超过100个开箱即用的算法函数。它们是C++标准库中最高频使用、最能带来代码质量飞跃的组件之一:遍历、查找、排序、计数、复制、替换、归并、堆操作......几乎所有你能想到的对数据的加工操作,这里都有现成的,而且通常比你手写循环更快、更安全、更不容易出Bug。
这篇文章,我们就来系统认识这套算法库。从"直接用容器传参"到"用谓词和投影表达意图",从基础的序列操作到并行加速,再到C++20后的ranges新范式。你会发现:原来只要一个标准算法的调用,就能消灭十几行业务代码;代码变漂亮的同时,Bug数也肉眼可见地下降。
一、算法库概述
1.1 STL算法的设计哲学
STL算法和容器之间不是强绑定的------算法并不知道操作的具体是什么容器,它只通过迭代器来跟数据打交道。这种解耦设计就像是一位经验丰富的搬家师傅:他不需要认识你家里的每一个人(不需要了解容器的内部类型),只需要按照门牌号清单(迭代器范围)搬运物品。这就让同一个算法(比如sort)可以同时服务于vector、deque、原始数组,甚至任何自定义的可迭代类型。

所有的STL算法都以迭代器对[begin, end)作为输入,表示一个"左闭右开"的范围。这是C++标准库最核心的约定之一:begin指向第一个要处理的元素,end指向最后一个元素"之后"的位置------而不是最后一个元素本身。所以循环条件永远是it != end而非it <= end。
1.2 算法的基本使用模式
STL算法的调用方式高度统一,掌握了基本模式,你就能举一反三:
cpp
// 基本模式:algorithm(first, last, ...args);
// 返回值通常是迭代器或结果值
vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
// 查找:返回迭代器,没找到则返回v.end()
auto it = find(v.begin(), v.end(), 5);
// 排序:直接修改原容器
sort(v.begin(), v.end());
// 计数:返回数值
int cnt = count(v.begin(), v.end(), 1);
// 复制:将v中的内容输出到目标容器(目标容器必须预先分配好空间)
vector<int> dest(v.size());
copy(v.begin(), v.end(), dest.begin());
注意copy这类写入操作有一个规则,目标容器必须已经分配好足够的空间。copy不会帮你扩容,它只是机械地将元素逐一复制到目标迭代器指向的位置。如果你不确定目标有多大,可以用back_inserter自动扩容。
1.3 用Lambda和谓词定制算法
绝大多数STL算法都支持传入一个谓词(Predicate)------一个返回bool的可调用对象,用来定制算法的行为。现代C++中最常用的谓词就是Lambda表达式:
cpp
// 不用Lambda:找一个偶数
auto it1 = find_if(v.begin(), v.end(), isEven); // isEven是普通函数
// 用Lambda:找一个偶数,逻辑直接写在调用处
auto it2 = find_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
// 按自定义规则排序:按绝对值降序
sort(v.begin(), v.end(), [](int a, int b) { return abs(a) > abs(b); });
Lambda还有一个绝妙之处,它能捕获外部的变量,把"上下文"带进算法中。比如你想查找第一个大于某个外部阈值的元素,传统做法要么是定义全局变量,要么是写函数对象。Lambda一行搞定:
cpp
int threshold = 5;
auto it = find_if(v.begin(), v.end(), [threshold](int x) { return x > threshold; });
把算法想成餐厅后厨的料理机,容器是食材筐,迭代器是厨师的手。基础模式是"全自动模式"------所有食材按顺序处理。而谓词就是后厨的"口味定制卡":不加谓词相当于"默认口味",加入Lambda就是"少盐多辣、不放香菜"。
二、非修改序列算法
非修改序列算法是最安全的一类:它们只读取数据,不修改容器中的元素内容,非常适合用于数据检查、统计和验证场景。
2.1 查找类算法
find / find_if / find_if_not
最常用的查找三件套:find按值查找,find_if按条件查找,find_if_not查找第一个不满足条件的元素。
cpp
vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
auto it1 = find(v.begin(), v.end(), 5); // 找值为5的元素
auto it2 = find_if(v.begin(), v.end(), [](int x) { return x > 5; }); // 找第一个>5的元素
auto it3 = find_if_not(v.begin(), v.end(), [](int x) { return x % 2 == 0; }); // 找第一个非偶数
if (it2 != v.end()) {
cout << "找到了: " << *it2 << ", 位置: " << distance(v.begin(), it2) << endl;
}
count / count_if
统计满足条件的元素个数,在数据分析中极为常用:
cpp
int cnt1 = count(v.begin(), v.end(), 1); // 统计值为1的元素个数
int cnt2 = count_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; }); // 偶数个数
search / find_end

这两个函数用于查找子序列:search找到第一个匹配的子序列,find_end找到最后一个匹配的子序列。
cpp
vector<int> v = {1, 2, 3, 1, 2, 3, 4};
vector<int> sub = {2, 3};
auto it1 = search(v.begin(), v.end(), sub.begin(), sub.end()); // 找第一个{2,3},指向位置1
auto it2 = find_end(v.begin(), v.end(), sub.begin(), sub.end()); // 找最后一个{2,3},指向位置4
2.2 逻辑判断类算法
all_of / any_of / none_of(C++11起)
这三个是数据验证的"三大护法",在项目中的出场率极高:
cpp
vector<int> scores = {85, 92, 78, 88};
bool allPass = all_of(scores.begin(), scores.end(), [](int s) { return s >= 60; }); // 全部及格?
bool anyFail = any_of(scores.begin(), scores.end(), [](int s) { return s < 60; }); // 有人不及格?
bool allFull = none_of(scores.begin(), scores.end(), [](int s) { return s == 0; }); // 没有空成绩?
all_of、any_of、none_of是C++界的"全员/有人/无人"三兄弟------项目经理让你检查"是不是所有模块都上线了",用all_of;测试让你检查"有没有哪个用例挂了",用any_of;合规让你确认"没有一个数据是空的",用none_of。把这三兄弟用熟,你的代码会从"一堆if"进化成"一句话"。
2.3 遍历与比较类算法
for_each:对范围内每个元素执行指定操作,适合需要为每个元素产生副作用(如打印、累加到外部变量)的场景。但需要说明的是,对简单的只读遍历,C++11的范围for循环通常比for_each更直观可读。for_each的优势主要体现在可以接收返回可调用对象的Lambda、或作为管道式处理链路的一部分。
cpp
vector<int> v = {1, 2, 3, 4, 5};
// 用for_each打印每个元素
for_each(v.begin(), v.end(), [](int x) { cout << x << " "; });
equal / mismatch:比较两个序列是否相同,或者找到第一个不同的位置。
cpp
vector<int> a = {1, 2, 3, 4};
vector<int> b = {1, 2, 5, 4};
bool same = equal(a.begin(), a.end(), b.begin()); // false
auto [itA, itB] = mismatch(a.begin(), a.end(), b.begin());
// itA指向a[2],itB指向b[2]
三、修改序列算法
修改序列算法会改变容器中元素的值、顺序或存在,它们是数据处理管道的核心组件。
3.1 复制与转移
copy / copy_if:将源数据复制到目标位置,copy_if可以有条件地复制。
cpp
vector<int> src = {1, 2, 3, 4, 5, 6};
vector<int> dest;
// 只复制偶数到dest
copy_if(src.begin(), src.end(), back_inserter(dest), [](int x) { return x % 2 == 0; });
// dest = {2, 4, 6}
3.2 transform:批量转换
transform是数据处理中最常用的算法之一,能将一个序列"映射"成另一个序列:
cpp
vector<int> v = {1, 2, 3, 4, 5};
vector<int> squares(v.size());
// 计算每个元素的平方
transform(v.begin(), v.end(), squares.begin(), [](int x) { return x * x; });
// squares = {1, 4, 9, 16, 25}
// 两两操作:逐元素相加
vector<int> a = {1, 2, 3};
vector<int> b = {4, 5, 6};
vector<int> sum(3);
transform(a.begin(), a.end(), b.begin(), sum.begin(), plus<int>());
// sum = {5, 7, 9}
3.3 replace / replace_if:就地替换
cpp
vector<int> v = {1, 2, 3, 2, 4, 2};
replace(v.begin(), v.end(), 2, 99); // 所有2变成99
replace_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; }, 0); // 所有偶数变成0
3.4 remove / remove_if + erase:经典的删除套装

这是STL最容易让新手踩坑的地方之一。remove本身不会真正删除元素,它只是把要保留的元素往前移动,返回指向"新末尾"的迭代器,容器的大小并不会改变。要真正删除,必须配合容器的erase成员函数------这就是著名的"erase-remove惯用法":
cpp
vector<int> v = {1, 2, 3, 2, 4, 2};
// 错误做法:只用remove,v.size()还是6,末尾有3个"垃圾值"
// remove(v.begin(), v.end(), 2);
// 正确做法:erase-remove
v.erase(remove(v.begin(), v.end(), 2), v.end());
// v = {1, 3, 4},干净利落
// C++20起可以用更简洁的erase_if
// erase_if(v, [](int x) { return x % 2 == 0; });
3.5 reverse / rotate / shuffle:序列重排
cpp
vector<int> v = {1, 2, 3, 4, 5};
reverse(v.begin(), v.end()); // {5, 4, 3, 2, 1}
rotate(v.begin(), v.begin() + 2, v.end()); // {3, 4, 5, 1, 2} // 以第2个元素为新起点
shuffle(v.begin(), v.end(), mt19937{random_device{}()}); // 随机重排
3.6 unique:去除连续重复元素
cpp
vector<int> v = {1, 1, 2, 3, 3, 3, 4, 5, 5};
auto last = unique(v.begin(), v.end()); // 把连续重复的移到末尾
v.erase(last, v.end()); // v = {1, 2, 3, 4, 5}
注意:如果要去除全部重复的元素(不考虑是否连续),需要先sort再unique,因为unique只处理相邻的重复项。
四、排序与搜索算法
排序是计算机科学中最基础也最核心的操作之一,C++标准库在这方面提供了工业级的实现和完善的接口。
4.1 sort:高效排序
C++的sort采用内省排序,结合了快速排序、堆排序和插入排序的优点------快速排序处理大规模数据,堆排序在递归过深时兜底防退化,插入排序处理小规模数据时的常数因子极低。平均时间复杂度O(n log n),最坏情况也被控制在O(n log n)。
cpp
vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
sort(v.begin(), v.end()); // 升序
sort(v.begin(), v.end(), greater<int>()); // 降序(greater在<functional>中)
sort(v.begin(), v.end(), [](int a, int b) { return a > b; }); // 降序(Lambda写法)
自定义类型排序:
cpp
struct Student {
string name;
int score;
};
vector<Student> students = {{"Alice", 85}, {"Bob", 90}, {"Charlie", 85}};
// 按分数降序,分数相同按姓名升序
sort(students.begin(), students.end(), [](const Student& a, const Student& b) {
return tie(b.score, a.name) < tie(a.score, b.name);
// 等价于: if (a.score != b.score) return a.score > b.score;
// return a.name < b.name;
});
注意:sort要求随机访问迭代器,因此适用于vector、deque、array和原始数组,但不能直接用于list------list有专属的list::sort()成员函数(内部实现为归并排序)。
4.2 stable_sort:保持等价元素的原有顺序
cpp
// stable_sort保证相等元素的相对位置不变
stable_sort(students.begin(), students.end(), [](const Student& a, const Student& b) {
return a.score > b.score;
});
// 同分的Alice和Charlie保持原有顺序
4.3 二分搜索算法

在已排序的序列上,二分搜索能将查找耗时从O(n)降到O(log n):
cpp
vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9};
bool found = binary_search(v.begin(), v.end(), 5); // true
// lower_bound:第一个>=目标的位置
auto lb = lower_bound(v.begin(), v.end(), 5); // 指向5
// upper_bound:第一个>目标的位置
auto ub = upper_bound(v.begin(), v.end(), 5); // 指向6
// 结合使用:找到所有等于目标的范围
auto [first, last] = equal_range(v.begin(), v.end(), 5);
// first指向5, last指向6,[first, last)是所有等于5的元素范围
4.4 归并与集合算法
merge:合并两个已排序的序列。
cpp
vector<int> a = {1, 3, 5};
vector<int> b = {2, 4, 6};
vector<int> result(6);
merge(a.begin(), a.end(), b.begin(), b.end(), result.begin());
// result = {1, 2, 3, 4, 5, 6}
set_union / set_intersection / set_difference:这些算法同样要求输入序列已排序。它们的实现基于有序序列的双指针归并,时间复杂度O(n+m)。这在需要求"同时满足A和B条件的集合"时非常有用。
五、数值算法
数值算法定义在<numeric>头文件中,是数据聚合和统计的好帮手。
5.1 accumulate:经典折叠求和
cpp
#include <numeric>
vector<int> v = {1, 2, 3, 4, 5};
int sum = accumulate(v.begin(), v.end(), 0); // 15
int product = accumulate(v.begin(), v.end(), 1, multiplies<int>()); // 120
accumulate的第三个参数是初始值,第四个参数是二元运算(默认是plus<>())。它从左到右依次执行op(op(op(init, x1), x2), x3)...。
5.2 reduce:并行版折叠(C++17)
reduce是C++17引入的、支持并行执行策略的折叠运算。与accumulate的主要区别在于:reduce不保证运算顺序(允许并行优化),因此要求运算满足结合律和交换律。
cpp
#include <numeric>
#include <execution>
int sum = reduce(execution::par, v.begin(), v.end(), 0); // 并行求和
5.3 transform_reduce:映射+归约一步到位(C++17)
当你需要先对每个元素做某种变换、再求和时,可以直接用transform_reduce,一步到位避免了生成中间结果的开销:
cpp
// 计算每个元素的平方和:1² + 2² + 3² + 4² + 5² = 55
int sumSq = transform_reduce(v.begin(), v.end(), 0, plus<>(), [](int x) { return x * x; });
5.4 inner_product / adjacent_difference
cpp
// 内积(点积)
vector<int> price = {10, 20, 30};
vector<int> qty = {2, 3, 4};
int revenue = inner_product(price.begin(), price.end(), qty.begin(), 0); // 10*2+20*3+30*4=200
// 相邻差分
vector<int> v = {1, 3, 6, 10};
vector<int> diff(4);
adjacent_difference(v.begin(), v.end(), diff.begin());
// diff = {1, 2, 3, 4}
六、C++17 并行算法:多核CPU
C++17引入了一项里程碑式的升级:STL算法支持并行执行策略,让标准算法可以利用多核CPU进行并行计算。

6.1 四种执行策略
C++17定义了三种执行策略,C++20补充了一种:
| 执行策略 | 说明 | 使用前提 |
|---|---|---|
| std::execution::seq | 顺序执行(默认行为) | 无 |
| std::execution::par | 允许并行执行 | 操作不能有数据竞争,不能抛异常 |
| std::execution::par_unseq | 允许并行+向量化 | 操作不能有同步/内存分配 |
| std::execution::unseq | 仅允许向量化(C++20) | 操作不能有同步/内存分配 |
6.2 使用示例
cpp
#include <algorithm>
#include <execution>
#include <vector>
vector<int> v(1000000);
// 串行排序
sort(execution::seq, v.begin(), v.end());
// 并行排序------多个核心一起干
sort(execution::par, v.begin(), v.end());
// 并行+向量化------能并行的并行,能做SIMD的就做SIMD
sort(execution::par_unseq, v.begin(), v.end());
数据量很大时(通常10万元素以上),并行排序可以带来2~4倍的加速。但并行策略不是免费的------它有调度开销和同步成本,如果任务太小,并行反而可能更慢。
并行算法对可调用对象的行为有更严格的要求------不允许抛异常、不允许数据竞争、不允许内存分配(对par_unseq)。对于新手来说,先了解并行策略的存在,在确实有性能需求时再深入研究和使用。
七、C++20 Ranges:范式重塑
如果说前面的内容是在学习一套工具的使用方法,那么C++20的Ranges库则是给你换了一种思维方式------从"怎么操作"转变为"想达成什么效果"。
7.1 Ranges解决了什么痛点?
传统STL算法有两个固有的不便之处:
- 始终需要传迭代器对:sort(v.begin(), v.end()),"begin"和"end"出现了,它们几乎一模一样,但你必须写两遍。
- 算法之间难以直接组合:要"过滤+转换+排序",你得写好几层中间变量,代码啰嗦又不好维护。
Ranges库就是为了解决这两个问题而诞生的。它让你直接传整个容器作为范围(Range),并通过管道操作符|将多个操作串联起来。
7.2 新基础:直接传容器
C++20起,所有算法的std::ranges版本都支持直接传容器:
cpp
// 传统写法
sort(v.begin(), v.end());
// C++20 ranges写法------v本身就是范围
ranges::sort(v);
这种写法除了更简洁,还有一个显著的安全性提升:避免了"迭代器来自不同容器"的乌龙------sort(v1.begin(), v2.end())这种错误代码,传统写法无法在编译期检测到,ranges::sort则能通过返回类型约定在编译期给出更清晰的错误信息。
7.3 投影(Projection):告别"鸡肋"Lambda
Ranges算法的参数列表中,很多都预留了一个投影函数的位置------让你在执行主操作之前,先提取或转换元素的某个属性,而不需要写冗长的Lambda。
cpp
struct Person {
string name;
int age;
};
vector<Person> people = {{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}};
// 传统写法:需要Lambda提取age
sort(people.begin(), people.end(), [](const Person& a, const Person& b) {
return a.age < b.age;
});
// C++20 ranges:用&Person::age作为投影,按age排序
ranges::sort(people, {}, &Person::age);
// 这里的{}是占位比较器,表示使用默认的升序比较
投影的妙处在于:它不仅可以是成员指针,也可以是任何一元可调用对象------你可以用Lambda、函数指针、函数对象等作为投影,在对元素执行主操作之前先做一层"数据预处理",而不用修改容器中存储的实际元素。比如查找第一个名字长度超过5的人:
cpp
auto it = ranges::find(people, true, [](const Person& p) { return p.name.size() > 5; });
// 投影将每个Person映射为bool(名字长度是否>5),然后find找第一个结果为true的元素
7.4 视图(Views)与管道
这是Ranges库最富魅力的部分------视图适配器允许你通过管道操作符|将多个处理步骤组合成一条"数据处理流水线",并且视图是惰性求值的:只有在真正遍历时,所有操作才会层层执行,不会生成中间结果。
cpp
#include <ranges>
using namespace std::ranges;
using namespace std::views;
vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
// 任务:取出v中的偶数,求平方,取前3个,并转为字符串
auto result = v
| filter([](int x) { return x % 2 == 0; }) // 只保留偶数
| transform([](int x) { return x * x; }) // 每个元素平方
| take(3) // 只取前3个
| transform([](int x) { return to_string(x); }); // 转为字符串
for (const auto& s : result) {
cout << s << " ";
}
// 输出: 4 16 36
这看起来是不是更像其他高级语言里的"函数式编程"风格了?从思考"怎么做"变成了宣告"要什么"。
常用视图一览:
| 视图 | 功能 | 示例 |
|---|---|---|
| filter | 保留满足条件的元素 | v | filter([](int x) { return x > 0; }) |
| transform | 对每个元素做变换 | v | transform([](int x) { return x * x; }) |
| take | 只取前n个 | v | take(5) |
| drop | 跳过前n个 | v | drop(3) |
| reverse | 反转序列 | v | reverse |
| join | 扁平化嵌套范围 | vv | join |
| iota | 生成序列 | views::iota(0, 10) → {0, 1, ..., 9} |
| common | 将范围转为begin()/end()兼容 | 用于与传统代码互操作 |
八、算法选择速查指南
面对100+个算法,选哪个?

查找某个元素?
· 按值查找 → find
· 按条件查找 → find_if
· 在已排序序列中 → binary_search(更快)
统计数量?
· 按值统计 → count
· 按条件统计 → count_if
遍历/操作每个元素?
· 只读 → 范围for循环(简单场景用range-for,复杂场景用for_each)
· 修改/转换 → transform
· 批量复制 → copy / copy_if
排序?
· 普通排序 → sort(不稳定)、stable_sort(稳定)
· 只需要部分 → partial_sort
· 只需要第n个元素 → nth_element
删除元素?
· 删除特定值 → remove + erase
· 删除满足条件的 → remove_if + erase
· C++20直接 erase_if
聚合/求和?
· 经典求和 → accumulate
· 并行求和 → reduce(C++17)
· 映射后聚合 → transform_reduce(C++17)
· C++23推荐 → ranges::fold_left
九、现代C++算法新特性速览(C++17到C++26)
C++的算法库一直在进化,以下是值得新手了解的更新脉络:
C++17:引入四种执行策略,让近70种标准算法支持并行执行。reduce、transform_reduce等新算法加入。
C++20:Ranges版本算法------std::ranges命名空间下提供了大多数传统算法的"约束版本",支持传容器、投影和管道组合。contains方法在关联容器中直接可用。
C++23:折叠算法ranges::fold_left/fold_right终于进入标准,解决了accumulate迟迟没有ranges版本的历史遗留问题。std::ranges::to<>让从视图快速构建容器成为可能(如auto v = view | ranges::to();)。
C++26:constexpr算法全面化。C++26放宽了编译期约束,不仅vector和string可以在constexpr上下文中使用,绝大多数的STL算法------包括sort、find、copy、transform、accumulate等------也都可以在编译期执行。你可以在编译期间对一个数据序列做排序、查找、变换,将结果"固化"进二进制文件。
十、常见陷阱与最佳实践
陷阱排行榜
| 陷阱 | 严重程度 | 表现 | 解决方案 |
|---|---|---|---|
| erase-remove没写erase | 致命 | 容器大小没变,末尾有垃圾值 | v.erase(remove(...), v.end()); |
| 在空容器上copy目标没分配空间 | 崩溃 | 写入未知内存 | 用back_inserter或预先resize |
| 对未排序序列使用binary_search | 逻辑错误 | 结果不可预测 | 先排序或使用find |
| 迭代器失效后继续使用 | 崩溃 | 野指针/段错误 | 注意容器修改操作对迭代器的影响 |
| remove用在map/set等关联容器上 | 编译错误 | 关联容器有专属的erase重载 | 直接调用容器的erase |
| 对list使用通用sort | 编译错误 | sort需要随机访问迭代器 | 用list::sort()成员函数 |
最佳实践
- 能用算法就别写循环------代码更短、意图更明、Bug更少。
- 优先用sort + binary_search代替手写查找与排序。
- Lambda是算法的最好搭档,让定制逻辑紧凑且可读。
- 删除元素用remove_if + erase的经典组合,或者C++20直接用erase_if。
- 需要极致性能时考虑并行策略(C++17起),但确保操作满足并行安全要求。
- 逐步拥抱Ranges------新项目建议优先使用std::ranges版本。
- constexpr算法在元编程和编译期数据处理中潜力巨大------了解趋势,适时使用。
十一、动手实践
打开你的Visual Studio,把下面的代码跑起来:
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <execution>
using namespace std;
int main() {
// 1. 查找与排序
cout << "=== find & sort ===" << endl;
vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
auto it = find(v.begin(), v.end(), 5);
if (it != v.end()) cout << "找到5,位置: " << distance(v.begin(), it) << endl;
sort(v.begin(), v.end());
cout << "排序后: ";
for (int x : v) cout << x << " ";
cout << endl;
// 2. 聚合与转换
cout << "\n=== accumulate & transform ===" << endl;
int sum = accumulate(v.begin(), v.end(), 0);
double avg = static_cast<double>(sum) / v.size();
cout << "总和: " << sum << ", 平均值: " << avg << endl;
vector<int> squares(v.size());
transform(v.begin(), v.end(), squares.begin(), [](int x) { return x * x; });
cout << "平方: ";
for (int x : squares) cout << x << " ";
cout << endl;
// 3. erase-remove删除元素
cout << "\n=== erase-remove ===" << endl;
int before = v.size();
v.erase(remove_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; }), v.end());
cout << "删除偶数前: " << before << " 个元素, 删除后: " << v.size() << " 个元素" << endl;
cout << "剩余: ";
for (int x : v) cout << x << " ";
cout << endl;
// 4. 二分搜索
cout << "\n=== binary_search ===" << endl;
// v已经是排序且过滤后的,可以直接二分
cout << "3存在吗? " << (binary_search(v.begin(), v.end(), 3) ? "是" : "否") << endl;
// 5. all_of / any_of / none_of
cout << "\n=== 逻辑判断 ===" << endl;
vector<int> test = {2, 4, 6, 8};
cout << boolalpha;
cout << "全为偶数? " << all_of(test.begin(), test.end(), [](int x) { return x % 2 == 0; }) << endl;
cout << "有>5的? " << any_of(test.begin(), test.end(), [](int x) { return x > 5; }) << endl;
cout << "没有负数? " << none_of(test.begin(), test.end(), [](int x) { return x < 0; }) << endl;
return 0;
}
十二、总结
恭喜你!现在你已经掌握了用标准算法代替手写循环的核心能力------你的C++代码从此更短、更快、更不容易出错。
快速回顾:
· algorithm和numeric是两大算法头文件,提供100+个标准算法,通过迭代器与容器解耦。
· 非修改算法:find/find_if、count/count_if、all_of/any_of/none_of------只读不写,数据验证好帮手。
· 修改算法:copy/transform/replace/reverse------数据处理的日常工作。remove必须搭配erase才能真删除。
· 排序与二分:sort(内省排序)、stable_sort、binary_search、lower_bound/upper_bound------高效查找与有序世界的入口。
· 数值算法:accumulate、reduce(C++17)、transform_reduce(C++17)------聚合与统计。
· 并行策略(C++17):seq/par/par_unseq/unseq,大数据量时加速2~4倍。
· Ranges(C++20):传容器、投影、管道适配器------从"怎么操作"转变为"要什么结果"。
思考题:
- remove为什么必须和erase配合才能真正删除元素?它自己到底做了什么?
- sort和stable_sort的区别是什么?给一个场景说明什么时候必须用stable_sort。
- C++20 Ranges的filter和transform视图是"急切求值"还是"惰性求值"?这意味着什么?
- 并行算法(execution::par)一定能比串行算法快吗?什么时候可能反而更慢?
- C++23的ranges::fold_left和传统的accumulate相比,为什么前者更"rangified"?它在接口上有哪些改进?
在下一篇文章中,我们将学习C++的模板与泛型编程------这是写出可复用代码的核心武器。等读完那篇你就会明白,vector<int>和vector<double>的底层代码只写了一份,而你已经在用它这么多年了!
谢谢大家!!!
------ 一个曾因连写七个不同容器的手动遍历循环、被别人问"你听说过for_each吗"的C++学习者