CC++ & Algorithm

STL的三大组件:容器、迭代器、算法

较难9
语言版本:通用
概述:把STL比作厨房系统:容器是锅碗瓢盆,迭代器是长筷子,算法是烹饪手法,层层递进讲解三者的关系与用法。

从一顿饭到代码世界:STL三大组件是什么?

想象一下你要请朋友们来家里吃火锅。你需要准备三样东西:

  • 容器:锅、碗、盘子——用来装食材和蘸料。在程序里,容器就是存放数据的地方,比如数组、链表、集合。
  • 工具:长筷子或漏勺——用来从锅里夹出肉片,又不直接伸手进去烫着。在程序里,迭代器就是这样的“智能筷子”,能安全地访问容器里的每个元素,不管容器底层是连续内存还是链表。
  • 方法:煮、涮、蘸——不同食材用不同烹饪手法。在程序里,算法就是对容器里的数据做操作,比如排序、查找、复制。

这三个东西配合起来就是STL(标准模板库)。STL的设计哲学是:容器只管存储数据,迭代器只管访问数据,算法只管处理数据,三者通过迭代器这个“万能转接头”连接。这样写出来的代码不仅灵活,还能重复利用——比如同一个排序算法,既能排数组,也能排链表,只要它们有对应的迭代器就行。


三兄弟登场:容器、迭代器、算法

1. 容器(Container)——你的数据仓库

容器就是用来装数据的东西。STL提供了十几种容器,按内部组织方式分成两大类:

序列式容器 —— 数据按你放进去的顺序排排站,像排队买奶茶。

  • vector(动态数组):在队伍末尾加人很快,中间加人需要让后面的人挪位置,慢。
  • deque(双端队列):在队伍前面或后面加人都快,就像奶茶店可以两头同时服务。
  • list(双向链表):每个节点手拉手,可以在任意位置插入删除,但想找第5个人必须从第1个开始数,不支持“跳着找”。
  • array(固定数组):大小固定,像教室里固定的座位数量。
  • forward_list(单向链表):比list省内存,但只能从前往后走,不能回头。

关联式容器 —— 数据按关键字排序,像字典按拼音排列,方便快速查找。

  • set / multiset:只存值,自动排好序(默认从小到大),set不允许重复,multiset允许重复。
  • map / multimap:存键值对,比如“学号→姓名”,按键排序。
  • unordered_set / unordered_map:用哈希表,查找速度飞快(平均O(1)),但元素不排序,像乱序放东西但给每个东西贴了标签。

生活例子

  • 你的零花钱记录本用vector最方便,每天往后记一笔。
  • 班级同学按学号查人名用map,学号就是键,姓名就是值。
  • 游戏中的敌人生成列表用forward_list,因为你只需要依次处理,不需要往回看。

所有容器都有一些共同的成员函数:size()(容器里有多少元素)、empty()(空吗?)、begin()(返回指向第一个元素的迭代器)、end()(返回指向最后一个元素后面的迭代器)。


2. 迭代器(Iterator)——数据世界的“智能筷子”

迭代器就像一个指针,但比指针更聪明:它知道怎么在容器里安全移动,而且不管容器是什么结构,用起来都一样。每种容器都提供自己的迭代器类型,但接口统一。

迭代器按能力分为五档:

迭代器类型能做什么像什么适用于哪些容器
输入迭代器只能读,只能单向走用眼睛扫读名单istream_iterator
输出迭代器只能写,只能单向走往黑板上写名字ostream_iterator
前向迭代器可读可写,单向走用铅笔在名单上做标记forward_list
双向迭代器可读可写,可以向前向后用遥控器上下翻页listsetmap
随机访问迭代器支持跳跃,能直接跳到第n个翻书页,直接翻到第100页vectordequearray

常用操作:

  • *it 获取迭代器当前指向的元素(像用筷子夹起肉片)。
  • ++it 移动到下一个元素(筷子往前伸)。
  • it != container.end() 判断是否走到了容器末尾(注意:end()指向的是最后一个元素的后一个位置,那个位置是空的,不能解引用)。
  • 随机访问迭代器还能做 it + 5(跳5步)、it - 3(往回3步)、it < it2(比较位置先后)。

常见错误:新手常犯的两个错误——

  1. end()迭代器解引用:*v.end()会崩溃,因为那里没有元素。
  2. 在遍历中修改容器导致迭代器失效:比如用vectorerase删掉一个元素后,原来指向那个位置及后面的迭代器都作废了,不能再使用。

3. 算法(Algorithm)——数据加工流水线

