《C++ Primer》 拾遗 第 10 章 泛型算法
Notes Cpp Primer
Lastmod: 2021-07-05 周一 00:51:04

第 10 章 泛型算法

泛型算法 generic algorithm

10.1 概述

大多数算法定义在头文件 algorithm 中。另外在 numeric 种定义了一组数值泛型算法。

一般来说这些算法并不直接操作容器,二十遍历由两个迭代器指定的一个元素范围来进行操作。

10.2 初识泛型算法

10.2.1 只读算法

accumulate(c.cbegin(), c.cend(), init);
操作两个序列的算法

一般操作两个序列的算法,第二个序列只接受一个迭代器时,都假定第二个序列至少和第一个一样长。

equal(vec.cbegin(), vec.cend(), vec2.cbegin());

10.2.2 写容器元素的算法

fill(c.begin(), c.end(), val);
算法不检查写操作
vector<int> vec;
fill_n(vec.begin(), 10, 0);

如上例,fill_n 并不检查是否有足够空间写入 10 个元素,应当由程序员来保证。

介绍 back_inserter

可以使用插入迭代器 insert iterator 来保证有足够空间插入元素。

fill_n(back_inserter(vec), 10, 0);
拷贝算法
auto ret = copy(vec.begin(), vec.end(), vec2.begin());

返回递增之后的迭代器。

replace(vec.begin(), vec.end(), oldVal, newVal);
replace(vec.begin(), vec.end(), back_inserter(vec2), oldVal, mewVal);

10.2.3 重排容器元素的算法

sort

unique

10.3 定制操作

谓词 predicate

一元谓词 unary predicate

二元谓词 binary predicate

10.3.2 lambda 表达式

可以向一个算法传递任何类别的可调用对象 callable object

lambda 表达式 lambda expression

[capture list](parameter list) -> return type { function body }

10.3.3 lambda 捕获和返回

值捕获

注意捕获发生在 lambda 创建时,而传入参数是调用时。

引用捕获

引用捕获的变量必须保证 lambda 被调用时还是存在的。

[]      // 空捕获列表
[names] // 逗号分隔的捕获列表,名字前使用 & 的是引用捕获
[&]     // 隐式捕获列表,所有变量采用引用捕获
[=]     // 隐式值捕获
[&, identifier_list] // identifier_list 中的变量使用值捕获,
                     // 其余使用引用捕获
[=, identifier_list] // identifier_list 中的变量使用引用捕获,且不能
                     // 包含 this
                     // 其余使用值捕获,且每个变量前必须加 &
可变 lambda

值被拷贝的变量,lambda 不会改变其值。如果我们希望能改变一个被捕获的变量的值,那么需要增加 mutable 关键字。

指定 lambda 返回类型

当 lambda 只有一个返回值时,由于返回值类型是可推断的,可以不指定。

当 lambda 不能推断返回值类型时可以指定返回值类型。

[](int i) -> int { if (i < 0) return -i; else return i; }

10.3.4 参数绑定

如果 lambda 的捕获列表为空,则通常可以用函数代替它。对于有捕获的 lambda 我们可以利用 bind

标准库 bind 函数
auto newCallable = bind(callable, arg_list);

其中 arg list 可能包含形如 _n 的占位符,n 是某个整数,代表 newCallable 的第 n 个参数,下标从 1 开始。因此 newCallable 参数顺序与 callable 无关。

将使用 arg_list 里面的参数调用 callable。

要使用这些占位符需要

using namespace std::placeholders;
绑定引用参数

bind 会拷贝它的参数,但是如果我们希望以引用方式传递则需要 ref() 。

void f(int &a);
int b;
auto ff = bind(f, ref(b));

ref 返回一个对象,包含给定的引用。此外 cref 函数,生成一个保存 const 引用的类。与 bind 一样,这些函数定义在头文件 functional 中。

注意新标准中已经弃用了限制比较大的 bind1st 和 bind2nd 函数。

10.4 再探迭代器

除了每个容器定义的迭代器外,标准库在头文件 iterator 中还定义了额外几种迭代器。

插入迭代器 insert iterator 绑定到容器,向容器插入元素

流迭代器 stream iterator 绑定到输入输出流,用来遍历关联的 IO 流

反向迭代器 reverse iterator 与一般的迭代器移动方向相反

移动迭代器 move iterator 这些迭代器不拷贝其中元素而是移动它们

10.4.1 插入迭代器

it = t; // 向 it 指定的当前位置插入值 t。
*it; ++it; it++; // 操作存在但不会对 it 做任何事。返回 it。
back_inserter; // 使用 push_back
front_inserter; // 使用 push_front
inserter; // 接受两个参数,第一个参数是容器,第二个参数是迭代器,
          // 插入到给定迭代器之前

10.4.2 iostream 迭代器

istream_iterator 操作

对于 istream_iterator<T> 需要类型 T 定义 >>

#include <iostream>
#include <iterator>
using namespace std;

