写循环遍历容器,是每个 C++ 新手每天的日常;但老手更习惯说:这件事交给 <algorithm>。排序、查找、统计、变换,STL 算法库全都有现成的,配合 lambda 表达式,常常一行顶你十行手写循环。本文用成绩处理的实战场景,把这套组合拳拆开讲清楚,学完你就能写出"意图自明"的代码。
1. 算法库解决什么问题
先看一段"新手代码":手动遍历找最大值、手动计数,每个都要写循环、管下标,还容易出错:
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores{85, 92, 78, 60, 88};
// 手写:找最高分
int max_score = scores[0];
for (std::size_t i = 1; i < scores.size(); ++i) {
if (scores[i] > max_score) max_score = scores[i];
}
// 手写:数及格人数
int pass = 0;
for (int s : scores) if (s >= 60) ++pass;
std::cout << "最高分 " << max_score << ",及格 " << pass << " 人\n";
return 0;
}
这段代码能跑,但每个"意图"(找最大、数及格)都被循环细节淹没:读代码的人要先理解循环结构,才能猜出作者想干什么。算法的思路是:把"遍历方式"和"判断规则"分离——遍历交给算法,规则交给 lambda,代码读起来就是一句句的意图声明。<algorithm> 里有上百个这样的算法,本文挑最常用的几个讲透。
2. lambda 速成
lambda(匿名函数)是算法的黄金搭档,语法三件套:方括号捕获、圆括号参数、花括号函数体:
#include <iostream>
int main() {
int base = 10;
// [base] 按值捕获:lambda 内部是 base 的副本
auto add_base = [base](int x) { return x + base; };
// [&base] 按引用捕获:可以修改外部变量
auto bump = [&base]() { base += 100; };
std::cout << add_base(5) << "\n"; // 15
bump();
std::cout << base << "\n"; // 110
// [=] 全部按值捕获,[&] 全部按引用捕获,按需使用
auto all = [=]() { return base + 1; };
std::cout << all() << "\n"; // 111
return 0;
}
捕获列表决定 lambda 能用哪些外部变量:[x] 复制、[&x] 引用、[=]/[&] 全部。注意按值捕获的 base 是副本,改它不影响外面;按引用捕获则相反,但引用捕获有"悬空"风险——lambda 存活时间超过被捕获变量的生命周期就会出问题,所以默认优先按值捕获。缺省情况下 lambda 的 operator() 是 const 的,想修改捕获的副本要加 mutable,但实践中很少用。
3. sort:排序与自定义规则
std::sort 默认升序,传第三个参数自定义比较规则。排序"学生"这种自定义结构,规则就得靠 lambda:
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
struct Student {
std::string name;
int score;
};
int main() {
std::vector<Student> students{
{"张三", 85}, {"李四", 92}, {"王五", 78}, {"赵六", 92}};
// 按成绩降序
std::sort(students.begin(), students.end(),
[](const Student& a, const Student& b) {
return a.score > b.score;
});
// 成绩相同按姓名升序:stable_sort 保证相等元素的相对顺序
std::stable_sort(students.begin(), students.end(),
[](const Student& a, const Student& b) {
if (a.score != b.score) return a.score > b.score;
return a.name < b.name;
});
for (const auto& s : students)
std::cout << s.name << " " << s.score << "\n";
return 0;
}
比较 lambda 返回"a 是否应该排在 b 前面"。两个 92 分的同学,sort 不保证相对顺序,stable_sort 保证——所以第二次排序后"李四"一定在"赵六"前面。记住比较器必须满足严格弱序(a<b 为真则 b<a 必假),否则 sort 行为未定义,这是新手最容易踩的坑。另外,如果只想取前几名,不必完整排序:std::partial_sort 只保证前 k 个元素有序,复杂度 O(n log k);想快速找"第 k 大"用 std::nth_element,平均 O(n)。这两个算法在排行榜、中位数这类场景里比 sort 快一个量级。
4. find 家族:查找元素
find 找"等于某值"的第一个元素,find_if 找"满足条件"的第一个元素,都返回迭代器,找不到就返回 end():
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> names{"张三", "李四", "王五", "李四"};
auto it = std::find(names.begin(), names.end(), "李四");
if (it != names.end())
std::cout << "找到: " << *it << "\n";
// find_if:找第一个长度大于 2 的名字
auto it2 = std::find_if(names.begin(), names.end(),
[](const std::string& s) { return s.size() > 2; });
if (it2 != names.end())
std::cout << "第一个超过 2 字的名字: " << *it2 << "\n";
// 想数"所有"匹配的,配合循环推进起点
std::size_t count = 0;
for (auto p = names.begin(); p != names.end();) {
p = std::find(p, names.end(), "李四");
if (p == names.end()) break;
++count;
++p;
}
std::cout << "\"李四\" 出现 " << count << " 次\n";
return 0;
}
三个要点:一是判断必须用 != end(),直接解引用未找到的迭代器是未定义行为;二是 find_if 接收"谓词",任何能返回 bool 的可调用对象都行;三是 find 只返回第一个匹配,数次数可以循环推进起点,不过更专业的场景有 std::count 一行搞定。性能提示:在无序 vector 上 find 是 O(n),如果查找非常频繁,考虑换 std::set(O(log n))或 std::unordered_set(平均 O(1)),但换来的是插入变慢、内存变大。算法选型要结合整个程序的热点,不能只看单次操作。
5. for_each 与 transform:遍历与变换
for_each 对每个元素执行副作用,transform 把每个元素"映射"成新值:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores{85, 92, 78, 60, 88};
// for_each:打印每个分数
std::for_each(scores.begin(), scores.end(),
[](int s) { std::cout << s << " "; });
std::cout << "\n";
// transform:每个分数 +5,存入新容器
std::vector<int> adjusted(scores.size());
std::transform(scores.begin(), scores.end(), adjusted.begin(),
[](int s) { return s + 5; });
// 原地变换:目标起点写成源起点即可
std::transform(scores.begin(), scores.end(), scores.begin(),
[](int s) { return s * 2; });
for (int s : adjusted) std::cout << s << " ";
std::cout << "| ";
for (int s : scores) std::cout << s << " ";
std::cout << "\n";
return 0;
}
transform 的三参数版本是"源起点、源终点、目标起点",输出到新容器时要提前 resize 出空间;把目标起点写成源起点就是原地变换。两者的分工要分清:for_each 关注"过程"(打印、计数、修改外部状态),transform 关注"结果"(产生新值)。C++20 之后还有 ranges 版本 std::views::transform,写法更优雅,但原理一样。
6. 统计与更多实用算法
count_if 与 accumulate
统计及格人数用 count_if,求总和/平均值用 accumulate(在 <numeric> 里):
#include <algorithm>
#include <iostream>
#include <numeric>
#include <string>
#include <vector>
int main() {
std::vector<int> scores{85, 92, 78, 60, 88};
int pass = std::count_if(scores.begin(), scores.end(),
[](int s) { return s >= 60; });
int total = std::accumulate(scores.begin(), scores.end(), 0);
double avg = static_cast<double>(total) / scores.size();
std::cout << "及格 " << pass << " 人,平均分 " << avg << "\n";
// accumulate 的第四参可以自定义"怎么加"
std::string joined = std::accumulate(
scores.begin(), scores.end(), std::string(),
[](const std::string& acc, int s) {
return acc + (acc.empty() ? "" : ",") + std::to_string(s);
});
std::cout << joined << "\n"; // 85,92,78,60,88
return 0;
}
count_if 统计满足条件的个数;accumulate 是"归约":从初值 0 开始,把每个元素按加法合并。第四参数是自定义合并函数,这里用它把整数拼成逗号分隔的字符串——同一个算法,换个"加法"就换了用途。同类的常用组合还有:std::adjacent_find 在有序区间里找相邻重复,std::equal 比较两个区间是否逐元素相同,std::fill 批量赋值。STL 算法的命名非常直白,用到时去 cppreference 按分类查,比硬记快得多。
min/max、区间判断与二分查找
还有一组高频算法:找极值、判断"是否全部/任意/没有满足条件"、在有序区间里二分查找:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores{85, 92, 78, 60, 88};
auto max_it = std::max_element(scores.begin(), scores.end());
auto min_it = std::min_element(scores.begin(), scores.end());
std::cout << "最高 " << *max_it << ",最低 " << *min_it << "\n";
// 区间判断:全部及格?有人上 90?没人低于 50?
bool all_pass = std::all_of(scores.begin(), scores.end(),
[](int s) { return s >= 60; });
bool any_high = std::any_of(scores.begin(), scores.end(),
[](int s) { return s >= 90; });
bool none_low = std::none_of(scores.begin(), scores.end(),
[](int s) { return s < 50; });
std::cout << all_pass << " " << any_high << " " << none_low
<< "\n"; // 1 1 1
// 二分查找:只对有序区间有效,O(log n)
std::sort(scores.begin(), scores.end());
bool found = std::binary_search(scores.begin(), scores.end(), 78);
std::cout << "78 是否存在: " << found << "\n"; // 1
return 0;
}
max_element/min_element 返回迭代器,记得解引用;三个 *_of 把"区间是否满足性质"表达成一条语句,配合返回的 bool 直接进 if;二分查找比 std::find 快得多,但前提是区间已排序——对无序数据用它结果未定义,这是必须记住的前提条件。
7. 实战:erase-remove 惯用法
从容器里"删除所有满足条件的元素",直接 erase 会让迭代器失效,新手常踩坑。标准做法是 remove_if + erase 两步:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores{85, 92, 78, 60, 88, 45, 59};
// 删除所有不及格的
auto new_end = std::remove_if(scores.begin(), scores.end(),
[](int s) { return s < 60; });
scores.erase(new_end, scores.end()); // 真正缩短容器
std::cout << "剩余 " << scores.size() << " 人:";
for (int s : scores) std::cout << " " << s;
std::cout << "\n"; // 剩余 5 人: 85 92 78 60 88
return 0;
}
remove_if 不会缩短容器,它把"幸存者"搬到前面,返回新逻辑末尾的迭代器;真正的删除交给 erase。这两步连写就是著名的 erase-remove 惯用法,对所有顺序容器(vector、deque、string)通用,比手写循环删除安全得多——手写循环删除时,erase 会让当前迭代器失效,稍不留神就漏元素或越界。值得说明的是,remove 系列只保证"逻辑删除":被删元素的值还残留在容器尾部,直到 erase 把那段空间真正释放,所以两步缺一不可。
8. 总结与练习
这一篇你掌握了:lambda 的捕获与写法、sort/stable_sort 的自定义排序、find/find_if 的查找、for_each/transform 的遍历变换、count_if/accumulate 的统计、极值与区间判断、二分查找,以及 erase-remove 惯用法。核心心法:遍历交给算法、规则交给 lambda、判断用 != end()、有序才能二分。这些算法组合起来,基本可以覆盖日常数据处理的全部需求。
练习题:
- 用
std::max_element找最高分,再用std::min_element找最低分,输出它们的差。 - 把第 7 节的逻辑反过来:保留不及格的、删除及格的,用同一个惯用法实现。
- 给
Student写一个"任意字段排序"函数:接收一个 lambda 比较器参数,内部调用std::sort,体会算法与规则的解耦。
💡 看到"手写循环遍历容器"的冲动时,先想想 <algorithm> 里有没有现成的——标准库算法经过精心优化,正确性也更有保障,你的循环大概率没它快。