文章目录
C++STL学习第三讲(讲解 STL 算法 Algorithm)
1. STL 的算法 Algorithm
1. STL的算法是什么
- 从语言层面讲:
- 算法 Algorithm 是个 function template(函数模板);
- 其他的 STL 部件 是 class template(类模板);
- 算法定义的两种基本形式:如图;
- Algorithm 需要从 Iterators 获取 Containers 的信息;
2. 容器的 iterators 中的 iterator_category
1. 五种 iterator category
-
iterator category 种类:
- input_iterator_tag:只读 iterator;
- output_iterator_tag:只写 iterator;
- forward__iterator_tag;
- bidirectional_iterator_tag;
- random_access_iterator_tag;
-
STL 设计五个空 struct 作为五个 iterator category(不是设置1、2、3、4、5,五个 num);
- 同时 iterator category 之间具有继承关系;
- 同时 iterator category 之间具有继承关系;
2. STL 不同容器的 iterator category
- array:
- 连续空间分布,iterator == pointer;
- random_access_iterator_tag;
- vector:
- 连续空间分布,iterator == pointer;
- random_access_iterator_tag;
- deque:
- 虽然是分段连续,但给使用者感觉是连续空间分布;
- 所以是 random_access_iterator_tag;
- list:
- bidirectional_iterator_tag;
- forward_list:
- forward__iterator_tag;
- set / multiset / map / multimap:
- 底层是 rb_tree,rb_tree 是双向的;
- 所以是 bidirectional_iterator_tag;
- unordered_set / unordered_multiset / unordered_map / unordered_multimap:
- 底层是 hashtable;
- iterator category 取决于 hashtable 链表的实现方式,是单向链表还是双向链表;
- 两个特殊的 iterator:
- istream_iterator:
- input_iterator_tag;
- ostream_iterator:
- output_iterator_tag;
- istream_iterator:
3. 用程序输出每个容器的 iterator category
4. 用程序输出每个容器的 iterator category 的 typeid
- typeid 的输出内容与 C++ 库的实现相关;
- 大体形式:前缀 + 主要名称 + 后缀(基本上是前、后缀有区别);
- 不同的 C++ 实现库,输出内容不一样;
3. 特殊 iterator 的 iterator category
1. istream_iterator 的 iterator category
- G2.9版的 istream_iterator 有两个模板参数;
- G3.3版的 istream_iterator 有四个模板参数;
- G4.9版的 istream_iterator 同样有四个模板参数,而且有继承关系:
- 【将五个 associated types 的 typedef 划分到了父类,这样可以实现代码复用,不用每个 iterator 都手动写一遍五个 associated types 的 typedef;】
- istream_iterator 的 iterator category 定义是 input_iterator_tag;
2. ostream_iterator 的 iterator category
- G2.9版的 ostream_iterator 有一个模板参数;
- G3.3版的 ostream_iterator 有三个模板参数;
- G4.9版的 ostream_iterator 同样有三个模板参数,而且有继承关系:
- 【将五个 associated types 的 typedef 划分到了父类,这样可以实现代码复用,不用每个 iterator 都手动 写一遍五个 associated types 的 typedef;】
- ostream_iterator 的 iterator category 定义是 output_iterator_tag;
4. iterator category 对算法的影响
1. distance 函数
- distance 函数:计算两个 iterator 之间的距离;
- 返回值:
- 向 iterator_traits 询问传入 iterator 的 difference_type;
- 返回 difference_type;
- 函数内部:
- 向 iterator_traits 询问传入 iterator 的 iterator category;
- 将 iterator category 临时对象传入次函数,根据 iterator category 调用对应的次函数版本;
- 对于 random_access_iterator_tag,直接使用减法即可;
- 对于 input_iterator_tag,只能逐步前进,计算移动了多少步;
- 可见 iterator category 类别的不同会影响 Algorithm 的效率;
- 注意点:
- 下图中两个次函数的模板参数名字不同(一个是 InputIterator,另一个是RandomAccessIterator);
- 不要误认为是传入的参数类型不同,单纯只是名字不同;
- 前两个参数都是模板参数,第三个参数不是模板参数,所以第三个参数就是用来区分次函数;
2. advance 函数
- advance 函数:将 iterator 进行移动 n 步;
- 返回值:
- 不需要返回值;
- 函数内部:
- 使用了一个辅助函数 iterator_category(),返回 iterator category 的临时对象(只是把向 traits 询问的部分包装起来);
- 将iterator category 临时对象传入次函数,根据 iterator category 调用对应的次函数版本;
- 对于 random_access_iterator_tag,直接使用 + n 即可;
- 对于 bidirectional_iterator_tag,逐步前进 or 逐步后退;
- 对于 input_iterator_tag,只能逐步前进;
3. copy 函数
- copy 函数:
- 需要传入三个 iterator;
- 将前两个 iterator 范围内的内容复制到目标 iterator;
- 返回值:
- 返回第三个 iterator;
- 注意点:
- function template 没有特化,只能函数重载;
- 下图中的特化是指函数重载(overloading);
- 调用函数过程:
- ① 判断 iterator 是否是 C 风格的字符串 pointer,分为三种情况;
- ② 在①之后判断 iterator 是否是 pointer,分为三种情况;
- 如果是 pointer,还需要通过 type traits 得知是否有 trivial 拷贝赋值函数,确定是否需要显式调用拷贝赋值函数;
- type traits** 不是 STL 的内容,但属于 C++ 标准库;
- 图中右上角和右下角的区别:
- 右上角:
- 每次循环都需要判断两个 iterator 是否相等,比起直接判断整数,更慢一些;
- 右下角:
- 循环多少次只需要判断整数 n;
- 循环多少次只需要判断整数 n;
- 右上角:
4. destory 函数
-
调用函数过程:
- ① 分为四种情况;
- ② 如果选择泛化版本:
- 需要通过 type traits 得知是否有 trivial 析构函数,确定是否需要显式调用析构函数;
- 需要通过 type traits 得知是否有 trivial 析构函数,确定是否需要显式调用析构函数;
-
更具体的图示:
5. unique_copy 函数
-
需要注意的部分:
-
forward_iterator_tag 的次函数版本:
-
其中使用了 iterator 的读取(*运算):
-
if (*result != *first) ...
-
-
【由于 ouput_iterator_tag 是只写 iterator,不允许读取,所以需要为 ouput_iterator_tag 重载一个次函数版本;】
-
ouput_iterator_tag 的次函数版本:
- 没有 read 操作,只有 write;
- 没有 read 操作,只有 write;
-
5. 算法源码对 iterator_category 的“暗示”
1. 算法实现的自由
- 由于所有的 Algorithm 都是 function template;
- 所以理论上可以传入任何 iterator category 的 iterator;
2. 算法使用时的限制
- 但是从源码对模板参数的命名可以看出,算法在实际使用时,对 iterator 的类型是有限制的;
- 如果输入不符合 Algorithm 要求的 iterator,Algorithm 编译不会报错,运行时就会出错;
6. iterator category 设计成 class 的原因
- ① 设计成 class,次函数可以根据 iterator category class 的不同进行函数重载;
- 主函数调用次函数的形式统一,交给编译器进行重载版本选择;
- 【如果设计成 1、2、3、4、5,就不能实现函数重载(都是整型),只能用 if-else】;
- ② iterator category 之间可以有继承关系;
- 次函数不需要为每个 iterator category 都重载一个函数;
- 如果次函数没有子类 iterator category 的重载版本,次函数会自动调用父类的重载版本;【子类自动向上转型】
7. STL 算法的大致设计结构
1. 算法设计结构
- 一个 主 function template;
- 多个对应的 次 function template;
- 次函数根据 iterator_category 参数进行区分;
2. 算法大致内容
- 内容:
- 主函数向 traits 询问传入 iterator 的 iterator_category;
- 使用 iterator 以及 iterator_category 临时对象 调用次函数;
- ALgorithm 的实现部分在次函数;
- 主函数向 traits 询问传入 iterator 的 iterator_category;
8. STL 算法例子
1. accumulate 函数(认识传递 function 的方式)
- 函数有两个版本:
- 一个有三个参数(两个 iterator,一个 init 值);
- 计算 iterator 和 init 的累计和;
- 一个有四个参数,多了一个 operation;
- 根据传入的 operation,计算 iterator 和 init 的累计 operation;
- 一个有三个参数(两个 iterator,一个 init 值);
- 函数使用:
- operation 参数传递:
- 一般函数;
- 仿函数(图中的 std::minus() 就是一个仿函数(functor),同时是个template);
- operation 参数传递:
2. for_each 函数(认识 C++11 新语法)
- 函数参数:
- 三个参数(两个 iterator,一个 function):
- 函数功能:
- 对容器中的每个值执行同一个操作;
- C++11 新语法:
- range-based for statement(基于范围的for语句)
- range-based for statement(基于范围的for语句)
3. replace,replace_if,replace_copy 函数(认识参数命名的方式)
- 函数功能:
- replace:用新值替代旧值;
- replace_if:
- 用新值替代满足条件的值;
- Predicate:返回 bool 函数称为 Predicate;
- replace_copy:类似于 replace,但是将新值放至新区间;
4. 容器是否有同名的成员函数(即自定义版本)
1. count,count_if 函数
- 图左边为 STL 中的一般版本;
- associated containers 有自己的 count 成员函数;
2. find,find_if 函数
- find,find_if 是循序式查找;
- associated containers 有 find 成员函数;
3. sort 函数
- list、forward_list 有 find 成员函数;
- 图中使用了 rbegin()、rend(),逆向迭代器;
5. 关于 reverse iterator,rbegin(),rend()
- rbegin() 和 end() 指向的位置相同,但是取值的方式不同;
- rend() 和 begin() 指向的位置相同,但是取值的方式不同;
- reverse iterator 的转换,通过 reverse_iterator(这是个 iterator adapter)实现;
6. binary_search 函数
- 函数功能:
- 在已排序的序列中,判断序列中是否存在 value;
- 函数步骤:
- 调用 lower_bound 函数:
- 如果 val 存在,返回第一个 val 的位置 iterator;
- 如果 val 不存在,返回 val 应该插入的位置 iterator;
- 根据 lower_bound 返回的位置 iterator,判断是否找到了 value;
- 调用 lower_bound 函数:
- 函数优化:
- 在调用 lower_bound 函数之前,应该先判断是否 val < *first(因为序列已经排序);
- 在调用 lower_bound 函数之前,应该先判断是否 val < *first(因为序列已经排序);