🔷 第十三章 STL
📄 一句话概括

第十三章讲的是"不要再自己造轮子"。 一句话串起来:容器(13.1 / 13.2)是"装数据的盒子",算法(13.3)是"往盒子上干活的手法",迭代器(13.4)是"两者的通用插头"。三者拼起来的结果是:你写业务代码时可以只关心"我要干什么",而不用关心"数组多大、内存怎么管、排序怎么写"。 整章最重要的一条因果链:内存怎么摆 → 迭代器是哪一级 → 能用哪些算法。 所以 "选容器"这个决定,会一路决定到"你能不能 sort"。

📝 本节和前面几节的关系

收束整章。它是全课程里"最实用"的一章 ——前面十二章你学的所有东西(类、模板、运算符重载、指针),在这一章变成了"用别人造好的巨型工具箱"。 往回想:第十二章的模板是这一章的地基(vector<int> 是类模板、sort 是函数模板);第十章的类是"vector 里装的对象";第十一章的运算符重载是"让自定义类能被 sort 排";第七章的指针是"迭代器的原型"。 往前看:13.4.5 那张表里"vector 中间插删要搬家"这一点,其实是 vector 唯一的痛点;而标准库里还有一个能和 vector 平起平坐的主力容器,这一章只提了名字没展开(string 之外的"能自动扩容的字符串 / 更现代的容器") ——这一笔账也算在了后面。

一、本章知识地图

块 小节 一句话要点
序列容器 13.1 按"位置"存(有 []、按下标取)。三兄弟的性格:vector(连续内存、尾快)、deque(分段连续、两头快)、list(链表、中间快但不能 [])
vector 13.1.1(含实例01) 默认就该用它。size / capacity / reserve 的区别;[] 不检查、at() 会抛 out_of_range;扩容成倍(O(n) 摊还);erase 传迭代器不传下标;循环里删元素要处理好 i
deque 13.1.2 两头 push/pop 都是 O(1)(vector 头插是 O(n),差三个数量级);但没有 capacity / reserve;数据不是一整块,不能对接 C 接口;stack / queue 的底层默认就是它
list 13.1.3 任意已知位置插删 O(1),但没有 []、不能 +n、不能用 std::sort;独有技能 splice(摘节点不复制)、merge、remove(真删)、unique(只合并相邻);优势的前提是"你已经拿着那个位置的迭代器"
关联式容器 13.2 按"键"存(元素自动排序、查找 O(log n))。表格:set / multiset(只存键)、map / multimap(键+值);带 multi- 的允许重复
set / multiset 13.2.1 / 13.2.2(含实例02) set:自动去重 + 自动升序;元素不能改(要 erase + insert);insert 返回 pair<iterator,bool>;erase(键) 在 multiset 里会删一片(只删一个要 erase(find(x)));equal_range 一次拿到"一段相同的键"
map / multimap 13.2.3 / 13.2.4 m[k] 在键不存在时会"偷偷插入一个默认值"(查询必须用 find);元素是 pair<const K, V>(键不能改);map 只能按键排(按值排要搬到 vector);multimap 没有 operator[](语义有歧义);"一对多"常常 map<K, vector<V>> 更好用
算法总览 13.3 统一姿势:算法名(first, last [, 额外参数]);区间左闭右开;四类:非修正 / 修正 / 排序 / 数值;带 _if 的配条件(普通函数 / 函数对象 / lambda)
非修正序列算法 13.3.1(含实例03、04) 只看不改:find / find_if / count / count_if / adjacent_find(只找"相邻"的重复) / for_each / all_of / equal / max_element
修正序列算法 13.3.2(含实例05、06) 会改数据:copy / fill / replace / remove(不删!只把要留的挪前面) / reverse / random_shuffle(C++14 弃用、C++17 移除 → 用 shuffle) / partition(不保序)/ stable_partition(保序) / unique(只合并相邻)
排序算法 13.3.3(含实例07) sort(不稳定)/ stable_sort(稳定);partial_sort(前 N 名)/ nth_element(中位数,O(n));binary_search / lower_bound / upper_bound(必须已排序);比较器必须严格弱序(写 < 不写 <=);sort 要随机访问迭代器
数值算法 13.3.4(含实例08) 在 <numeric>(不在 <algorithm>!):accumulate / inner_product / partial_sum / adjacent_difference;初值的类型 = 结果的类型(初值给 0 会让 double 数据被截断)
迭代器 13.4 五级阶梯:输出(只写)/ 输入(只读)/ 前向(只 ++)/ 双向(++ --)/ 随机访问(+n [] <);等级决定能用的算法
输出 / 输入迭代器 13.4.1 / 13.4.2(含实例09) ostream_iterator(往屏幕写)、back_inserter(自动 push_back —— 解决 copy 到空容器崩溃);istream_iterator(vector<int> v(in, eof); 一句读完);两者都是单遍
前向 / 双向 / 随机访问 13.4.3 / 13.4.4 / 13.4.5 前向:forward_list、unordered_*;双向:list / set / map(能用 reverse,不能 sort);随机访问:vector / deque / string / 数组(能用 sort / binary_search);next / prev / advance / distance 是"走"的通用替代