int main() {
   istream_iterator<int> int_it(cin), int_eof;
   while(int_it != int_eof) {
       cout << *int_it++ <<endl;
   }
   return 0;
}
istream_iterator<T> in(is); // in 从输入流 is 读取 T 的值
istream_iterator<T> end;    // 尾后迭代器
in1 == in2;                 // 读取相同类型且
                            // 同为尾后迭代器或绑定到相同输入
in1 != in2;
*in;                        // 读取流中的值
in->mem                     // (*in).mem
++in, in++;                 // 读取流中的下一个值

当然我们也可以像使用其他迭代器一样使用流迭代器

accumulate(in, eof, 0)
istream_iterator 允许使用懒惰求值

istream_iterator 绑定到流时,标准库并不保证迭代器立即从流读取数据,直到我们使用迭代器时才真正读取。标准库的实现保证的是,第一次解引用迭代器之前,从流中读取数据的操作已经完成了。

ostream_iterator 操作

对于 ostream_iterator<T> 需要类型 T 定义 <<

ostream_iterator<T> out(os);
ostream_iterator<T> out(os,d); // d 是一个 C 风格字符串
                               // 每次输出都会追加一个 d
out = val;                     // val 写入绑定的流
*out, ++out, out++;            // 存在但无用,返回 out 统一迭代器行为

10.4.3 反向迭代器

反向迭代器 ++-- 的方向与普通迭代器相反

反向迭代器需要支持递减运算符,因此不能从 forward_list 和流迭代器创建反向迭代器。

反向迭代器和其他迭代器的关系

注意我们不能混用普通迭代器和反向迭代器,我们可以使用 base 函数将反向迭代器转换为普通迭代器

           rcomma
            v
FIRST,SECOND,THIRD
             ^
             rcomma.base()

注意为了使反向迭代器与普通迭代器表示的范围一致,因此实际指向的位置会不同。

10.5 泛型算法结构

迭代器类别 iterator category

迭代器类别 操作
输入迭代器 只读,不写;单遍扫描,只能递增
输出迭代器 只写,不读;单遍扫描,只能递增
前向迭代器 可读写;多遍扫描,只能递增
双向迭代器 可读写;多遍扫描,可递增递减
随机访问迭代器 可读写;多遍扫描,支持全部迭代器运算

10.5.1 5 类迭代器

C++ 标准指明了泛型和数值算法的每个迭代器参数的最小类别。

对于每个迭代器参数,其能力至少与最小类别相当。

输入迭代器 input iterator

用于比较两个迭代器的相等和不相等运算符 == !=

用于推进迭代器的前置和后置递增运算 ++

用于读取元素的解引用运算符 * 解引用只能出现在赋值运算符的右侧

箭头运算符 ->

输出迭代器 output iterator

用于推进迭代器的前置和后置递增运算 ++

用于读取元素的解引用运算符 * 解引用只能出现在赋值运算符的左侧

前向迭代器 forward iterator

支持所有输入输出迭代器操作,且可以多次读写同一元素。

双向迭代器 bidirectional iterator

支持所有前向迭代器的操作,支持前置和后置递减运算符 --

随机访问迭代器 random-access iterator

支持双向迭代器所有功能,提供常数时间访问序列中任意元素的能力。

用于比较两个迭代器相对位置关系的关系算符 < <= > >=

迭代器和一个整数值的加减运算 + += - -=

两个迭代器上的 -

下标运算符 iter[n]

10.5.2 算法形参模式

大多数算法有如下 4 种形式之一

alg(beg, end, other args);
alg(beg, end, dest, other args);
alg(beg, end, beg2, other args);
alg(beg, end, beg2, end2, other args);

dest 一般假定 assume 能够写入足够多的数据。

只有 beg2 (没有 end2)作为输入的算法假定 beg2 开始的范围与 beg end 的范围至少一样大。

10.5.3 算法命名规范

一些算法采用重载形式传递谓词来代替 <==

unique(beg, end);
unique(beg, end, comp);

有些算法本身会接受一个 val 因此使用谓词的版本会加上 _if

find(beg, end, val);
find_if(beg, end, pred);

有些算法需要区分写回原序列和写到新序列,使用 _copy

reverse(beg, end);
reverse_copy(beg, end, dest);

remove_copy_if(beg, end, dest, pred);

10.6 特定容器算法

对于 list 和 forward_list

lst.merge(lst2);
lst.merge(lst2, comp); // 要求两个链表有序
lst.remove(val);
lst.remove_if(pred);
lst.reverse();
lst.sort();
lst.sort(comp);
lst.unique();
lst.unique(pred);
splice 成员

lst.splice 和 flst.splice_after

(p, lst2) // p 是 lst 的迭代器或 flst 的首前迭代器。将 lst2 元素插入到对应位置。将元素从 lst2 中删除。
(p, lst2, p2) // 将 p2 之后的元素移动插入到 p
(p, lst2, b, e) // 将 [b, e) 中元素移动插入到 p
Prev: 《C++ Primer》 拾遗 第 9 章 顺序容器
Next: [模板][计算几何] 平面最近点对