STL提供了超过100个算法,主要藏在<algorithm>头文件里,少数数值算法在<numeric>中。它们不直接操作容器,而是通过迭代器这个中介,所以同一个算法可以用于不同容器。

算法分类:

  • 非修改式算法:只看不碰。比如find(找某个值)、count(数个数)、equal(比较两个范围是否相等)。
  • 修改式算法:会改变容器里的值,但通常不改变容器大小(删除元素需要配合容器自己的成员函数)。比如copy(复制)、replace(替换)、remove(移动元素到末尾,不真正删除)。
  • 排序相关sort(快速排序)、stable_sort(稳定排序)、partial_sort(只排前k个)、binary_search(二分查找,要求已排序)。
  • 集合算法set_union(并集)、set_intersection(交集)等,注意需要容器已排序。

生活例子

  • 期末考试后老师用sort按分数排序全班成绩。
  • 找你的零花钱记录里有没有200块钱的“生日红包”,用find
  • 把所有不及格的分数用replace改成60分。

注意:算法通过一对迭代器(开始和结束)指定操作范围,有时还需要一个输出位置的迭代器。例如std::copy(vec.begin(), vec.end(), dest.begin())会把vec里的所有元素复制到dest里,但dest必须事先有足够空间,否则会越界。安全的做法是用std::back_inserter(后面示例会讲)。


完整示例:C++版

下面我们用三个容器(vector、set、list)演示容器+迭代器+算法的协同工作。每行变量定义都写了中文注释,方便你理解。

#include <iostream>
#include <vector>      // vector容器
#include <set>         // set容器
#include <list>        // list容器
#include <algorithm>   // sort, find, copy等算法
#include <iterator>    // std::back_inserter