二、必须记住的十个结论

  1. 默认用 vector。90% 的场合它都是最好的选择(连续内存、缓存友好、能随机访问)。只有明确知道"瓶颈在中间插删"或"两头进出"时才换。
  2. 选容器问一句话:"我的数据主要在哪一端动?" —— 尾 → vector;两头 → deque;中间 → list;按值找 → set / map。
  3. erase-remove 惯用法必须背下来:v.erase(remove(v.begin(), v.end(), x), v.end()); —— 因为 remove 不删元素,只把要留的挪到前面,返回新的逻辑末尾。 unique 也是同一个脾气(还要先 sort)。
  4. map 的 m[k] 会"偷偷插入"。只想查询必须用 find(或 at,它会抛异常)。写 if (m[k] != 0) 是错的——每次判断都会往容器里塞一个。
  5. set / map 的元素"键"不能改(是排序依据,const)。要改就 erase + insert。
  6. 算法的调用姿势是"一对迭代器 + 额外参数",区间左闭右开 [first, last)。算法不认识容器 —— 所以 sort(arr, arr+n) 对裸数组也能用。代价:算法改不了容器大小(要扩容就用插入迭代器)。
  7. "迭代器等级"决定"能用的算法":sort / binary_search / random_shuffle 要随机访问(list / set / map 用不了);reverse 要双向;find / count 只要输入迭代器(哪都能用)。
  8. accumulate 的初值类型就是结果类型(在 <numeric>)。要浮点结果,初值写 0.0(初值 0 会让每一步都截断)。
  9. sort 的比较规则必须"严格弱序":写 < 或 >,绝不能写 <= / >=(会造成越界、崩溃或结果错乱)。同样适用于 set 的比较器和所有 _if 条件。
  10. "会用"占 90%,"会写"占 10%。现实里写代码:vector + sort + find + map 就能解决绝大多数问题。 自己写容器/算法是"造工具箱"的活(第十二章说的"学习顺序:会用 → 会定制 → 会写")。

三、动手 Demo:两关自测

第一关(写代码):一个"班级成绩管理"

要求:只用本章的东西,做出一个能跑的小系统。

功能 该用什么 对应小节
存一组学生成绩 vector<Student> 13.1.1
按分数排名(同分按名字) sort + lambda 13.3.3
算总分 / 平均 accumulate(注意初值给 0.0) 13.3.4
找出最高最低 max_element / min_element 13.3.1
统计各分数段人数 map<string,int> 或 count_if 13.2.3 / 13.3.1
按名字查某人的成绩 map<string,int>(O(log n)) 13.2.3
去掉重复的分数(看有哪些档次) set<int> 13.2.1
删掉所有不及格的 erase-remove 惯用法 13.3.2

参考实现(自己先写,写不出来再看):

#include <iostream>
#include <vector>
#include <map>
#include <set>
#include <string>
#include <algorithm>
#include <numeric>
using namespace std;

struct Student {
    string name;
    int    score;
    Student(const string &n, int s) : name(n), score(s) {}
};

void line(const char *t) { cout << "── " << t << " ──" << endl; }

