C++ 泛型算法
Outline:
- 概述
- 泛型算法概览
- 定制操作
- 再探迭代器
- 泛型算法结构
- 特定容器算法
概述
- 头文件
<algorithm>
,<numeric>
- 泛型算法: 对不同类型容器的通用算法
- 大多数情况下,泛型算法操作迭代器,而不是直接操作容器
- 泛型算法不依赖容器,但依赖于元素类型的操作,比如运算符重载
算法概览
只读算法
find
accumulate
- 必须有合适的
+
运算符string sum = accumulate(v.cbegin(), v.cend(), string(""));
string sum = accumulate(v.cbegin(), v.cend(), "");
是错误的,因为const char*
没有+
运算符
- 必须有合适的
equal
- 必须有合适的
==
运算符
- 必须有合适的
写容器元素的算法
fill
接受一对迭代器表示范围fill(vec.cbegin(), vec.cend() + vec.size()/2, 10 );
`
fill_n( dest, n , val )
接受一个迭代器来指出单独的目的位置copy( begin, end, dest )
unique(begin, end)
重排输入范围,使得不重复的元素出现在容器的开始部分
返回指向不重复区域之后一个位置的迭代器,
若没有不重复区域,则返回尾后迭代器
与
erase
搭配使用,删除重复的元素
1
2
3
4
5
6
7
8
9
10//按字典序排序words并删除重复单词
void elimDups( vector<string> &words )
{
sort( words.begin(), words.end() );
auto end_unique = unique( words.begin(),words.end() );
words.erase( end_unique , words.end());
}测试:
1
2
3
4
5
6
7
8
9
10int main()
{
istream_iterator<string> in_iter(cin), eof;
ostream_iterator<string>out_iter( cout, " " );
vector<string>vec(in_iter, eof);
elimDups( vec );
copy(vec.begin(), vec.end(), out_iter);
}1
aaa bbb ddd ccc aaa //输入
1
aaa bbb ccc ddd //输出
算法假定:
- 很多算法操作两个序列, 它们接受接受第三个的迭代器来表示第二个序列的目标位置,这些算法都假定, 第二个序列至少与第一个序列一样长
- 如
equal
,copy
- 返回(递增后的)目的位置迭代器
- 如
- 很多算法操作两个序列, 它们接受接受第三个的迭代器来表示第二个序列的目标位置,这些算法都假定, 第二个序列至少与第一个序列一样长
一些算法接受一个单独的迭代器来指出一个单独的目的位置,这类算法不检查写操作,因此越界访问是
undefined behavior
比如,不能在空容器上调用
fill_n
- 可以用
fill_n(back_inserter(),10,0)
,每次通过此迭代器赋值时,赋值运算符被重载为调用push_back
,这就不用担心越界访问
- 可以用
定制操作
谓词
- 定义: 返回值为
bool
的可调用对象- 一元谓词: 只接受一个参数
- 二元谓词: 接受两个参数
- 可调用对象:可以对其使用调用运算符的对象或表达式
- 函数和函数指针
- 重载了函数调用运算符的类
- lambda表达式
- 算法对接受的谓词有要求,为了绕过这个限制,可以使用lambda表达式
find_if()
接受一个一元谓词,但有时该谓词函数需要不止一个参数
lambda的应用
for_each(begin,end, callable)
:接受一个可调用对象
再探迭代器
除了标准迭代器外,还有以下几种迭代器,头文件:<iterator>
insert iterator
stream iterator
reverse iterator
move iterator
Insert Iterator
- 迭代器适配器, 接受一个容器,生成一个迭代器
- 调用容器操作向给定容器的指定位置插入一个元素
操作 | 解释 |
---|---|
it = t |
在it 指定的当前位置插入t . 假定 c 是it 绑定的容器,依赖于插入迭代器的不同种类, 此赋值会分别调用 push_back(t) ,push_front(t) ,insert(t,p) , 其中p 为传递给inserter 的迭代器位置 |
*it , ++it , it++ |
空操作。 都返回it |
迭代器适配器 | 功能 |
---|---|
back_insert_iterator |
创建一个使用push_back 的迭代器(这意味着不会发生越界访问,容器大小永远足够),前提是提供有 push_back() 成员方法的容器(包括 vector、deque 和 list)。 |
front_insert_iterator |
创建一个使用push_front 的迭代器,前提是提供有 push_front() 成员方法的容器(包括 list、deque 和 forward_list)。 |
insert_iterator |
在容器的指定位置之前插入新元素,前提是该容器必须提供有 insert() 成员方法。 |
当调用
inserter(c, iter)
时,得到一个迭代器,接下来使用它时,会将元素插入到iter
原来所指向的元素之前的位置, 即,如果it
是由inserter
生成的迭代器,则:1
*it = val;
效果与下面的代码一样:
1
2it = c.inserter(it,val);//it指向新加入的元素
++it; //递增it使它指向原来的元素front_inserter
生成的迭代器与inserter
生成的完全不同。当调用front_inserter
时,元素总是插入到容器第一个元素之前:1
2
3
4list<int> lst = {1,2,3,4};
list<int> lst2, lst3 ; // empty list
copy(lst.cbegin(), lst.cend(), front_inserter(lst2)); //拷贝完成后, lst2包含4,3,2,1
copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin()));//拷贝完成后, lst3包含1,2,3,4当调用
push_front(c)
时,得到一个插入迭代器,接下来会调用push_front
iostream Insrator
itstream
不是容器,但STL
定义了可以用于这些IO类型对象的迭代器。istream_iterator
读取输入流,ostream_iterator
向一个输出流写数据。 这些迭代其将它们对应的流当作一个特定类型的元素序列来处理。 通过使用流迭代器,我们可以使用泛型算法从流对象读取数据以及向其写入数据。
- 可以为任何定义了
>>
和<<
运算符的类型创建istream_iterator
和ostream_iterator
istream_iterator操作
当创建流迭代器时,必须指定迭代器将要读写的对象类型。 一个istream_iterator
使用>>
来读取流。 因此, istream_iterator
要读取的类型必须定义了输入运算符。
- 当创建一个
istream_iterator
时, 我们可以将它绑定到一个流。 当然,我们还可以默认初始化迭代器,这样就创建了一个尾后迭代器
1 | vector<int> vec; |
该程序可以改写为:
1 | istream_iterator<int> in_iter(cin) , eof; //从cin读取int |
- 可以用一对表示元素范围的迭代器构造
vec
- 这两个迭代器是
istream_iterator
, 这意味着元素范围是通过从关联的流中读取数据获得的, 这个构造函数从cin
中读取数据,直至遇到文件尾或者遇到一个不是int
的数据位置。
使用算法操作流迭代器
算法使用迭代器,而流迭代器至少支持某些迭代器操作,因此至少可以用某些算法来操作流迭代器
1 |
|
istream_iterator允许使用懒惰求值
- 当
istream_iterator
绑定到一个流时,标准库并不保证迭代器立即从流中读取数据。 具体实现可以直到使用迭代器时才真正读取。
ostream_iterator
可以对任何具有<<
运算符的类型定义ostream_iterator
. 当创建ostream_iterator
时,我们可以提供(可选的)第二个参数。它是一个C风格字符串, 在输出每个元素后都会打印此字符串
- 必须将
ostream_iterator
绑定带一个指定的流。 不允许空的或者表示尾后位置的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 可写的类型兼容( 即为T ) |
*out ,++out , out++ |
这些运算符存在,但不对out 做任何改变。 均返回out |
使用
ostream_iterator
输出值的序列1
2
3
4
5
6
7
8
9
10
11
12
13int main()
{
istream_iterator<int> in_iter(cin), eof;
vector<int>vec(in_iter, eof);
ostream_iterator<int>out_iter( cout, " " );//使用ostream_iterator输出值的序列
for( auto e: vec )
{
*out_iter++ = e;
}
cout << endl;
return 0;
}事实上,
*
和++
不对ostream_iterator
对象做任何事,因此可以写成:1
2
3
4for( auto e: vec )
{
out_iter = e;
}但推荐前者,因为易于理解
当然,还可以通过
copy
来打印vector
中的元素:1
copy(vec.begin(), vec.end(), out_iter);
reverse_iterator
与普通迭代器一样,只是是反向的
- 除了
forward_list
之外,所有容器都支持反向迭代器
- 除了
可以让算法透明地向前或向后处理容器:
1
sort(vec.rbigin(). vec.rend());
除了流迭代器,其余迭代器都支持递减运算
反向迭代器的
base()
可以返回对应的正向迭代器- 返回的正向迭代器的位置在原反向迭代器的后一位(按正序排列)
反向迭代器的删除:
1
2
3
4
5
6
7
8for( auto: rit: vec,begin(); vec.end(); )
{
if( *tir == XX )
rit = decltype(rit)( erase( ++rit.base() ) );
else{
rit++;
}
}erase()
只接受正向迭代器,因此要base()
转换注意到
decltype(rit)
将正向迭代器转为反向迭代器时,会将位置往前移一位(与之前后移一位对应),避免了手动++rit
例子:打印最后一个逗号后的字符串
1
2
3string line("HELLO, MIKE!");
auto rcomma = find( line.crbegin(), line.crend(), ',' );
cout << string( line.crbegin(), rcomma ) << endl;输出为:
1
!EKIM
想要正确输出
MIKE!
,要使用正向迭代器:1
cout << string( rcomma.base(), line.cend() ) << endl;
泛型算法结构
任何算法都对其迭代器提供的操作有要求,这里将迭代器分为五类:
name 解释 例子 输入迭代器 只读,不写;单遍扫描,只能递增 istream_iterator
输出迭代器 只写,不读; 单遍扫描,只能递增 ostream_iterator
前向迭代器 可读写,多遍扫描,只能递增 forward_list
上的迭代器双向迭代器 可读写,多遍扫描,可递增递减 很多 随机访问迭代器 可读写,多遍扫描,支持全部迭代器运算 vector
,string
,deque
. etc
算法形参模式
大多数算法具有如下参数规范之一:
1 | alg(beg,end,other args); |
dest
: 算法可以写入的目的位置的迭代器- 算法假定:目标空间足够容纳写入的数据
dest
经常被绑定到一个插入迭代器或ostream_iterator
算法命名规范
一些算法使用重载形式传递一个谓词
1 | unique(beg,end); |
_if版本的算法
接受一个元素值的算法通常有一个不同名(因此非重载)的_if
版本,它接受一个谓词来代替元素值:
1 | find( beg, end, val ); |
区分拷贝元素和不拷贝的版本
默认情况下,重排元素的算法将重排后的元素写回给定的输入序列中。 这些算法还提供另一个版本,将元素写入一个指定的输出目的位置, 这些算法都在名字后面附加一个_copy
1 | reverse(beg,end);// 翻转输入序列中元素的顺序 |
1 | replace( lst.begin(), lst.end(), 0, 42 );//将序列中的所有0替换为42 |
一些算法同时提供_copy
和_if
版本,接受一个dest
和一个谓词:
1 | //从v1中删除奇数元素 |
特定容器算法
list
和forward_list
定义了几个成员函数形式的算法,对于这类容器,应当优先使用使用成员函数版本的算法而不是通用算法