C++算法库定义了一系列函数,用于查找、排序、计数和操纵某个范围(range),这个范围是按照左闭右开的方式给出的,也就是容器迭代器中的[first,last)。从算法要求角度迭代器可以分为五类,分别是输入、输出、单向、双向和随机;迭代器的来源有两个地方:
- 通过调用容器成员(
begin()
、end()
之类的) - 标准库iterator处理得到的
对于后者,可以分为插入、流、反向和移动四种。
一、迭代器方法及分类
迭代器是类模板,它定义了遍历容器的操作。那么这些操作有哪些呢?
1.1 迭代器方法
迭代器的基本操作:自增自减解引用返成员及判等,如果一个类重载了上述操作,那么就符合迭代器的概念。
在vector和string中,我们既可以通过下标访问其中的元素,也可以通过迭代器。之所以说迭代器更加通用,是因为不是所有的标准库都支持下标操作,但都支持迭代器操作。所以指针和迭代器有什么不一样?获取方式不同。指针通过取地址方式,迭代器通过成员函数(begin和end之类的)。对于不同容器,迭代器支持的操作可能不同。
所有迭代器都有的操作(自增自减解引用返成员及判等):
操作 | 含义 |
---|---|
*iter | 返回迭代器所指元素的引用(解引用) |
iter->mem | 解引用并返回mem成员 |
++iter | 迭代器指向下一个元素 |
–iter | 迭代器指向上一个元素 |
iter1==iter2 | 判断两个迭代器是否相等,如果解引用相同,则为真 |
iter1!=iter2 | 判断两个迭代器是否不等,如果解引用不同,则为真 |
有些迭代器可能支持额外的操作:
string和vector支持的额外操作 | 含义 |
---|---|
iter+n | 迭代器加一个整数值仍为迭代器,和原位置相比向前移动了若干个元素。结果可能是容器内或尾部迭代器 |
iter-n | 迭代器减一个整数值仍为迭代器,和原位置相比向后移动了若干个元素。结果可能是容器内或尾部迭代器 |
iter1+=n | 迭代器复合加法语句,iter1+n的结果赋给iter1自身 |
iter1-=n | 迭代器复合减法语句,iter1-n的结果赋给iter1自身 |
iter1-iter2 | 右侧迭代器向前移动若干个元素的得到左侧的迭代器,必须保证是同一个容器,结果是迭代器之间的距离(可正可负) |
、>=、<、<=|迭代器关系运算符,表示迭代器的前后关系。必须保证是同一个容器,结果是布尔值。
1.2 分类
STL算法中使用迭代器作为输入和输出,具体到不同算法对迭代器的要求也有所区别,有些算法( std::find, std::equal, std::count)只要求迭代器具备访问、递增和比较的能力;而有些算法(sort)则要求读写、随机访问能力;对于reverse则要求双向读写。我们知道不是所有容器都具有“理想”迭代器的所有功能,如我们的forward_list就不具备“后撤”、随机访问能力,如果迭代器加上const属性,那就不具备写的能力。每个算法都会对迭代器参数进行指定,表明其需要迭代器具备何种能力才能帮忙完成我的算法需求。
标准库容器一般实现了对应的迭代器,通过begin()
end()
等返回对应位置迭代器,这一途径获取的迭代器简单来说可以分为五个迭代器类别(iterator category):
迭代器类别 | 特点 |
---|---|
输入迭代器 | 只读、不写;单遍扫描,只能递增(++) |
输出迭代器 | 只写、不读;单遍扫描,只能递增(++) |
前向迭代器 | 可读写;多遍扫描,只能递增(–) |
双向迭代器 | 可读写;多遍扫描,能递增、递减(++ --) |
随机访问迭代器 | 可读写;多遍扫描,支持迭代器所有操作 |
使用一个迭代器需要考量三个方面的内容:
- 读写性如何?
- 扫描性如何?
- 方向性如何?
这五种迭代器层次(hierarchy)关系如上图所示。处于上游的迭代器一定满足下游迭代器要求,反之则不一定。如,vector的迭代器属于随机访问迭代器,因为他处于上游,故它可以再算法参数要求为剩余四个迭代器中使用。
练习1:标准库forward_list lst迭代器属于什么类型的?
答:forward_list可读可写,只可以前向移动,且可以多次扫描。
练习2:标准库迭代器库中的istream_iterator属于什么类型的?
答:istream_iterator只写不读,只能单次扫描,前向运动。因此属于输入迭代器。
由上图可以看出,前向迭代器、双向迭代器和随机访问迭代器也是一个有效的输入迭代器。(简单来说,如果迭代器是前向、双向和随机迭代器,那么它肯定具备输入迭代器要求,是一个输入迭代器)
Tips: 只要你使用的是非const的vector和string那么你完全不用考虑算法对迭代器的“要求”。
二、功能划分的迭代器
向一个算法传递一个错误类别的迭代器,很多编译器并不会给出任何警告。按照迭代器的功能划分为五种类型的迭代器:
2.1 输入迭代器(input iterator)
- 判等(==、!=)
- 推进(++)
- 解引用 (*)
- 箭头 (->)
输入迭代器只用于顺序访问,大概是算法中使用了类似*it++[1]的运算,使用了递增运算符,意味着使用完了迭代器就失效了。因此输入迭代器适用于单遍扫描。算法find
accumulate
;istream_iterator
是输入迭代器。
2.2 输出迭代器(output iterator)
- 推进(++)
- 解引用 (*)后作为左值
输出迭代器只能赋值一次,只适用于一次扫描。如copy的第三个参数就是输出迭代器。ostream_iterator
是输出迭代器。
template< class InputIt, class OutputIt >
OutputIt copy( InputIt first, InputIt last, OutputIt d_first );
std::copy将一个容器范围的元素拷贝到另一个容器中。输入迭代器能够很好的完成访问的任务(前两个参数),第三个参数是需要写值的,那么她就不能使用输入迭代器,而是一个允许写的迭代器——输入迭代器。需要注意的是写容器算法假定空间是足够的,新手容易犯下往空容器写元素的错误:
vector<int> vec;
fill_n(vec.begin(),10,0);//算法不会检查这样的错误,运行时才会发现
我们常常会使用back_iterater迭代器来完成写的操作。该迭代器将会在赋值之前创建空间,防止出现没有空间的错误。
vector<int> vec;
fill_n(back_inserter(vec),10,0);
2.3 前向迭代器(forward iterator)
前向迭代器是可读可写,只能往一个方向多次扫描》》》》》》》而不能《《《《《《《《。最典型的例子就是replace和replace_if:
template< class ForwardIt, class T >
void replace( ForwardIt first, ForwardIt last,
const T& old_value, const T& new_value );
template< class ForwardIt, class UnaryPredicate, class T >
void replace_if( ForwardIt first, ForwardIt last,
UnaryPredicate p, const T& new_value );
下面就是将vector中的6替换成66,将大于5的替换成0。
std::vector<int> v = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; // or std::vector<int>v(10, 2);
std::replace(v.begin(), v.end(), 6, 66);
std::replace_if(v.begin(), v.end(), [](int ele) {return ele > 5; }, 0);
注意:虽然名字似乎和forward_list相关(都是前向),这是最低要求,事实上所有标准库(string vector list forward_list deque)都能满足。
2.4 双向迭代器(bidirectional iterator)
双向迭代器与前向迭代器相比,多了一个方向,也就是反向扫描(--
)。除了forward_list都有满足要求的双向迭代器(begin end),双向扫描。
2.5 随机访问迭代器(random-access iterator)
随机访问迭代器是可以在常数时间内访问到容器中的任意元素的能力。除了链表类的容器,都具有随机访问能力。
三、算法对迭代器的要求
算法内部可能对迭代器进行如下操作:
- 改写迭代器所指向的值
- 正反扫描容器中的值
- 解引用、访问某个数据成员
对应到迭代器就有:
- 读写要求
- 扫描方向要求
- 访问要求
我们在使用算法时只需要传递正确的迭代器进去即可。什么叫做不正确的!传递一个能力比较差的迭代器进去,如前面提到的fill_n,显然我们是需要改写迭代器的值的,就不能只传递一个输入迭代器。一般来说,使用算法如果你不是传递一个cend()和cbegin()之类的const迭代器,就不需要考虑读写之类的问题;如果你使用的不是链表之类的容器,那你总能使用所有的算法,毕竟除了链表,剩下的全是随机访问能力的容器了,已经算法最高要求了。
四、算法参数形式
绝大部分算法参数形式都属于以下形式中的一种:
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和beg2 end2两种,两者都是迭代器参数:
- dest
目标迭代器,算法假定了其需要写入参数,不管写多少都是安全的 - beg2 end2
它可以是完整的迭代器范围[beg2 end2 );也可以是只指定[beg2, + ∞ +\infin +∞)也就是结束位置不确定,假定至少和完整迭代器一样大。
五、算法命名规范
除了参数规范外,算法还遵循一套命名和重载规范。当然,谓词也有一定的规范,如定义< ==以及拷贝原序列还是覆盖原序列。
5.1 重载,增加谓词
unique(beg,end);
unique(beg,end,comp);//comp定义了什么是unique
谓词丰富了算法的使用范围,将解释的权力交给程序员。
5.2 _if假设版本
相对于非if版本,_if版本将值换成了谓词。将value切确的值变成了模糊的、满足一定条件的元素。
find(beg,end,val);
find(beg,end,pred);//不再是切确的val,而是模糊的pred(如大于某个值)
5.3 _copy拷贝版本
相对于非copy版本,_copy不是直接"污染"源序列,而是给定一个新的迭代器(或迭代器范围),完全不改变原序列地完成算法结果的输出。
reverse(beg,end);
reverse(beg,end,dest);//不再是直接输出到原序列,而是输出在dest
5.4 _copy_if 拷贝假设版本
如:
remove_if(v1.begin(),v1.end(),[](int i){return i%2;});
remove_copy_if(v1.begin(),v1.end(),back_inserter(v2),[](int i){return i%2;});
5.5 特定容器算法
因为某些容器具备某些特征,通用算法可能需要独特定制。如list和forward_list,他们就针对其容器特点,定制了更加高效、合适的算法版本,不过他们是以成员函数的形式给出的。
//都返回void
lst.merge(lst2);//有序链表lst2 lst合并,使用<运算进行合并
lst.merge(lst2,comp);//有序链表lst2 lst合并,使用comp运算进行合并
lst.remove(val);//调用erase ==val的值
lst.remove_if(pred);//调用erase 符合pred 的val的值
lst.reverse();//反转元素顺序
lst.sort();//<比较排序
lst.sort(comp);//comp比较排序
lst.unique();//调用erase删除重复元素
lst.unique(pred);//调用erase删除符合pred元素
链表类型还定义了splice算法,这个算法是链表独有的:
lst.splice(args) flst.splice(args)
(p,lst2)
(p,lst2,p2)
(p,lst2,b,e)
此外,需要注意的这种成员函数版的算法和通用的算法相比,他会直接改变底层重复元素。
六、功能划分算法
- Non-modifying sequence operations
- Modifying sequence operations
- Partitioning operations
- Sorting operations
- Binary search operations (on sorted ranges)
- Other operations on sorted ranges
- Set operations (on sorted ranges)
- Heap operations
- Minimum/maximum operations
- Comparison operations
- Permutation operations
- Numeric operations(Defined in header )
- Operations on uninitialized memory(Defined in header )
- C library(Defined in header )
详细的函数说明可以查阅:https://en.cppreference.com/w/cpp/algorithm
[1] ++操作符的优先级高于*号
[2] https://www.geeksforgeeks.org/input-iterators-in-cpp/
[3] StanleyB.Lippman, JoseeLajoie, BarbaraE.Moo,等. C++ Primer中文版[J]. 2013.