int main() {
    vector<Student> cls = {
        Student("小明", 88), Student("小红", 95), Student("小刚", 71),
        Student("小丽", 88), Student("小强", 100), Student("小敏", 55)
    };

    // ① 原始名单
    line("原始名单");
    for (const Student &s : cls) cout << s.name << "(" << s.score << ") ";
    cout << endl;

    // ② 排名(分数降序,同分按名字)
    vector<Student> rank = cls;
    sort(rank.begin(), rank.end(), [](const Student &a, const Student &b) {
        if (a.score != b.score) return a.score > b.score;
        return a.name < b.name;
    });
    line("排行榜");
    int no = 1;
    for (const Student &s : rank) cout << no++ << ". " << s.name << "(" << s.score << ") ";
    cout << endl;

    // ③ 总分 / 平均(★ 初值 0.0)
    double total = accumulate(cls.begin(), cls.end(), 0.0,
                              [](double acc, const Student &s) { return acc + s.score; });
    cout << "总分 " << total << ",平均 " << total / cls.size() << endl;

    // ④ 最高最低
    auto hi = max_element(cls.begin(), cls.end(),
                          [](const Student &a, const Student &b) { return a.score < b.score; });
    auto lo = min_element(cls.begin(), cls.end(),
                          [](const Student &a, const Student &b) { return a.score < b.score; });
    cout << "最高 " << hi->name << "(" << hi->score << "),"
         << "最低 " << lo->name << "(" << lo->score << ")" << endl;

    // ⑤ 分数档次(set 自动去重排序)
    set<int> levels;
    for (const Student &s : cls) levels.insert(s.score);
    cout << "出现过的分数档次(去重+排序):";
    for (int x : levels) cout << x << " ";
    cout << endl;

    // ⑥ 分数段人数(count_if)
    line("分数段");
    int seg[5] = {0};
    const char *names[] = {"90+", "80-89", "70-79", "60-69", "<60"};
    for (const Student &s : cls) {
        if (s.score >= 90) seg[0]++;
        else if (s.score >= 80) seg[1]++;
        else if (s.score >= 70) seg[2]++;
        else if (s.score >= 60) seg[3]++;
        else seg[4]++;
    }
    for (int i = 0; i < 5; i++) cout << names[i] << ": " << seg[i] << " 人  ";
    cout << endl;

    // ⑦ 按名字查成绩(map:O(log n))
    map<string, int> lookup;
    for (const Student &s : cls) lookup[s.name] = s.score;
    line("按名字查");
    for (const string &who : {"小红", "小强", "不存在"}) {
        auto it = lookup.find(who);                    // ★ 必须用 find(不能用 [])
        if (it != lookup.end()) cout << who << " 的成绩是 " << it->second << endl;
        else                    cout << who << ":查无此人" << endl;
    }

    // ⑧ 删掉不及格的(erase-remove 惯用法)
    cls.erase(remove_if(cls.begin(), cls.end(),
                        [](const Student &s) { return s.score < 60; }), cls.end());
    line("删掉不及格后");
    for (const Student &s : cls) cout << s.name << "(" << s.score << ") ";
    cout << "  共 " << cls.size() << " 人" << endl;
    return 0;
}

看到什么算成功:

── 原始名单 ──
小明(88) 小红(95) 小刚(71) 小丽(88) 小强(100) 小敏(55) 
── 排行榜 ──
1. 小强(100) 2. 小红(95) 3. 小明(88) 4. 小丽(88) 5. 小刚(71) 6. 小敏(55) 
总分 497,平均 82.8333
最高 小强(100),最低 小敏(55)
出现过的分数档次(去重+排序):55 71 88 95 100 
── 分数段 ──
90+: 2 人  80-89: 2 人  70-79: 1 人  60-69: 0 人  <60: 1 人  
── 按名字查 ──
小红 的成绩是 95
小强 的成绩是 100
不存在:查无此人
── 删掉不及格后 ──
小明(88) 小红(95) 小刚(71) 小丽(88) 小强(100)   共 5 人

这一关用到了整章的每一条主线。如果 ⑧ 那句 erase(remove_if(...), end()) 你能默写出来,说明这一章最"要命"的那个惯用法已经进肌肉了。