int main() {
    // ---- 1. vector + 迭代器 + sort ----
    std::vector<int> vec = {5, 2, 8, 1, 9};  // 创建存放int的vector
    std::cout << "原始vector: ";
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        std::cout << *it << " ";  // 解引用迭代器,获取元素值
    }
    std::cout << std::endl;

    // 使用算法sort排序(需要随机访问迭代器,vector满足)
    std::sort(vec.begin(), vec.end());  // 传入一对迭代器,默认升序
    std::cout << "排序后vector: ";
    for (int x : vec) std::cout << x << " ";  // 范围for底层也是迭代器
    std::cout << std::endl;

    // ---- 2. set + 迭代器 + find ----
    std::set<int> s = {3, 7, 1, 7, 2};  // set自动去重并排序(1,2,3,7)
    std::cout << "set中的元素: ";
    for (auto it = s.begin(); it != s.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    // 用通用算法find在set中找7(set自己有更快的find成员,这里演示通用)
    auto it2 = std::find(s.begin(), s.end(), 7);
    if (it2 != s.end()) {
        std::cout << "找到了7!" << std::endl;
    }

    // ---- 3. list + 迭代器 + remove_if ----
    std::list<int> lst = {10, 20, 31, 40, 51, 60};  // 双向链表
    // 用list自己的成员函数remove_if移除所有奇数
    lst.remove_if([](int n) { return n % 2 != 0; });  // lambda判断奇数
    std::cout << "移除奇数后的list: ";
    for (auto it = lst.begin(); it != lst.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    // ---- 4. 使用back_inserter安全复制到空vector ----
    std::vector<int> dest;  // 空vector
    // back_inserter创建一个插入迭代器,每复制一个元素就调用dest.push_back
    std::copy(vec.begin(), vec.end(), std::back_inserter(dest));
    std::cout << "复制到dest: ";
    for (int x : dest) std::cout << x << " ";
    std::cout << std::endl;

    return 0;
}

输出结果(注意元素顺序):

原始vector: 5 2 8 1 9 
排序后vector: 1 2 5 8 9 
set中的元素: 1 2 3 7 
找到了7!
移除奇数后的list: 10 20 40 60 
复制到dest: 1 2 5 8 9 

代码要点讲解

  • vec.begin()返回指向第一个元素(5)的迭代器,vec.end()返回尾后迭代器(指向9后面的位置)。std::sort(vec.begin(), vec.end())对整个范围排序。
  • std::find(s.begin(), s.end(), 7):输入迭代器即可,任何容器都能用。但set自己有find成员函数,速度更快(O(logN))。这里教学演示通用算法。
  • lst.remove_if(...):list的成员函数直接操作链表节点,比通用算法std::remove_if更高效(通用算法需要配合erase)。
  • std::copy(vec.begin(), vec.end(), std::back_inserter(dest)):因为dest是空vector,直接用dest.begin()会越界。std::back_inserter会每次调用dest.push_back,自动扩容,安全又方便。

Python版的“三兄弟”

Python没有显式的迭代器概念,但for x in container背后就是迭代器机制。Python的内置容器和函数提供了类似功能,代码更简洁。

# ---- 1. list (类似vector) + 排序 ----
lst = [5, 2, 8, 1, 9]  # Python的list是动态数组
print("原始list:", lst)
lst.sort()  # 原地排序,相当于C++的sort(vec.begin(), vec.end())
print("排序后list:", lst)

# ---- 2. set (类似C++的set) ----
s = {3, 7, 1, 7, 2}  # 自动去重,元素不保证顺序(实际是哈希表)
print("set中的元素:", sorted(s))  # 输出有序版本
# 查找
if 7 in s:  # 底层哈希查找,O(1)
    print("找到了7!")

# ---- 3. list + 列表推导式(类似 remove_if) ----
lst2 = [10, 20, 31, 40, 51, 60]
# 列表推导式:保留偶数
lst2 = [x for x in lst2 if x % 2 == 0]
print("移除奇数后的list:", lst2)

# ---- 4. 复制 ----
dest = lst.copy()  # 或者 list(lst)
print("复制到dest:", dest)

说明

  • Python的list.sort()排序,相当于C++的std::sort
  • in运算符对set是O(1)哈希查找,对list是O(n)线性查找。
  • 列表推导式[x for x in lst2 if x % 2 == 0]等价于C++的lst.remove_if加重新赋值,但更简洁。
  • Python没有显式的迭代器对象,但理解for循环背后的迭代器机制有助于迁移概念。

新手最容易踩的坑

1. 迭代器失效

vectordeque中插入或删除元素后,之前获得的迭代器可能全部失效(因为内存重新分配)。例如:

std::vector<int> v = {1,2,3,4,5};
auto it = v.begin() + 2;  // 指向3
v.erase(v.begin());        // 删掉1,现在v为{2,3,4,5},it失效了!
// *it  // 未定义行为,可能崩溃

解决办法:修改容器后重新获取迭代器,或者使用不会失效的容器(如list插入删除不会影响其他迭代器)。

2. 对end()解引用

std::vector<int> v = {1,2,3};
auto it = v.end();  // 指向3后面的位置
// *it   // 危险!那里没有元素

3. 对不支持的容器使用特定算法

std::list<int> lst = {5,2,8};
std::sort(lst.begin(), lst.end());  // 编译错误!list的迭代器不是随机访问迭代器

正确做法list有自己的sort成员函数:lst.sort();

4. 算法不修改容器大小

std::remove只是把要删除的元素移动到末尾,并不真正删除,需要配合erase

std::vector<int> v = {1,2,3,2,4};
auto new_end = std::remove(v.begin(), v.end(), 2);  // 把2移到末尾,返回新的逻辑结尾
v.erase(new_end, v.end());  // 真正删除尾部多余元素(erase-remove惯用法)

接下来学什么?

掌握了三大组件,你已经站在了STL的门口。继续深入,可以学习:

  • 如何选择合适的容器:根据插入删除频繁程度、是否需要随机访问、是否排序等。
  • 迭代器类型详解:为什么有的算法要求随机访问迭代器?std::advancestd::next等工具函数。
  • 算法复杂度sort是O(n log n),find是O(n),binary_search要求有序且O(log n)。
  • C++17/20新特性:并行算法、std::string_viewstd::span等。

下一站试试用map做一个简单的“学生成绩查询系统”,或者用vectoralgorithm实现一个“单词计数器”。动手写一写,STL就不神秘了!

例题精讲

1单选题

以下哪个STL容器支持随机访问迭代器?

Astd::list
Bstd::vector
Cstd::forward_list
Dstd::set
2判断题

STL算法是独立于具体容器实现的,它们通过迭代器来操作容器中的元素。

3填空题
使用std::find算法在vector中查找元素value,需要传入容器的起始和结束迭代器,代码为:auto it = find(vec.___(), vec.end(), value);
4单选题

关于STL容器、迭代器和算法的关系,下列说法正确的是?

A容器负责存储数据,迭代器负责遍历数据,算法负责处理数据
B迭代器是容器的一部分,不能单独使用
C算法必须直接操作容器对象,不能通过迭代器
D每种容器只能使用固定的算法,不能复用
5判断题

所有STL容器都支持双向迭代器(bidirectional iterator)。