doc/docs/Cpp-Notes-main/Cpp-Primer/10.泛型算法.md
sairate b5160be045 docs(book): 添加现代 C++教程及相关代码
- 新增现代 C++ 教程的 Preface 章节,包括英文和中文版本
- 添加 C++ Primer 练习代码
- 新增 Learn C++ 教程的 C++ 开发简介章节
- 添加头文件解析文档
- 更新 mkdocs.yml,包含新教程的目录结构
- 修改项目设置,使用 Python 3.10环境
2025-07-06 16:14:30 +08:00

226 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 第十章 泛型算法
## 泛型算法
- 因为它们实现共同的操作,所以称之为“**算法**”;而“**泛型**”、指的是它们可以操作在多种容器类型上。
- 泛型算法本身不执行容器操作,只是单独依赖迭代器和迭代器操作实现。
- 头文件: `#include <algorithm>`或者 `#include <numeric>`(算数相关)
- 大多数算法是通过遍历两个迭代器标记的一段元素来实现其功能。
- 必要的编程假定:算法永远不会改变底层容器的大小。算法可能改变容器中保存的元素的值,也可能在容器内移动元素,但不能直接添加或者删除元素。
### find
- `vector<int>::const_iterator result = find(vec.begin(), vec.end(), search_value);`
- 输入:两个标记范围的迭代器和目标查找值。返回:如果找到,返回对应的迭代器,否则返回第二个参数,即标记结尾的迭代器。
## 初识泛型算法
- 标准库提供了超过100个算法但这些算法有一致的结构。
- 理解算法的最基本的方法是了解它们是否读取元素、改变元素、重排元素顺序。
### 只读算法
- 只读取范围中的元素,不改变元素。
-`find``accumulate`(在`numeric`中定义,求和)。
- `find_first_of`,输入:两对迭代器标记两段范围,在第一段中找第二段中任意元素,返回第一个匹配的元素,找不到返回第一段的`end`迭代器。
- 通常最好使用`cbegin``cend`
- `equal`:确定两个序列是否保存相同的值。
### 写容器元素的算法
- 一些算法将新值赋予序列中的元素。
- 算法不检查写操作。
- `fill` `fill(vec.begin(), vec.end(), 0);` 将每个元素重置为0
- `fill_n` `fill_n(vec.begin(), 10, 0);`
- 插入迭代器`back_inserter`
- 用来确保算法有足够的空间存储数据。
- `#include <iterator>`
- `back_inserter(vec)`
- 拷贝算法`copy`
- 输入:前两个参数指定输入范围,第三个指向目标序列。
- `copy (ilst.begin(), ilst.end(), back_inserter(ivec));`
- `copy`时必须保证目标目的序列至少要包含与输入序列一样多的元素。
### 重排容器元素的算法
- 这些算法会重排容器中元素的顺序。
- 排序算法`sort`
- 接受两个迭代器,表示要排序的元素范围。
- 消除重复`unique`
- 之前要先调用`sort`
- 返回的迭代器指向最后一个不重复元素之后的位置。
- 顺序会变,重复的元素被“删除”。
- 并没有真正删除,真正删除必须使用容器操作。
## 定制操作
### 向算法传递函数:
- 谓词(`predicate`
- 是一个**可调用的表达式**,返回结果是一个能用作条件的值
- 一元谓词:接受一个参数
- 二元谓词:接受两个参数
- 例子:
- `stable_sort`
- 保留相等元素的原始相对位置。
- `stable_sort(words.begin(), words.end(), isShorter);`
### lambda表达式
- 有时可能希望操作可以接受更多的参数。
- `lambda`表达式表示一个可调用的代码单元,可以理解成是一个未命名的内联函数。
- 形式:`[capture list](parameter list) -> return type {function body}`
- 其中`capture list`捕获列表是一个`lambda`所在函数定义的局部变量的列表(通常为空)。不可忽略。
- `return type`是返回类型。可忽略。
- `parameter`是参数列表。可忽略。
- `function body`是函数体。不可忽略。
- `auto f = [] {return 42;}`
- 例子:
- `find_if`:
- 接受一对表示范围的迭代器和一个谓词用来查找第一个满足特定要求的元素。返回第一个使谓词返回非0值的元素。
- `auto wc = find_if(words.begin(), words.end(), [sz](const string &a){return a.size() >= sz;});`
- `for_each`
- 接受一个可调用对象,并对序列中每个元素调用此对象。
- `for_each(wc, words.end(), [](const string &s){cout << s << " ";})`
### lambda捕获和返回
- 定义`lambda`时会生成一个新的类类型和该类型的一个对象。
- 默认情况下,从`lambda`生成的类都包含一个对应该`lambda`所捕获的变量的数据成员,在`lambda`对象创建时被初始化。
- **值捕获**:前提是变量可以拷贝,`size_t v1 = 42; auto f = [v1] {return v1;};`
- **引用捕获**:必须保证在`lambda`执行时,变量是存在的,`auto f2 = [&v1] {return v1;};`
- 尽量减少捕获的数据量,尽可能避免捕获指针或引用。
- **隐式捕获**:让编译器推断捕获列表,在捕获列表中写一个`&`(引用方式)或`=`(值方式)。`auto f3 = [=] {return v1;}`
**lambda捕获列表**
| 捕获列表 | 解释 |
|-----|-----|
| `[]` | 空捕获列表。`lambda`不能使用所在函数中的变量。一个`lambda`只有在捕获变量后才能使用它们。 |
| `[names]` | `names`是一个逗号分隔的名字列表,这些名字都是在`lambda`所在函数的局部变量,捕获列表中的变量都被拷贝,名字前如果使用了`&`,则采用引用捕获方式。 |
| `[&]` | 隐式捕获列表,采用引用捕获方式。`lambda`体中所使用的来自所在函数的实体都采用引用方式使用。 |
| `[=]` | 隐式捕获列表,采用值捕获方式。 |
| `[&, identifier_list]` | `identifier_list`是一个逗号分隔的列表包含0个或多个来自所在函数的变量。这些变量采用值捕获方式而任何隐式捕获的变量都采用引用方式捕获。`identifier_list`中的名字前面不能使用`&` |
| `[=, identifier_list]` | `identifier_list`中的变量采用引用方式捕获,而任何隐式捕获的变量都采用值方式捕获。`identifier_list`中的名字不能包括`this`,且前面必须使用`&` |
### 参数绑定
- `lambda`表达式更适合在一两个地方使用的简单操作。
- 如果是很多地方使用相同的操作,还是需要定义函数。
- 函数如何包装成一元谓词?使用参数绑定。
- 标准库`bind`函数:
- 定义在头文件`functional`中,可以看做为一个通用的函数适配器。
- `auto newCallable = bind(callable, arg_list);`
- 我们再调用`newCallable`的时候,`newCallable`会调用`callable`并传递给它`arg_list`中的参数。
- `_n`代表第n个位置的参数。定义在`placeholders`的命名空间中。`using std::placeholder::_1;`
- `auto g = bind(f, a, b, _2, c, _1);`,调用`g(_1, _2)`实际上调用`f(a, b, _2, c, _1)`
- 非占位符的参数要使用引用传参,必须使用标准库`ref`函数或者`cref`函数。
## 再探迭代器
### 插入迭代器
- 插入器是一种迭代器适配器,接受一个容器,生成一个迭代器,能实现向给定容器添加元素。
- 三种类型:
- `back_inserter`:创建一个使用`push_back`的迭代器。
- `front_inserter`创建一个使用`push_front`的迭代器。
- `inserter`创建一个使用`insert`的迭代器。接受第二个参数,即一个指向给定容器的迭代器,元素会被查到迭代器所指向的元素之前。
**插入迭代器操作**
| 操作 | 解释 |
|-----|-----|
| `it=t` | 在`it`指定的当前位置插入值`t`。假定`c``it`绑定的容器,依赖于插入迭代器的不同种类,此赋值会分别调用`c.push_back(t)``c.push_front(t)``c.insert(t, p)`,其中`p`是传递给`inserter`的迭代器位置 |
| `*it, ++it, it++` | 这些操作虽然存在,但不会对`it`做任何事情,每个操作都返回`it` |
### iostream迭代器
- 迭代器可与输入或输出流绑定在一起,用于迭代遍历所关联的 IO 流。
- 通过使用流迭代器,我们可以用泛型算法从流对象中读取数据以及向其写入数据。
**istream_iterator的操作**
| 操作 | 解释 |
|-----|-----|
| `istream_iterator<T> in(is);` | `in`从输入流`is`读取类型为`T`的值 |
|`istream_iterator<T> end;` | 读取类型是`T`的值的`istream_iterator`迭代器,表示尾后位置 |
| `in1 == in2` | `in1``in2`必须读取相同类型。如果他们都是尾后迭代器,或绑定到相同的输入,则两者相等。 |
| `in1 != in2` | 类似上条 |
| `*in` | 返回从流中读取的值 |
| `in->mem` | 与`*(in).mem`含义相同 |
| `++in, in++` | 使用元素类型所定义的`>>`运算符从流中读取下一个值。前置版本返回一个指向递增后迭代器的引用,后置版本返回旧值。 |
**ostream_iterator的操作**
| 操作 | 解释 |
|-----|-----|
| `ostream_iterator<T> out(os);` | `out`将类型为`T`的值写到输出流`os`中 |
| `ostream_iterator<T> out(os, d);` | `out`将类型为`T`的值写到输出流`os`中,每个值后面都输出一个`d``d`指向一个空字符结尾的字符数组。 |
| `out = val` | 用`<<`运算符将`val`写入到`out`所绑定的`ostream`中。`val`的类型必须和`out`可写的类型兼容。 |
| `*out, ++out, out++` | 这些运算符是存在的,但不对`out`做任何事情。每个运算符都返回`out`。 |
### 反向迭代器
- 反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。
- 对于反向迭代器,递增和递减的操作含义会颠倒。
- 实现向后遍历,配合`rbegin``rend`
## 泛型算法结构
### 5类迭代器
| 迭代器类别 | 解释 | 支持的操作|
|-----|-----|-----|
| 输入迭代器 | 只读,不写;单遍扫描,只能递增 | `==`,`!=`,`++`,`*`,`->` |
| 输出迭代器 | 只写,不读;单遍扫描,只能递增 | `++`,`*` |
| 前向迭代器 | 可读写;多遍扫描,只能递增 | `==`,`!=`,`++`,`*`,`->` |
| 双向迭代器 | 可读写;多遍扫描,可递增递减 | `==`,`!=`,`++`,`--`,`*`,`->` |
| 随机访问迭代器 | 可读写,多遍扫描,支持全部迭代器运算 | `==`,`!=`,`<`,`<=`,`>`,`>=`,`++`,`--`,`+`,`+=`,`-`,`-=`,`*`,`->`,`iter[n]`==`*(iter[n])` |
### 算法的形参模式
- `alg(beg, end, other args);`
- `alg(beg, end, dest, other args);`
- `alg(beg, end, beg2, other args);`
- `alg(beg, end, beg2, end2, other args);`
其中,`alg`是算法名称,`beg``end`表示算法所操作的输入范围。`dest``beg2``end2`都是迭代器参数,是否使用要依赖于执行的操作。
### 算法命名规范
- 一些算法使用重载形式传递一个谓词。
- 接受一个元素值的算法通常有一个**不同名**的版本:加`_if`,接受一个谓词代替元素值。
- 区分拷贝元素的版本和不拷贝的版本:拷贝版本通常加`_copy`
## 特定容器算法
- 对于`list``forward_list`,优先使用成员函数版本的算法而不是通用算法。
**list和forward_list成员函数版本的算法**
| 操作 | 解释 |
|-----|-----|
| `lst.merge(lst2)` | 将来自`lst2`的元素合并入`lst`,二者都必须是有序的,元素将从`lst2`中删除。 |
| `lst.merge(lst2, comp)` | 同上,给定比较操作。 |
| `lst.remove(val)` | 调用`erase`删除掉与给定值相等(==)的每个元素 |
| `lst.remove_if(pred)` | 调用`erase`删除掉令一元谓词为真的每个元素 |
| `lst.reverse()` | 反转`lst`中元素的顺序 |
| `lst.sort()` | 使用`<`排序元素 |
| `lst.sort(comp)` | 使用给定比较操作排序元素 |
| `lst.unique()` | 调用`erase`删除同一个值的连续拷贝。使用`==`。 |
| `lst.unique(pred)` | 调用`erase`删除同一个值的连续拷贝。使用给定的二元谓词。 |
- 上面的操作都返回`void`
**list和forward_list的splice成员函数版本的参数**
| 参数 | 解释 |
|-----|-----|
| `(p, lst2)` | `p`是一个指向`lst`中元素的迭代器,或者一个指向`flst`首前位置的迭代器。函数将`lst2`中的所有元素移动到`lst``p`之前的位置或是`flst``p`之后的位置。将元素从`lst2`中删除。`lst2`的类型必须和`lst`相同,而且不能是同一个链表。 |
| `(p, lst2, p2)` | 同上,`p2`是一个指向`lst2`中位置的有效的迭代器,将`p2`指向的元素移动到`lst`中,或将`p2`之后的元素移动到`flst`中。`lst2`可以是于`lst``flst`相同的链表。 |
| `(p, lst2, b, e)` | `b``e`表示`lst2`中的合法范围。将给定范围中的元素从`lst2`移动到`lst``first`中。`lst2``lst`可以使相同的链表,但`p`不能指向给定范围中的元素。 |
- 使用`lst.splice(args)``flst.splice_after(args)`