加分实验:

  1. 把 ③ 的初值从 0.0 改成 0 → 平均变成 82(整数除法)。亲眼确认一次"初值类型"这件事。
  2. 把 ⑦ 的 find 改成 lookup[who] → "不存在"那个会输出 0,而且 lookup.size() 多了一个。map::operator[] 的坑。
  3. 把 ⑤ 的 set 改成 vector + sort + unique → 结果一样,体会"两种去重手段"(set 保持有序;sort+unique 一次性开销)。
  4. 把 vector<Student> 换成 list<Student> → sort 那句编译不过,得改成 cls.sort(比较器),而且 cls.erase(remove_if(...)) 那套也要换成 cls.remove_if(...)。这就是 13.4.5 讲的"等级"的真实代价。

第二关(找错):十个判断,逐个说明对错与原因

# 代码 / 说法 对/错 为什么
① vector<int> v(5); cout << v[10]; 错 越界(未定义行为)。v[10] 不检查;要检查写 v.at(10)(抛 out_of_range)
② vector<int> v; copy(a.begin(), a.end(), v.begin()); 错 v.begin() 在空容器里就是 end(),没位置可写 → 崩。要用 back_inserter(v)
③ for (int i = 0; i < v.size(); i++) ⚠️ 不推荐 size() 是 size_t(无符号),会有比较警告;写成 size_t i。若写成 i <= v.size() - 1,空容器时会下溢成死循环
④ map<string,int> m; if (m["x"] > 0) ... 错 m["x"] 会插入一个 x -> 0,容器被改了。要用 find
⑤ map<string,int> m; for (auto &p : m) p.first = "y"; 错 键是 const(pair<const string,int>),报 assignment of read-only。要改键就 erase + insert
⑥ vector<int> v; v.erase(remove(v.begin(), v.end(), 5), v.end()); 对 这就是 erase-remove 惯用法。如果只写 remove(...),size() 不会变
⑦ list<int> l; sort(l.begin(), l.end()); 错 list 的迭代器是双向,而 sort 要随机访问 → no match for 'operator-'。要用 l.sort()
⑧ accumulate(v.begin(), v.end(), 0)(v 是 vector<double>) 错(且不报错) 初值 0 是 int → 每步都截断。要写成 0.0。这是本章最"安静"的一个错
⑨ set<int> s; *s.begin() = 5; 错 set 的元素是 const(它是排序依据)。要改就 erase + insert
⑩ unordered_map<string,int> m; sort(m.begin(), m.end()); 错 哈希表的迭代器是前向的(而且元素只读)。它根本不有序,也不能排序

30 秒实验:把 ① ② ④ ⑤ ⑦ ⑨ ⑩ 逐个在编译器里敲一遍,认一下每种报错(或崩溃)的样子。

四、常见误区(本章汇总)

  1. "我自己写循环也行,何必用算法"——行,但更容易错。remove / unique / accumulate 这些"看起来简单"的循环里藏着不少坑(初值类型、越界、单遍)。算法是"被无数人检视过的循环"。
  2. "STL 慢"——恰恰相反:sort 是精心调优的混合排序、vector 的扩容是摊还 O(1)、二分查找是 O(log n)。 慢通常是因为"选错了容器/算法",而不是"用了 STL"。
  3. "vector 一定要用,list 是过时的东西"——不是过时,是专用。"已知位置频繁插删"的场景 list 是不可替代的(还有 splice 这种独门技能)。只是这类场景比想象中少。
  4. "关联式容器比序列容器高级"——只是用法不同:按位置操作 → 序列;按值查找 → 关联。 vector 按下标遍历比 map 快得多(缓存友好)。
  5. "算法和成员函数哪个好,随便"——要看名字后面的东西:list::remove 真删、std::remove 不删;list::sort 能用、std::sort 不能用;set::find 是 O(log n)、std::find 是 O(n)。"同名不同行为"是 STL 里最需要小心的一类陷阱。
  6. "迭代器就是指针,一样用"——接口像,能力差很远。能不能 --、能不能 +n、能不能比大小,决定了它能用哪些算法(这是本章最主要的一句话)。
  7. "模板报错太难懂,STL 没法用"——报错长是因为模板层层展开。技巧:先看"你写的哪一行",再看"报错里有没有 no match for / is not a member of",最后看"最后一条 note"(12.4 小结说的)。本章那些"❌"的例子,报错脸都认一认,以后看到就知道是哪一类问题。
  8. "学了 STL 就不用懂数据结构了"——反过来:不知道"链表不能随机访问",就不明白 list 为什么没有 [];不知道"哈希 vs 树",就选不出 map 还是 unordered_map。 STL 是"数据结构的成品",但你得知道每个成品适合什么场合。

