第11章节:STL算法与迭代器 - 数据处理的利器
11.1 STL算法的设计哲学
11.1.1 算法的通用性思维
想象你是一家餐厅的主厨,需要处理各种食材。无论是切蔬菜、调味还是装盘,这些操作技巧(算法)可以应用于不同的食材(数据)。STL算法的核心思想就是:同样的操作可以应用于不同类型的数据。
核心设计原则:
- 算法与容器分离:就像切菜技巧不依赖于具体的蔬菜种类
- 迭代器作为桥梁:就像刀具让你能够处理各种食材
- 函数对象定制行为:就像调味料让你定制菜品口味
- 性能保证:每个算法都有明确的性能特点
实际意义:你可以用同样的find算法在vector中查找数字,在list中查找字符串,在数组中查找自定义对象,而不需要为每种情况重写代码。
11.1.2 算法的分类体系
非修改性算法(只查看,不修改):
- 查找算法:
find、find_if- 就像在一堆书中找特定的书 - 计数算法:
count、count_if- 就像统计图书馆中某类书的数量 - 比较算法:
equal、mismatch- 就像比较两个书单是否一致
修改性算法(会改变数据):
- 复制算法:
copy、copy_if- 就像复印文档 - 变换算法:
transform、replace- 就像修改文档内容 - 填充算法:
fill、generate- 就像批量填写表格
排序算法:
- 排序:
sort、stable_sort- 就像整理书架 - 二分查找:
binary_search- 就像用索引快速找书 - 合并:
merge- 就像合并两个有序书单
数值算法:
- 累加:
accumulate- 就像计算购物清单总价 - 内积:
inner_product- 就像计算加权平均 - 部分和:
partial_sum- 就像计算累计消费
11.1.3 算法的性能保证
性能就像烹饪时间:
- O(1):常数时间 - 就像从桌子上拿盐(瞬间完成)
- O(n):线性时间 - 就像检查每颗蔬菜是否新鲜
- O(n log n):线性对数时间 - 就像用高效方法整理大量食材
- O(n²):平方时间 - 就像用最笨的方法整理食材(很少使用)
11.2 迭代器 - 数据的导航员
11.2.1 迭代器的层次结构
想象迭代器是不同类型的交通工具:
输入迭代器(Input Iterator):
- 特点:只能向前行驶,只能载客(读取数据)
- 用途:单程观光车,看一次就不能回头
- 限制:不能保存位置供以后使用
输出迭代器(Output Iterator):
- 特点:只能向前行驶,只能卸货(写入数据)
- 用途:送货卡车,只能送货不能查看货物
- 限制:不能查看已送的货物
前向迭代器(Forward Iterator):
- 特点:可以向前行驶,可以多次经过同一路线
- 用途:城市公交车,可以多次乘坐同一路线
- 优势:可以保存站点供以后使用
双向迭代器(Bidirectional Iterator):
- 特点:可以前进也可以倒车
- 用途:双向地铁,可以来回行驶
- 操作:
--iter就像地铁倒车
随机访问迭代器(Random Access Iterator):
- 特点:可以瞬间跳到任意位置
- 用途:传送门,可以直接跳到任意楼层
- 操作:
iter + n就像按电梯按钮直达目标楼层
11.2.2 迭代器标签与性能优化
标签系统就像交通分类:
// 不同的迭代器有不同的"行驶证"
struct input_iterator_tag {}; // 普通车辆
struct random_access_iterator_tag {}; // 紧急车辆(可以超速)
性能优化的例子:
// 计算距离的优化版本
template<typename RandomAccessIterator>
size_t distance_impl(RandomAccessIterator first, RandomAccessIterator last,
std::random_access_iterator_tag) {
return last - first; // 随机访问迭代器可以直接相减(瞬间完成)
}
template<typename InputIterator>
size_t distance_impl(InputIterator first, InputIterator last,
std::input_iterator_tag) {
size_t n = 0;
while (first != last) { // 普通迭代器需要一步步走
++first;
++n;
}
return n;
}
11.2.3 迭代器失效 - 导航系统的崩溃
迭代器失效就像GPS失效:
vector的失效情况(动态数组):
- 重新分配:就像商场扩建,原来的地图全部失效
- 插入/删除:就像队伍中间有人插队,后面所有人的位置都变了
list的失效情况(链表):
- 插入:就像队伍中新增一个人,不影响其他人的位置
- 删除:就像队伍中有人离开,只影响被删除的人
map的失效情况(红黑树):
- 插入/删除:就像图书馆新增/移除一本书,不影响其他书的位置
11.3 查找算法实战
11.3.1 线性查找 - 逐个检查
现实场景:在一堆未排序的简历中找某个人的简历
// 简单的线性查找
template<typename InputIterator, typename T>
InputIterator find(InputIterator first, InputIterator last, const T& value) {
while (first != last && !(*first == value)) {
++first; // 逐个检查
}
return first; // 找到或到达末尾
}
实际应用:
std::vector<int> scores = {85, 92, 78, 95, 88};
auto it = std::find(scores.begin(), scores.end(), 95);
if (it != scores.end()) {
std::cout << "找到95分,位置:" << std::distance(scores.begin(), it) << std::endl;
}
11.3.2 二分查找 - 高效搜索
现实场景:在电话簿中找人(电话簿是按字母顺序排列的)
前提条件:数据必须是有序的,就像字典必须按字母顺序排列
std::vector<int> sorted_scores = {78, 85, 88, 92, 95};
bool found = std::binary_search(sorted_scores.begin(), sorted_scores.end(), 88);
// 找到插入位置
auto insert_pos = std::lower_bound(sorted_scores.begin(), sorted_scores.end(), 90);
性能对比:
- 线性查找:O(n) - 就像从电话簿第一页开始翻
- 二分查找:O(log n) - 就像直接跳到电话簿中间
11.3.3 搜索算法 - 找子序列
现实场景:在一篇文章中找特定的短语
std::string text = "C++ is a powerful programming language";
std::string pattern = "powerful";
auto it = std::search(text.begin(), text.end(), pattern.begin(), pattern.end());
if (it != text.end()) {
std::cout << "找到匹配,位置:" << std::distance(text.begin(), it) << std::endl;
}
11.4 排序算法实战
11.4.1 内省排序 - 智能排序
introsort的工作原理:就像聪明的图书管理员
- 快速排序阶段:先用快速方法大致整理
- 深度监控:如果递归太深,切换到更稳定的方法
- 小数组优化:对小部分使用简单方法
std::vector<int> books = {5, 2, 8, 1, 9, 3};
std::sort(books.begin(), books.end()); // 使用introsort
// 结果:1, 2, 3, 5, 8, 9
性能特点:
- 平均情况:O(n log n) - 很快
- 最坏情况:O(n log n) - 避免了快速排序的最坏情况
- 空间复杂度:O(log n) - 只需要少量额外空间
11.4.2 稳定排序 - 保持相对顺序
现实场景:学生按成绩排序,但成绩相同的要保持原来的报名顺序
struct Student {
std::string name;
int score;
int registration_order; // 报名顺序
};
std::vector<Student> students = {
{"Alice", 85, 1}, {"Bob", 92, 2}, {"Charlie", 85, 3}
};
// 按成绩排序,但成绩相同保持报名顺序
std::stable_sort(students.begin(), students.end(),
[](const Student& a, const Student& b) {
return a.score > b.score; // 高分在前
});
// 结果:Bob(92,2), Alice(85,1), Charlie(85,3) - 85分的保持了报名顺序
11.4.3 部分排序 - 找前几名
现实场景:比赛只颁发前三名奖牌,只需要找出前三名
std::vector<int> scores = {85, 92, 78, 95, 88, 91, 87};
// 只要前三名
std::partial_sort(scores.begin(), scores.begin() + 3, scores.end(),
std::greater<int>());
// 结果:95, 92, 91, ... (前三名排好序,后面的不保证顺序)
11.5 数值算法实战
11.5.1 累加算法 - 计算总和
现实场景:计算购物车总价、统计总分
std::vector<double> prices = {19.99, 5.50, 3.25, 12.80};
// 简单累加
double total = std::accumulate(prices.begin(), prices.end(), 0.0);
// 自定义操作:计算总价并加8%税
double total_with_tax = std::accumulate(prices.begin(), prices.end(), 0.0,
[](double sum, double price) { return sum + price * 1.08; });
11.5.2 内积算法 - 加权计算
现实场景:计算加权平均分、计算点积
std::vector<int> scores = {85, 92, 78};
std::vector<double> weights = {0.3, 0.5, 0.2}; // 权重总和为1
// 计算加权平均分
double weighted_average = std::inner_product(scores.begin(), scores.end(),
weights.begin(), 0.0);
std::cout << "加权平均分:" << weighted_average << std::endl; // 87.1
11.5.3 部分和算法 - 累计计算
现实场景:计算累计消费、生成斐波那契数列
std::vector<int> daily_sales = {100, 150, 200, 120, 180};
std::vector<int> cumulative_sales(daily_sales.size());
// 计算累计销售额
std::partial_sum(daily_sales.begin(), daily_sales.end(), cumulative_sales.begin());
// 结果:100, 250, 450, 570, 750
11.6 函数对象与谓词 - 自定义行为
11.6.1 函数对象 - 可调用的对象
函数对象 vs 普通函数:
- 普通函数:就像标准菜谱,固定不变
- 函数对象:就像智能厨具,可以记住设置和状态
// 函数对象:可以保存状态
class GradeFilter {
int min_grade;
public:
GradeFilter(int min) : min_grade(min) {}
bool operator()(int grade) const {
return grade >= min_grade;
}
};
// 使用
std::vector<int> scores = {85, 92, 78, 95, 88};
std::vector<int> good_scores;
// 找出80分以上的成绩
std::copy_if(scores.begin(), scores.end(), std::back_inserter(good_scores),
GradeFilter(80));
11.6.2 谓词 - 条件判断
谓词就像筛选条件:
// 一元谓词:单个条件
auto is_even = [](int n) { return n % 2 == 0; };
// 二元谓词:比较条件
auto less_than = [](int a, int b) { return a < b; };
std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
// 统计偶数个数
int even_count = std::count_if(numbers.begin(), numbers.end(), is_even);
// 按自定义规则排序
std::sort(numbers.begin(), numbers.end(), less_than);
11.6.3 Lambda表达式 - 简洁的函数对象
Lambda就像即兴菜谱:简单、直接、就地使用
std::vector<Student> students = {
{"Alice", 85}, {"Bob", 92}, {"Charlie", 78}
};
// 用lambda找出高分学生
auto high_achievers = std::count_if(students.begin(), students.end(),
[](const Student& s) { return s.score >= 90; });
// 用lambda自定义排序
std::sort(students.begin(), students.end(),
[](const Student& a, const Student& b) {
return a.score > b.score; // 按分数降序
});
11.7 并行算法 - 多核加速
11.7.1 并行执行策略
就像多人协作:一个人打扫房间很慢,多人一起就快多了
#include <execution> // C++17并行算法
std::vector<int> large_data(1000000);
// 顺序执行(单线程)
std::for_each(std::execution::seq, large_data.begin(), large_data.end(),
[](int& x) { x = x * x; });
// 并行执行(多线程)
std::for_each(std::execution::par, large_data.begin(), large_data.end(),
[](int& x) { x = x * x; });
// 并行且可向量化
std::for_each(std::execution::par_unseq, large_data.begin(), large_data.end(),
[](int& x) { x = x * x; });
11.7.2 并行算法的适用场景
适合并行的场景:
- 数据量大:处理百万级数据
- 操作独立:每个元素的处理不依赖其他元素
- 计算密集:不是简单的内存操作
// 并行计算大数组的和
std::vector<double> data(10000000, 1.0);
double sum = std::reduce(std::execution::par, data.begin(), data.end(), 0.0);
// 并行排序
std::sort(std::execution::par, data.begin(), data.end());
11.8 性能优化技巧
11.8.1 算法选择指南
选择算法就像选择工具:
- 迭代器类型:能用随机访问就别用线性访问
- 数据规模:大数据用高效算法,小数据简单算法可能更快
- 稳定性需求:需要保持顺序就用稳定算法
- 内存限制:内存紧张就用原地算法
11.8.2 实际优化案例
案例1:避免不必要的复制
// 差的写法:会复制数据
std::vector<int> get_large_data() {
std::vector<int> data(1000000);
// ... 填充数据
return data; // C++11后会优化,但老代码可能有问题
}
// 好的写法:使用引用
void process_large_data(std::vector<int>& data) {
// 直接处理原数据
}
案例2:选择合适的算法
std::vector<int> data = {3, 1, 4, 1, 5, 9, 2, 6};
// 只要最大值,不需要完整排序
auto max_it = std::max_element(data.begin(), data.end());
// 只要前3个最大的
std::partial_sort(data.begin(), data.begin() + 3, data.end(), std::greater<int>());
// 需要完整排序才用sort
std::sort(data.begin(), data.end());
11.9 综合实战项目
11.9.1 学生成绩管理系统
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <iomanip>
struct Student {
std::string name;
std::vector<int> scores; // 各科成绩
double average() const {
if (scores.empty()) return 0.0;
return std::accumulate(scores.begin(), scores.end(), 0.0) / scores.size();
}
int total() const {
return std::accumulate(scores.begin(), scores.end(), 0);
}
};
class StudentManager {
std::vector<Student> students;
public:
void addStudent(const std::string& name, const std::vector<int>& scores) {
students.push_back({name, scores});
}
void printTopStudents(int count) {
// 按总分排序
std::partial_sort(students.begin(),
students.begin() + std::min(count, (int)students.size()),
students.end(),
[](const Student& a, const Student& b) {
return a.total() > b.total();
});
std::cout << "前" << count << "名学生:\n";
for (int i = 0; i < std::min(count, (int)students.size()); ++i) {
std::cout << std::setw(10) << students[i].name
<< " - 总分: " << students[i].total()
<< " - 平均分: " << std::fixed << std::setprecision(1)
<< students[i].average() << std::endl;
}
}
void printStudentsAboveAverage(double threshold) {
auto count = std::count_if(students.begin(), students.end(),
[threshold](const Student& s) { return s.average() >= threshold; });
std::cout << "平均分在" << threshold << "分以上的学生数量:" << count << std::endl;
}
double classAverage() const {
if (students.empty()) return 0.0;
double total_sum = std::accumulate(students.begin(), students.end(), 0.0,
[](double sum, const Student& s) { return sum + s.total(); });
return total_sum / students.size();
}
};
int main() {
StudentManager manager;
// 添加学生数据
manager.addStudent("Alice", {85, 92, 78, 88});
manager.addStudent("Bob", {92, 88, 95, 91});
manager.addStudent("Charlie", {78, 85, 82, 79});
manager.addStudent("Diana", {95, 91, 89, 93});
manager.addStudent("Eve", {88, 79, 85, 87});
// 显示前3名学生
manager.printTopStudents(3);
std::cout << std::endl;
// 显示平均分85分以上的学生数量
manager.printStudentsAboveAverage(85);
std::cout << std::endl;
// 显示班级平均分
std::cout << "班级平均分:" << manager.classAverage() << std::endl;
return 0;
}
11.9.2 文本分析工具
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <algorithm>
#include <unordered_map>
#include <cctype>
class TextAnalyzer {
std::vector<std::string> words;
// 清理单词:移除标点,转为小写
std::string cleanWord(const std::string& word) {
std::string cleaned;
for (char c : word) {
if (std::isalnum(c)) {
cleaned += std::tolower(c);
}
}
return cleaned;
}
public:
void loadText(const std::string& text) {
std::istringstream stream(text);
std::string word;
while (stream >> word) {
std::string cleaned = cleanWord(word);
if (!cleaned.empty()) {
words.push_back(cleaned);
}
}
}
void loadFile(const std::string& filename) {
std::ifstream file(filename);
if (!file) {
std::cerr << "无法打开文件:" << filename << std::endl;
return;
}
std::string line;
std::string content;
while (std::getline(file, line)) {
content += line + " ";
}
loadText(content);
}
size_t wordCount() const {
return words.size();
}
size_t uniqueWordCount() const {
std::vector<std::string> unique_words = words;
std::sort(unique_words.begin(), unique_words.end());
auto last = std::unique(unique_words.begin(), unique_words.end());
return std::distance(unique_words.begin(), last);
}
std::vector<std::string> mostFrequentWords(size_t count) {
std::unordered_map<std::string, size_t> frequency;
// 统计词频
for (const auto& word : words) {
frequency[word]++;
}
// 转换为vector以便排序
std::vector<std::pair<std::string, size_t>> freq_vec(frequency.begin(), frequency.end());
// 按频率排序(降序)
std::partial_sort(freq_vec.begin(),
freq_vec.begin() + std::min(count, freq_vec.size()),
freq_vec.end(),
[](const auto& a, const auto& b) { return a.second > b.second; });
std::vector<std::string> result;
for (size_t i = 0; i < std::min(count, freq_vec.size()); ++i) {
result.push_back(freq_vec[i].first);
}
return result;
}
bool containsWord(const std::string& word) const {
return std::binary_search(words.begin(), words.end(), cleanWord(word));
}
void sortWords() {
std::sort(words.begin(), words.end());
}
void printWords() const {
std::cout << "所有单词:";
for (const auto& word : words) {
std::cout << word << " ";
}
std::cout << std::endl;
}
};
int main() {
TextAnalyzer analyzer;
// 示例文本
std::string text = R"(
C++ is a powerful programming language.
C++ supports object-oriented programming,
generic programming, and functional programming.
Many modern applications are built with C++.
)";
analyzer.loadText(text);
analyzer.sortWords(); // 排序以便二分查找
std::cout << "文本分析结果:\n";
std::cout << "总词数:" << analyzer.wordCount() << std::endl;
std::cout << "不同单词数:" << analyzer.uniqueWordCount() << std::endl;
std::cout << "\n最频繁的单词:";
auto frequent = analyzer.mostFrequentWords(5);
for (const auto& word : frequent) {
std::cout << word << " ";
}
std::cout << std::endl;
std::cout << "\n是否包含'programming':"
<< (analyzer.containsWord("programming") ? "是" : "否") << std::endl;
std::cout << "是否包含'java':"
<< (analyzer.containsWord("java") ? "是" : "否") << std::endl;
return 0;
}
11.10 学习要点总结
11.10.1 核心概念回顾
- 算法分类:了解不同算法的用途和特点
- 迭代器层次:理解不同迭代器的能力和限制
- 性能考虑:根据数据规模和需求选择合适的算法
- 函数对象:学会自定义算法行为
- 并行算法:利用多核处理器提升性能
11.10.2 最佳实践
- 优先使用STL算法:不要自己实现已有的功能
- 理解复杂度:知道算法的性能特点
- 注意迭代器失效:避免在修改容器时使用失效的迭代器
- 合理使用lambda:简单逻辑用lambda,复杂逻辑用函数对象
- 考虑并行化:大数据处理时考虑并行算法
11.10.3 常见陷阱
- 算法与迭代器不匹配:比如对list使用sort(需要随机访问)
- 忽略前置条件:比如对未排序的数据使用binary_search
- 不必要的复制:尽量使用引用和移动语义
- 过度优化:小数据简单算法可能更高效
通过掌握STL算法和迭代器,你可以写出更简洁、高效、可维护的C++代码。记住:不要重复造轮子,学会用好STL这个强大的工具箱!
浙公网安备 33010602011771号