一、算法概述:STL 算法的分类与设计哲学
STL 算法,说白了就是一组操作容器的函数模板。它们不依赖具体容器,只通过迭代器工作。这个设计非常巧妙——你写一次 sort,就能对 vector、deque、array 甚至原生数组排序。
我个人习惯把算法分成四大类:
| 分类 | 特点 | 典型代表 |
|---|---|---|
| 只读算法 | 不修改元素,只做查找、计数、判断 | find, count, for_each, all_of |
| 修改算法 | 会改变容器内容 | copy, replace, fill, remove |
| 排序与相关操作 | 排序、二分查找、合并、堆操作 | sort, stable_sort, binary_search |
| 数值算法 | 数学计算,定义在 <numeric> 中 | accumulate, inner_product, partial_sum |
嗯,这里要注意:只读算法是日常开发中使用频率最高的。你想想看,大部分业务逻辑其实就是在数据里找东西、数个数、做判断。所以这一章我们重点啃它。
核心原则:STL 算法通过迭代器与容器解耦。同一个 find 函数,可以查找 vector、list、set,甚至 C 风格数组。这就是泛型编程的魅力。
二、只读算法详解
2.1 find / find_if:查找元素
find 是最基础的线性查找。它从迭代器范围中找出第一个等于目标值的元素,返回指向它的迭代器。如果没找到,返回 end()。
我在项目中遇到过一个问题:用 find 在 vector 里查找自定义类型,结果死活找不到。后来才发现——忘了重载 operator==。嗯,这是个经典坑。
#include <algorithm>
#include <vector>
#include <iostream>
struct User {
int id;
std::string name;
// 必须重载 ==,find 才能工作
bool operator==(const User& other) const {
return id == other.id;
}
};
int main() {
std::vector<User> users = {{1, "Alice"}, {2, "Bob"}, {3, "Charlie"}};
auto it = std::find(users.begin(), users.end(), User{2, ""});
if (it != users.end()) {
std::cout << "找到用户: " << it->name << "\n";
}
return 0;
}
find_if 更灵活——它接受一个谓词(函数或 lambda),只要条件满足就算找到。
// 查找第一个名字长度大于 5 的用户
auto it = std::find_if(users.begin(), users.end(),
[](const User& u) { return u.name.length() > 5; });
我的习惯:能用 find_if 就别用裸循环。lambda 表达式让意图一目了然,而且编译器优化起来更顺手。
2.2 count / count_if:统计出现次数
count 返回等于某个值的元素个数。count_if 返回满足条件的元素个数。它们都是 O(n) 的线性扫描。
你想想看,什么时候用 count 而不是 find?
- 需要知道「有多少个」而不是「有没有」
- 判断重复数据是否超过阈值
- 统计日志中某种级别的错误数量
std::vector<int> scores = {85, 92, 78, 92, 88, 92};
int count_92 = std::count(scores.begin(), scores.end(), 92);
// count_92 == 3
int count_above_90 = std::count_if(scores.begin(), scores.end(),
[](int s) { return s > 90; });
// count_above_90 == 3
我曾经踩过的坑:在 unordered_set 上用 count 判断元素是否存在。虽然能工作,但语义不对——set 里元素唯一,count 返回 0 或 1。更清晰的做法是用 find 或 contains(C++20)。
2.3 for_each:遍历并执行操作
for_each 对范围内的每个元素执行一个可调用对象。它是最「古老」的遍历算法,C++11 之后有了范围 for 循环,但 for_each 仍有其价值。
我个人习惯在两种场景下用 for_each:
- 需要把遍历逻辑封装成函数对象,方便复用
- 配合并行策略(C++17),实现并行遍历
// 打印每个元素
std::for_each(scores.begin(), scores.end(),
[](int s) { std::cout << s << " "; });
// 累加(虽然用 accumulate 更直接)
int sum = 0;
std::for_each(scores.begin(), scores.end(),
[&sum](int s) { sum += s; });
注意:for_each 返回传入的可调用对象。这意味着你可以在 lambda 中维护状态,最后通过返回值获取。不过说实话,这个特性用得不多,我更喜欢用引用捕获。
2.4 all_of / any_of / none_of:范围判断三兄弟
这三个算法是 C++11 引入的,专门用来回答「范围内元素是否全部/任意/没有满足条件」。它们返回 bool,语义非常清晰。
| 算法 | 返回 true 的条件 | 空范围时的返回值 |
|---|---|---|
| all_of | 所有元素都满足谓词 | true(空集的所有元素都满足) |
| any_of | 至少一个元素满足谓词 | false |
| none_of | 没有元素满足谓词 | true |
std::vector<int> data = {2, 4, 6, 8, 10};
bool all_even = std::all_of(data.begin(), data.end(),
[](int x) { return x % 2 == 0; });
// true
bool any_negative = std::any_of(data.begin(), data.end(),
[](int x) { return x < 0; });
// false
bool none_odd = std::none_of(data.begin(), data.end(),
[](int x) { return x % 2 != 0; });
// true
我的建议:写条件判断时,优先用这三个算法而不是手写循环。代码更短,意图更明确。比如「检查所有用户是否都已激活」——all_of 一行搞定,比 for 循环加 flag 变量优雅得多。
三、实战:日志分析工具
理论说完了,我们来点实际的。假设你有一个日志文件,每行是一条日志记录。你需要分析:
- 统计不同级别的日志数量
- 查找第一条 ERROR 日志
- 检查是否所有日志都包含时间戳
- 输出所有 WARNING 日志
下面是我写的一个简化版日志分析工具,用到了本章所有只读算法。
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <fstream>
struct LogEntry {
std::string level; // INFO, WARNING, ERROR
std::string message;
bool has_timestamp;
};
int main() {
// 模拟从文件读取的日志数据
std::vector<LogEntry> logs = {
{"INFO", "Server started", true},
{"WARNING", "Disk usage above 80%", true},
{"ERROR", "Connection timeout", true},
{"INFO", "Request processed", false},
{"ERROR", "Out of memory", true},
{"WARNING", "CPU temperature high", true}
};
// 1. 统计 ERROR 数量
int error_count = std::count_if(logs.begin(), logs.end(),
[](const LogEntry& e) { return e.level == "ERROR"; });
std::cout << "ERROR 数量: " << error_count << "\n";
// 2. 查找第一条 ERROR 日志
auto it = std::find_if(logs.begin(), logs.end(),
[](const LogEntry& e) { return e.level == "ERROR"; });
if (it != logs.end()) {
std::cout << "第一条 ERROR: " << it->message << "\n";
}
// 3. 检查是否所有日志都有时间戳
bool all_timestamped = std::all_of(logs.begin(), logs.end(),
[](const LogEntry& e) { return e.has_timestamp; });
std::cout << "所有日志都有时间戳: "
<< (all_timestamped ? "是" : "否") << "\n";
// 4. 检查是否有 WARNING 日志
bool has_warning = std::any_of(logs.begin(), logs.end(),
[](const LogEntry& e) { return e.level == "WARNING"; });
std::cout << "存在 WARNING 日志: "
<< (has_warning ? "是" : "否") << "\n";
// 5. 输出所有 WARNING 日志(用 for_each)
std::cout << "WARNING 日志列表:\n";
std::for_each(logs.begin(), logs.end(),
[](const LogEntry& e) {
if (e.level == "WARNING") {
std::cout << " - " << e.message << "\n";
}
});
// 6. 检查是否没有 FATAL 级别日志
bool no_fatal = std::none_of(logs.begin(), logs.end(),
[](const LogEntry& e) { return e.level == "FATAL"; });
std::cout << "没有 FATAL 日志: "
<< (no_fatal ? "是" : "否") << "\n";
return 0;
}
输出结果:
ERROR 数量: 2
第一条 ERROR: Connection timeout
所有日志都有时间戳: 否
存在 WARNING 日志: 是
WARNING 日志列表:
- Disk usage above 80%
- CPU temperature high
没有 FATAL 日志: 是
实战要点:
- 用
count_if替代手写计数循环,代码量减少 50% find_if找到第一个匹配项后立即停止,比遍历整个容器高效all_of / any_of / none_of让条件判断变成一句话,可读性极强for_each配合 lambda,适合需要「副作用」的遍历操作
说实话,这个例子虽然简单,但覆盖了日常开发中 80% 的只读算法使用场景。你想想看,日志分析、数据校验、报表统计——这些业务逻辑本质上就是查找、计数、判断、遍历。掌握了这几个算法,你写出来的代码会更简洁、更不容易出错。
我的经验:刚开始用 STL 算法时,总觉得不如手写循环「直观」。但用习惯了就会发现,算法把循环的控制逻辑和业务逻辑分开了——你不用关心怎么遍历,只关心对每个元素做什么。这就是抽象的力量。
公众号:蓝海资料掘金营,微信deep3321