五、写完这一章,你会什么了

这一章之后,你的代码"上一档次"了 ——因为你不再写那些"和业务无关、却必须写对"的样板代码:

同时拿到三张能一直用的"判断表":

判断 出处 一句话
"这个容器能切片吗?" 13.4.5 看内存怎么摆 —— 连续/分段连续 → 能随机访问 → 能用 sort;链表/树/哈希 → 不能
"名字一样,行为一样吗?" 13.1.3 / 13.3.2 list::remove 真删、std::remove 不删 —— 成员版和算法版要分开记
"这个错是"编译错"还是"安静错"?" 13.3.4 / 13.2.3 accumulate 初值、map[] 偷偷插入 —— 这类错必须靠"跑一遍看结果"来抓
自测十问(点开对答案) 1. STL 的三个零件是什么?一句话说清它们的关系。 —— **容器(装数据的盒子)+ 算法(干活的手法)+ 迭代器(通用插头)**。**算法只认迭代器、不认识容器**,所以一份 `sort` 能服务所有"迭代器等级够"的容器(以及裸数组)。 2. 三个序列容器各快在哪?怎么选? —— **`vector`:连续内存,按下标取 / 尾部加 / 遍历最快**;**`deque`:两头插删 O(1),也有 `[]`**;**`list`:已知位置插删 O(1),但没有 `[]`、不能 `sort`**。**判断标准:"数据主要在哪一端动?" 无特殊情况默认 `vector`。** 3. `remove` 为什么"不删元素"?正确的删除写法是什么? —— 因为**算法只拿到两个迭代器,改不了容器大小**。它**把要留的挪到前面,返回新的逻辑末尾**。**正确写法(erase-remove 惯用法):`v.erase(remove(v.begin(), v.end(), x), v.end());`** 条件版用 `remove_if`。 4. `map` 的 `m[k]` 有什么坑?查询该用什么? —— **键不存在时会插入一个默认值再返回引用**(容器被改了)。**查询必须用 `find`**(或 `at`,越界抛异常)。**`if (m[k] != 0)` 是错的。** 5. `set` / `map` 的元素为什么不能改? —— **键是排序依据**(`set` 的元素、`map` 的 `first` 都是 `const`)。**改它会破坏有序性**。**要改:`erase` + `insert`。** 6. 迭代器五个等级是什么?等级由什么决定? —— **输出(只写)/ 输入(只读)/ 前向(只 `++`)/ 双向(`++ --`)/ 随机访问(`+n [] <`)**。**由内存布局决定**:连续/分段连续 → 随机访问;链表 → 双向/前向;树 → 双向;哈希 → 前向。 7. 为什么 `list` 不能 `std::sort`? —— **`sort` 要随机访问**(快排/堆排要跳着选主元和交换),**`list` 的迭代器是双向的**("跳 n 步"要 O(n))。**用成员 `l.sort()`**(归并排序,只需往前挪)。 8. `accumulate` 在哪个头文件?初值有什么讲究? —— **``**(不是 ``)。**初值的类型就是结果的类型** —— `accumulate(v.begin(), v.end(), 0)` 对 `double` 数据会**按 `int` 截断**。**要浮点结果初值写 `0.0`。** 9. `copy` 到空容器为什么崩?怎么修? —— 因为**`dst.begin()` 就是 `end()`,没有可写位置**。**用 `back_inserter(dst)`**(把"写"实现成 `push_back`,写操作自动扩容)。**注意 `back_inserter` 要求容器有 `push_back`(`set` 不行)。** 10. `sort` 的比较函数为什么不能写 `<=`? —— 因为它必须是**严格弱序**:**不能有 `a < a` 为真**。写成 `<=` 会导致**越界访问、崩溃或结果错乱**(报错还常在标准库内部,很难看)。**同理适用于 `set` 的比较器和所有 `_if` 条件。**

相关笔记