generic algorithm
一, 概述
1,标准库没有为每种容器类型都定义是吸纳某些特定操作的成员函数,而是定义了一组泛型算法;自定义的容器类型只要与标准库兼容,也可以使用这些算法。
2,每个泛型算法的实现独立于容器,且不依赖于容器存储的元素类型;
3,算法往往需要通过两个迭代器遍历一段元素来实现其功能;
a) 迭代器支持自增操作符,相等和不等操作符;
b) 第二个迭代器为超出末端迭代器,第一个迭代器必须能够通过自增到达第二个迭代器;
4,泛型算法本身不执行容器操作:
a) 基于迭代器及其操作实现,不急于容器的操作;
b) 即算法从不修改基础容器的大小,即可能会改变存储在容器中的值,可能会移动元素,但是不会直接添加或者删除元素。
c) 如果需要实现更多的操作,可以使用插入器(也是一种迭代器),但是算法本身不这么做;
5,泛型算法需包含
二, 初窥算法
1,只读算法:例如find,accumulate,find_first_of等;
int sum = accumulate(vec.begin(), vec.end, 42); //第三个参数为起累加始值
a) accumulate算法对容器一无所知:
i. 必须传递一个起始值告知起始值类型;
ii. 容器内的元素类型必须和起始值类型匹配,或者可以转换;
b) 标记迭代器范围的两个实参类型必须精确匹配:
i. 第一个迭代器必须能够通过不断自增到达第二个迭代器;
ii. 如果算法带有两队迭代器参数,则每对迭代器中的实参类型必须精确匹配,但不要求两对之间的类型匹配,即元素可以存储在不同类型的序列中,只要两个元素的序列可以比较即可。
2,写容器元素的算法:小心!必须确保算法所写的序列至少足以存储要写入的元素。
a) 三种类型的写入算法:
i. 直接将数据写入输入序列;
ii. 带有额外的迭代器参数指定写入目标;
iii. 将指定数目的元素写入某序列:这种算法不检查目标的大小是否足以存储要写入的元素,容易造成灾难性后果!
b) 确保算法有足够的元素存储输出数据的一种方法是使用插入迭代器,
i. 插入迭代器是可以给基础容器添加元素的迭代器;
ii. 需要包含
iii. 是一种迭代器适配器,使用一个对象作为实参,并生成一个适应其实参行为的新对象;
iv. 如back_inserter生成一个绑定在该容器上的插入迭代器,在试图通过该迭代器给元素赋值时,赋值运算符将调用push_back添加一个具有指定值的元素。
c) 写入到目标迭代器的算法:
i. 该算法向目标迭代器写入未知个数的元素,如copy算法:
vector
copy( ilst.begin(), ilst.end(), back_inserter(ivec));
d) 各个算法的_copy版本:有的操作在对序列做操作后如果不想改变原序列,就会定义一个复制版本,将新生成的序列存储到目标序列中;如:replace和replace_copy。
replace( ilst.begin(), ilst.end(), 0, 42);
replace_copy( ilst.begin(), ilst.end(), back_inserter(ivec), 0, 42 );
3,对容器元素重新排序的算法
a) 通过一个例子说明:统计一段话中使用了多少个由6个或以上字母组成的单词,同一单词不重复计数。实现步骤如下:
i. 去掉所有重复的单词;
ii. 按单词的长度排序;
iii. 统计长度等于或者超过6个字符的单词数;
iv. 程序如下:
bool isShorter(const string & s1, const string & s2 ){
return s1.size() < s2.size(); //长度比较函数
}
bool GT6 (const string & s ){ return s.size() >= 6; } //判断长度函数
int main(){ //主函数
vector
string next_word;
while (cin >> next_word){ word.push_back(next_word); }
//对数组排序
sort ( words.begin(), words.end() );
//删除重复的元素
vector
unique(words.begin(), words.end() );
words.erase(end_unique, words.end() );
//
stable_sort( words.begin(), words.end(), isShorter ); //需要一个谓词
vector
count_if (words.begin(), words.end(), GT6 ); //需要一个谓词
cout<< … …
return 0;
}
b) unique的使用
i. “删除”相邻的重复元素,重新排列输入范围内的元素,返回一个迭代器,表示无重复的值范围的结束;
ii. 但此时words的大小并没有改变!只是顺序变了,除了被新排序的元素覆盖的元素外,剩下的元素还在;算法不能删除元素!
iii. 要用算法返回的迭代器配合容器操作erase来删除多余的元素;
c) 使用容器操作删除元素
d) 定义需要的谓词
i. 一些算法的某个重载版本使用时可能需要一个配套的实用函数,即谓词,如count_if和stable_sort;
ii. 传递给算法的谓词的性参数目是有要求的,如count_if的谓词只能接受1个形参,而stable_sort的谓词则需要接受2个形参;
三, 再谈迭代器
1,包含
2, 插入迭代器
a) 插入器是一种迭代器适配器,带有一个容器参数,并生成一个迭代器,用于在指定容器中插入元素;
b) back_inserter使用push_back,只有一个形参;
vector
copy( ilst.begin(), ilst.end(), back_inserter(ivec));
c) front_inserter使用push_front,只有一个形参;
i. 只有当容器提供push_front和push_back操作时,才能使用上述迭代器适配器,在vector上就不能使用push_f;
d) inserter将产生在指定位置时先插入的迭代器
i. 有两个实参:所关联的容器和只是其实插入位置的迭代器;
ii. 总是在它的迭代器实参所标明的位置前面插入新元素;
iii. 与front_inserter的区别:front_inserter始终在首元素之前插入,而即使给inserter传递了ivec.begin()作为迭代器实参,也不能实现front_inserter的操作,因为在插入第一个元素后,它就不再是首元素了;
即用front_inserter插入将导致元素反向排列,而用inserter则不会;
3, iostream迭代器
iostream迭代器的构造函数 |
|
istream_iterator |
创建从输入流strm中读取T类型对象的istream_iterator对象 |
istream_iterator |
istream_iterator的超出末端迭代器 |
ostream_iterator |
创建将T类型的对象写到输出流strm的ostream_iterator对象 |
ostream_iterator |
同上,在写入过程中将delim作为元素的间隔符。delim是以空字符结束的字符数组。 |
a) 流迭代器的定义和创建
i. 流迭代器都是类模板:任何已定义>>操作符操作符的类型都可以定义istream_iterator,任何已定义<<操作符的类型都可以定义ostream_iterator;
ii. 创建流迭代器时,必须指定迭代器所读写的对象类型;
iii. 创建ostream_iterator必须和特定的流绑定在一起;ostream_iterator不提供超出末端迭代器。
iv. 创建istream_iterator时,可以直接绑定到一个流上,也可以创建时不提供实参,则迭代器指向超出末端位置。
istream_iterator的操作 |
|
it1 == it2 |
比较是否相等,迭代器读取的必须是相同的类型。 |
it1 != it2 |
如果两个迭代器都是end值,则相等; 对于两个都不指向end的迭代器,如果他们使用同一个输入流构造,则他们也相等; |
*it |
返回流中读取的值 |
it->mem |
返回从流中读取的对象的mem成员 |
++it |
通过使用元素类型提供的>>操作符从输入流中读取下一个元素值,使迭代器向前移动; 前缀版本返回移动后对迭代器的引用,后缀版本返回原值。 |
it++ |
b) istream_iterator对象上的操作
i. 用法1:
istream_iterator
istream_iterator
while ( in_iter != eof) vec.push_back(*in_iter++);
eof迭代器定义为空的istream_iterator对象,用作结束迭代器。绑在流上的迭代器遇到文件结束或者某个错误时,将等于结束迭代器的值;
ii. 用法2:
istream_iterator
istream_iterator
vector
用一对标记元素范围的迭代器构造vec对象。效果是读cin,知道到达文件结束或者输入的不是int型数值为止,读的元素用于构造vec对象。
c) ostream_iterator对象和ostream_iterator对象的使用
i. 用法1:
ostream_iterator
istream_iterator
while ( in_iter != eof ) *out_iter++ = *in_iter++;
作用:读cin,并将读入的值写到cout中不同的行中;
d) 再累类型上使用istream_iterator
i. 提供了>>操作符的任何类型都可以创建istream_iterator对象。
ii. 例子如下:
istream_iterator
Sales_item sum;
sum = *item_iter++;
while (item_iter) {
if (item_iter->same_isbn(sum)) sum = sum + *item_iter;
else { cout << sum << endl; sum = *item_iter; }
++item_iter;
}
cout << sum << endl;
e) 迭代器的限制:
i. 不能从ostream_iterator中读入,也不能写到istream_iterator对象中;
ii. 一段给ostream_iterator赋值,就不能修改了,每个不同值都只能正好输出一次;
iii. ostream_iterator没有->操作符。
f) 与算法一起使用流迭代器
i. 例子1:从输入读取一些数,再不重复的输出到标准输出
istream_iterator
istream_iterator
vector
sort ( vec.begin(), vec.end() );
ostream_iterator
unique_copy ( vec.begin(), vec.end(), output );
4, 反向迭代器reverse_iterator
a) 反向迭代器是一种反向遍历容器的迭代器:
i. ++运算符将访问前一个元素,反之亦然;
ii. 容器定了begin和end成员,也定义了rbegin和rend成员,后者就是反向迭代器;
b) 例子1:逆序输出元素:
vector
for (r_iter = vec.rbegin(); r_iter != vec.rend(); ++r_iter )
cout << *r_iter << endl;
c) 例子2:将序排列vector,只需向sort传递一对反向迭代器:
sort( vec.rbegin(), vec.rend() );
d) 反向迭代器需要使用自减操作符,所以流迭代器不能创建反向迭代器,因为流不能被反向遍历;
e) 反向迭代器与其他迭代器之间的关系:
如图显示的对象直观的结束了普通迭代器与反向迭代器之间的关系:
i. 设计普通迭代器与反向迭代器之间的关系是为了适应左闭合范围,以使[ line.rbegin(), rcomma )和( rcomma.base(), line.end() ]标记的是line中的相同元素。
ii. 程序如下(假设有一个名为line的string对象,存储以逗号分隔的单词列表,我们希望输出line中的第一个单词):
string::iterator comma = find( line.begin(), line.end(), ‘,’ );
cout << string(line.begin(), comma) << endl;
//输出:FIRST
string::reverse_iterator rcomma = find( line.rbegin(), line.rend(), ‘,’ );
cout << string( line.rbegin(), rcomma ) << endl;
//输出:TSAL 这是错误的!原因从上图可以看出,正确的方法如下:
cout << string( rcomma.base(), line.end() ) << endl;
5, const迭代器
a) 如果我们不希望使用迭代器来修改容器中的元素,那么可以定义为const_iterator类型;
b) 但是,用于算法中表示范围的两个迭代器必须具有完全一样的类型,所以,如果另一个迭代器不是const类型,那么即使不希望改变元素,也不能定义成const类型;
c) 类似lst.begin(),lis.end()返回的迭代器的类型依赖于所指向的容器存放的元素的类型,存放的是const对象,则返回const_iterator类型的迭代器,反之亦然。
6, 五种迭代器
迭代器种类(按算法要求的迭代器操作分类) |
|
输入迭代器 |
读,不能写;只支持自增运算 |
输出迭代器 |
写,不能读;只支持自增运算 |
前向迭代器 |
读和写;只支持自增运算 |
双向迭代器 |
读和写;支持自增和自减运算 |
随机访问迭代器 |
读和写;支持完整的迭代器算术运算 |
a) 输入迭代器
i. 可读,不保证能写;
ii. 支持==和!=操作符,自增操作符(++),解引用(*)操作符,箭头操作符(->)(对迭代器进行解应用来获取所关联的对象的成员)。
iii. 一旦自增了,就无法检查之前的元素;
iv. istream_iterator即为此类型,find和accumulate算法;
b) 输出迭代器
i. 可写,不保证可读;
ii. 支持++操作符;
iii. 支持解引用(*),此操作符只能出现在赋值运算的左操作数上,用来对该迭代器所指向的元素做写入操作;
iv. 可以要求每个迭代器的值必须正好写入一次;??
v. 输出迭代器一般用作算法的第三个实参,用于标注开始写入的位置;
vi. copy算法;ostream_iterator为输出迭代器。
c) 前向迭代器
i. 只能以一个方向遍历序列;
ii. 支持输入迭代器和输出迭代器的所有操作,支持对同一个元素的多次读写;
iii. 需要复制向前迭代器来记录序列中的一个位置,以便返回此处;
iv. replace算法。
d) 双向迭代器
i. 从两个方向读写容器;
ii. 支持前向迭代器的所有操作,还有前置和后置的--操作;
iii. 所有标准库容器提供的迭代器都至少达到双向迭代器的要求;
iv. reverse算法;
v. map,set,list提供双向迭代器。
e) 随机访问迭代器
i. 访问容器任意位置;
ii. 支持关系操作符,与整数值之间的算术运算操作符,两个迭代器之间的减法操作符(返回距离),下标操作符(iter[n]相当于*(iter+n));
iii. vector,deque,string和内置数组元素的指针都是随机访问迭代器;
iv. sort算法。
f) 除了输出迭代器:在需要低级类别迭代器的地方,可以使用任意一种更高级的迭代器。
注:向算法传递无效的迭代器类别所引起的错误,无法保证会在编译时被捕捉到!
四, 泛型算法的结构
1,算法的形参模式:算法最基本的性质是需要使用的迭代器总类。
a) alg( beg, end, other parms );
b) alg( beg, end, dest, other parms );
i. 算法假定无论需要写入多少元素都是安全的;
ii. 调用算法是,必须确保输出容器有足够大的容量存储输出数据,这正是通常要使用插入迭代器或者ostream_iterator来调用这些算法的原因。
c) alg( beg, end, beg2, otherparms );
i. 算法假定以beg2开始的范围至少与beg和end指定的范围一样大。
d) alg( beg, end, beg2, end2, otherparms );
2,算法的命名规范:算法使用一组相同的命名和重载函数的规范。
a) 两种基本模式:测试输入范围内元素的算法,对输入范围内元素重新排序的算法。
b) 测试输入的元素的算法:区别带有一个值或一个为此函数参数的算法版本
i. 通常要用到标准关系操作符:==和<;
ii. 其中大部分算法提供第二个版本,允许程序员提供谓词取代操作符;
iii. 如果算法的两种版本性参数不同,则通过重载函数实现第二个版本:
sort (beg, end ); //
sort ( beg, end, comp ); //通过重载实现,comp为谓词
iv. 如果算法的两种版本形参数相同,则通过增加名字后缀_if来区别:
find ( beg, end, val );
find_if ( beg, end, pred ); //通过名字后缀来区分,pred为谓词
c) 对输入元素重新排序的算法:区别是否实现复制的算法版本
i. 无论算法是否检查它的元素值,都可能重新排列输入范围内的元素;
ii. 默认情况下,算法将重新排列后的元素写回其输入的范围;
iii. 如果要将排列后的元素写到输出目标,则用名字后缀_copy加以区分:
reverse ( beg, end );
reverse_copy ( beg, end, dest);
五, 容器特有的算法
list容器的特有操作 |
|
lst.merge (lst2) |
将lst2的元素合并到lst中; 两个list对象都必须排序; 合并后来lst2为空,返回void类型。 |
lst.merge (lst2, comp) |
|
lst.remove ( val ) |
调用lst.erase删除所有等于指定值或使指定的为此函数返回非零值的元素; 返回void类型。 |
lst.remove_if ( unaryPred ) |
|
lst.reverse () |
反向排列 |
lst.sort () |
排序 |
lst.splice ( iter, lst2 ) |
将lst2中的元素一道lst中iter指向的元素前,并删除lst2中移除的元素; 第一个版本:移动所有元素,lst2和lst不能是同一个list对象; 第二个版本:移动iter2指向的元素,必须是lst2中的元素,lst2和lst可以是同一个list对象; 第三个版本:移动范围内的元素,必须是一个有效的范围,可以是lst中的元素,但iter指向的元素不能再该范围内; |
lst.splice ( iter, lst2, iter2 ) |
|
lst.splice ( iter, beg, end ) |
|
lst.unique () |
调用erase删除同一个值的连续副本 |
lst.unique ( binaryPred ) |