内容简介:本文主要讨论C++标准库中的泛型算法(generic algorithm)。泛型算法是使用容器的强有力的辅助工具。
本文主要讨论C++标准库中的泛型算法(generic algorithm)。泛型算法是使用容器的强有力的辅助工具。
如果文中有错误或遗漏之处,敬请指出,谢谢!
标准库为容器类型定义的操作很少,并没有为每个容器实现更多的操作。因为这部分操作可以抽象出来为所有的容器工作,那就是泛型算法。所谓“泛型”是指这些算法可以应用于多种容器类型上,而容器内的元素类型也可以多样化。标准库提供了100多个泛型算法,主要定义于头文件<algorithm>中,还有一组泛化的算术算法定义于头文件<numeric>中。
大多数泛型算法是工作于容器的一对迭代器所标识的范围,并完全通过迭代器来实现其功能。这段由迭代器指定的范围称为“输入范围”。带有输入范围参数的算法总是使用前两个参数标记该范围,分别指向要处理的第一个元素和最后一个元素的下一个位置。
这些算法一般可划分为只读算法、改写元素算法或对元素重新 排序 算法,下面分别叙述之。
只读算法
find 算法
1 2 |
template<class InIt, class T> InIt find(InIt first, InIt last, const T& val); |
查询迭代器指定范围[first, last)范围内是否有val值。如果有,则返回该值对应的迭代器;否则,返回last表示查找失败。
accumulate 算法
1 2 3 4 |
template<class InIt, class T> T accumulate(InIt first, InIt last, T val); template<class InIt, class T, class Pred> T accumulate(InIt first, InIt last, T val, Pred pr); |
累加迭代器指定范围[first, last)范围内所有元素,再加上累加的初值val,返回累加的结果。第二个函数自定义操作:val = pr(val, *it)。
注:用于指定累加起始值的第三个参数是必要的,因为算法对将要累加的元素类型一无所知,没有别的办法创建合适的起始值或者关联的类型。
find_first_of 算法
1 2 3 4 |
template<class FwdIt1, class FwdIt2> FwdIt1 find_first_of(FwdIt1 first1, FwdIt1 last1, FwdIt2 first2, FwdIt2 last2); template<class FwdIt1, class FwdIt2, class Pred> FwdIt1 find_first_of(FwdIt1 first1, FwdIt1 last1, FwdIt2 first2, FwdIt2 last2, Pred pr); |
查询第一段范围内与第二段范围内任意元素匹配的元素的位置。如果找到,返回该元素对应的迭代器;否则,返回last1。第二个函数使用判断:pr(*it1, *it2)来代替第一个函数中的判断:*it1 == *it2。
写容器元素的算法
在使用写元素的算法时,必须确保算法所写的序列至少足以存储要写入的元素。有些算法直接将数据写入到输入序列,另外一些则带有一个额外的迭代器参数指定写入目标。这类算法将目标迭代器用作输出的位置。还有第三种算法将指定数目的元素写入某个序列。
写入输入序列的元素
写入到输入序列的算法本质上是案例的,因为只会写入与指定输入范围数量相同的元素。如fill算法:
1 2 |
template<class FwdIt, class T> void fill(FwdIt first, FwdIt last, const T& x); |
这个算法将指定范围内的每个元素都设定为给定的值。如果输入范围有效,则可以安全写入。这个算法只会对输入范围内已存在的元素进行写入操作。
不检查写入操作的算法
这类算法如fill_n算法:
1 2 |
template<class OutIt, class Size, class T> void fill_n(OutIt first, Size n, const T& x); |
该算法从迭代器指向的元素开始,将指定数量的元素设置为给定的值。如果目标范围内的某些元素不存在,则该操作未定义。如下面的代码将发生不可预料的结果:
1 2 |
vector<int> vec; // empty vector fill_n(vec.begin(), 10, 0); // disaster behavior |
注:对指定数目的元素做写入运算,或者写到目标迭代的算法,都不检查目标的大小是否足以存储要写入的元素。
back_inserter
确保算法有足够的元素存储输出数据的一种方法是使用插入迭代器(insert iterator)。插入迭代器是可以给基础容器添加元素的迭代器。通常,用迭代器给容器元素赋值时,被赋值的是迭代器所指向的元素。而使用插入迭代器赋值时,则会在容器中添加一个新元素,其值等于赋值运算的右操作数的值。
back_inserter函数是迭代器适配器,其使用一个对象作为实参,并生成一个适应其实参行为的新对象。比如,在下例中,传递给back_inserter的实参是一个容器的引用。back_inserter生成一个绑定在该容器上的插入迭代器。在试图通过这个迭代器给元素赋值时,赋值运算将调用push_back在容器中添加一个具有指定值的元素。因此,用back_inserter改写上面的代码可以有效地工作:
1 2 |
vector<int> vec; // empty vector fill_n(back_inserter(vec), 10, 0); // ok: appends 10 elements to vec |
写入到目标迭代器的算法
第三类算法向目标迭代器写入未知个数的元素。这类算法最简单的如copy算法:
1 2 |
template<class InIt, class OutIt> OutIt copy(InIt first, InIt last, OutIt x); |
copy算法带有三个迭代器参数:前两个指定输入范围,第三个指向目标序列的第一个元素。
算法的_copy版本
有些算法提供所谓的“_copy”版本。这些算法对输入序列的元素做处理,但不修改原来的元素,而是创建一个新序列存储元素的处理结果
例如,replace算法:
1 2 |
template<caass FwdIt, class T> void replace(FwdIt first, FwdIt last, const T& vold, const T& vnew); |
该算法指定范围[first, last)内的所有元素值为vold替换为vnew。
如果不想改变原序列,可以用replace_copy算法:
1 2 |
template<class InIt, class OutIt, class T> OutIt replace_copy(InIt first, InIt last, OutIt x, const T& vold, const T& vnew); |
这个算法接受第三个迭代器参数,指定保存替换后的序列的目标位置。例如:
1 2 |
vector<int> vec; replace(ilist.begin(), ilist.end(), back_inserter(ivec), 1, 10); |
调用该函数后,ilist没有改变,而ivec存储ilist的一份替换后的副本。
对容器元素重新排序的算法
sort算法
这里只介绍sort和stable_sort这个类排序算法:
1 2 3 4 5 6 7 8 |
template<class RanIt> void sort(RanIt first, RanIt last); template<class RanIt, class Pred> void sort(RanIt first, RanIt last, Pred pr); template<class RanIt> void stable_sort(RanIt first, RanIt last); template<class RanIt, class Pred> void stable_sort(RanIt first, RanIt last, Pred pr); |
sort排序算法是最一般的类型,而stable_sort排序算法是稳定排序。
unique和unique_copy
unique函数“删除”指定范围内的重复元素。注意:这里的“删除”不是真正意义上的删除,只是在有重复元素时,把后面的元素向前移动覆盖了原来的元素。函数返回的迭代器指向无重复元素序列最后一个元素的下一个位置。而unique_copy是它的“_copy”版本,返回的是生成的序列的最后一个元素的下一个位置。
1 2 3 4 5 6 7 8 |
template<class FwdIt> FwdIt unique(FwdIt first, FwdIt last); template<class FwdIt, class Pred> FwdIt unique(FwdIt first, FwdIt last, Pred pr); template<class InIt, class OutIt> OutIt unique_copy(InIt first, InIt last, OutIt x); template<class InIt, class OutIt, class Pred> OutIt unique_copy(InIt first, InIt last, OutIt x, Pred pr); |
注意:unique调用后,原序列的前面部分是无重复元素的序列,而后半部分是剩下没有被覆盖的序列。这里,需要手动删除后面的元素序列,范围由返回的迭代器和容器末端决定。
迭代器
插入迭代器
插入迭代器是一种迭代器适配器,带有一个容器参数,并生成一个迭代器,用于在指定的容器中插入元素。通过插入迭代器赋值时,迭代器将会插入一个新的元素。C++语言提供了三种插入器,其差别在于插入元素的位置不同:
1)back_inserter,创建使用push_back实现插入的迭代器;
2)front_inserter,使用push_front实现插入;
3)inserter,使用insert实现插入操作。除了所关联的容器外,inserter还带有第二个实参:指向插入起始位置的迭代器。
front_inserter的操作类似于back_inserter:该函数将创建一个迭代器,调用它所关联的基础容器的push_front成员函数代替赋值操作。注意:只有当容器提供push_front操作时,才能使用front_inserter。在vector或其他没有push_front运算的容器上使用front_inserter,将产生错误。
inserter将产生在指定位置实现插入的迭代器,inserter总是在它的迭代器参数所标明的位置前面插入新元素。看看下面的例子:
1 2 3 4 5 6 7 8 |
list<int> ilst, ilst2, ilst3; //empty lists // after this loop ilst contains: 1 2 3 4 for (list<int>::value_type i = 0; i != 4; ++i) ilst.push_front(i + 1); // after copy ilst2 contains: 4 3 2 1 copy (ilst.begin(), ilst.end(), front_inserter(ilst2)); // after copy ilst3 contains: 1 2 3 4 copy (ilst.begin(), ilst.end(), inserter(ilst3, ilst3.begin())); |
iostream 迭代器
虽然iostream类型不是容器,但标准库同样提供了在iostream对象上使用的迭代器:istream_iterator用于读取读入流,而ostream_iterator用于写输出流。这些迭代器将它们所对应的流视为特定类型的元素序列。使用流迭代器时,可以用泛型算法从流对象中读数据(或将数据写到流对象中)。
流迭代器只定义了最基本的迭代器操作:自增、解引用和赋值。此外,可比较两个istream迭代器是否相等(或不等)。而ostream迭代器则不提供比较运算。
使迭代器向前移动。通常,前缀版本使迭代器在流中向前移动,并返回对加1后的迭代器的引用。it++ 而后缀版本使迭代器在流中向前移动后,返回原值。
流迭代器是类模板:任何已定义输入操作符(>>操作符)的类型都可以定义istream_iterator。类似地,任何已定义输出操作符(<<操作符)的类型也可以ostream_iterator。
istream_iterator使用举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> #include <vector> #include <iterator> using namespace std; int main() { istream_iterator<int> in_iter(cin); istream_iterator<int> eof; //vector<int> vec(in_iter, eof); //do the same work as following loop vector<int> vec; while (in_iter != eof) vec.push_back(*in_iter++); vector<int>::const_iterator it = vec.begin(); for(; it != vec.end(); ++it) cout<<*it<<endl; return 0; } |
ostream_iterator使用举例:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <iostream> #include <iterator> using namespace std; int main() { ostream_iterator<string> out_iter(cout, "/n"); istream_iterator<string> in_iter(cin), eof; while (in_iter != eof) *out_iter++ = *in_iter++; return 0; } |
流迭代器的限制:
1)不可能从ostream_iterator对象读入,也不可能写到istream_iterator对象中;
2)一旦给ostream_iterator对象赋了一个值,写入就提交了。赋值后,没有办法再改变这个值。此外,ostream_iterator对象中每个不同的值都只能正好输出一次。
3)ostream_iterator没有->操作符。
与算法一起使用流迭代器,如下面的示例实现从标准输入读取一些数,然后将不重复的数写到标准输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <iostream> #include <vector> #include <iterator> #include <algorithm> using namespace std; int main() { istream_iterator<int> in_it(cin), eof; vector<int> vec(in_it, eof); sort(vec.begin(), vec.end()); ostream_iterator<int> out_it(cout, " "); unique_copy(vec.begin(), vec.end(), out_it); return 0; } |
反向迭代器
反向迭代器是一种反向遍历容器的迭代器。也就是,从最后一个元素到第一个元素遍历容器。反向迭代器将自增(和自减)的含义反过来了:对于反向迭代器,++运算将访问前一个元素,而–运算则访问下一个元素。
1)反向迭代器需要使用自减操作符:标准容器上的迭代器(reverse_iterator)既支持自增运算,也支持自减运算。但是,流迭代器由于不能反向遍历流,因此流迭代器不能创建反向迭代器。
2)可以通过reverse_iterator::base()将反向迭代器转换为普通迭代器使用,从逆序得到普通次序。如下面的例子所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> #include <string> #include <iterator> #include <algorithm> using namespace std; int main() { string str = "this 'sentence' is a test"; cout<<"String: "<<str<<endl; string::iterator it1 = find(str.begin(), str.end(), '/''); string::iterator it2 = find(++it1, str.end(), '/''); // output: sentence cout<<"B-E: "<<string(it1, it2)<<endl; string::reverse_iterator rit1 = find(str.rbegin(), str.rend(), '/''); string::reverse_iterator rit2 = find(++rit1, str.rend(), '/''); // output: ecnetnes cout<<"R-B-E 1: "<<string(rit1, rit2)<<endl; // output: sentence cout<<"R-B-E 2: "<<string(rit2.base(), rit1.base())<<endl; return 0; } |
const 迭代器
在标准库中,有输入范围的泛型算法要求其两个迭代器类型完全一样,包括const属性。要么都是const,要么都是非const,否则无法通过编译。同样,它们的返回值迭代器也与参数类型保持一致。
迭代器分类
不同的迭代器支持不同的操作集,而各种算法也要求相应的迭代器具有最小的操作集。因此,可以将算法的迭代器分为下面五类:
除了输出迭代器,其他类别的迭代器形成了一个层次结构:需要低级类别迭代器的地方,可使用任意一种更高级的迭代器。例如,对于需要输入迭代器的算法,可传递前向、双向或随机访问迭代器调用该算法。而反之则不行。注意:向算法传递无效的迭代器类别所引起的错误,无法保证会在编译时被捕获到。
map, set, list类型提供双向迭代器,而string, vector和deque容器上定义的迭代器都是随机访问迭代器,用作访问内置数组元素的指针也是随机访问迭代器。istream_iterator是输入迭代器,ostream_iterator是输出迭代器。
另外,虽然map和set类型提供双向迭代器,但关联容器只能使用这部分算法的一个子集。因为关联容器的键是const对象。因此,关联容器不能使用任何写序列元素的算法。只能使用与关联容器绑在一起的迭代器来提供用于读操作的实参。因此,在处理算法时,最好将关联容器上的迭代器视为支持自减运算的输入迭代器,而不是完整的双向迭代器。
泛型算法的结构
就像所有的容器都建立在一致的 设计模式 上一样,算法也具有共同的设计基础。
算法最基本的性质是需要使用的迭代器种类。
另一种算法分类方法是前面介绍的按实现的功能分类:只读算法,不改变元素的值和顺序;给指定元素赋新值的算法;将一个元素的值移给另一个元素的算法。
另外,算法还有两种结构上的算法模式:一种模式是由算法所带的形参定义;另一种模式则通过两种函数命名和重载的规范定义。
算法的形参模式
大多数算法采用下面四种形式之一:
1 2 3 4 |
alg (beg, end, other parms); alg (beg, end, dest, other parms); alg (beg, end, beg2, other parms); alg (beg, end, beg2, end2, other parms); |
其中,alg是算法名,[beg, end)是输入范围,beg, end, dest, beg2, end2都是迭代器。
对于带有单个目标迭代器的算法:dest形参是一个迭代器,用于指定存储输出数据的目标对象。算法假定无论需要写入多少个元素都是安全的。注意:调用这类算法时,算法是将输出内容写到容器中已存在的元素上,所以必须确保输出容器中有足够大的容量存储输出数据,这也正是通过使用插入迭代器或者ostream_iterator来调用这些算法的原因。
对于带第二个输入序列的算法:beg2和end2标记了完整的输出范围。而只有beg2的算法将beg2视为第二个输入范围的首元素,算法假定以beg2开始的范围至少与beg和end指定的范围一样大。
算法的命名规范
包括两种重要模式:第一种模式包括测试输入范围内元素的算法,第二种模式则应用于输入范围内元素的重新排序的算法。
1)区别带有一个值或一个谓词函数参数的算法版本
很多算法通过检查其输入范围内的元素实现其功能。这些算法通常要用到标准关系操作符:== 或 < 。其中的大部分算法都提供了第二个版本的算法,允许 程序员 提供比较或测试函数取代默认的操作符的使用。
例如, 排序算法默认使用 < 操作符,其重载版本带有一个额外的形参,表示取代默认的 < 操作符。
1 2 |
sort (beg, end); // use < operator to sort the elements sort (beg, end, comp); // use function named comp to sort the elements |
又如,查找算法默认使用 == 操作符。标准库为这类算法提供另外命名的(而非重载的)版本,带有谓词函数形参。对于带有谓词函数形参的算法,其名字带有后缀 _if:
1 2 |
find (beg, end, val); // find first instance of val in the input range find_if (beg, end, pred); // find first instance for which pred is true |
标准库为这类算法提供另外命名的版本,而非重载版本,原因在于这两种版本的算法带有相同的参数个数,容易导致二义性。
2)区别是否实现复制的算法版本
默认情况下,算法将重新排列的写回其范围。标准库也为这类算法提供了另外命名的版本,将元素写到指定的输出目标。此版本的算法在名字中添加 _copy后缀,例如:
1 2 |
reverse (beg, end); reverse_copy (beg, end, dest); |
第一个版本将输入序列中的元素反向重新排列;而第二个版本将复制输入序列中的元素,并将它们以逆序存储到dest开始的序列中。
容器特有的算法
list容器上的迭代器是双向的,而不是随机访问类型。由于list容器不支持随机访问,因此,在此容器上不能使用需要随机访问迭代器的算法。如sort类算法。其它有些算法,如merge, remove, reverse, unique等,虽然可以用在list上,但性能太差。list容器结合自己的结构专门实现了更为高效的算法。因此,对于list对象,应该优先使用list容器特有的成员版本,而不是泛型算法。
下表列出了list容器特有的操作:
list容器特有的算法与其泛型算法版本之间有两个重要的差别:1)remove和unique的list版本修改了其关联的基础容器:真正删除了指定的元素;2)list容器提供的merge和splice操作会破坏它们的实参。使用泛型算法的merge版本,合并的序列将写入目标迭代器指向的对象,而它的两个输入序列保持不变。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 通俗易懂--决策树算法、随机森林算法讲解(算法+案例)
- 限流算法之漏桶算法、令牌桶算法
- 什么是Paxos算法?Paxos算法是区块链核心算法之一
- 一文读懂对称加密算法、非对称加密算法和Hash算法
- 算法(六):图解贪婪算法
- 算法篇03:排序算法
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
深入浅出Struts 2
Budi Kuniawan / 杨涛、王建桥、杨晓云 / 人民邮电出版社 / 2009-04 / 59.00元
本书是广受赞誉的Struts 2优秀教程,它全面而深入地阐述了Struts 2的各个特性,并指导开发人员如何根据遇到的问题对症下药,选择使用最合适的特性。作者处处从实战出发,在丰富的示例中直观地探讨了许多实用的技术,如数据类型转换、文件上传和下载、提高Struts 2应用的安全性、调试与性能分析、FreeMarker、Velocity、Ajax,等等。跟随作者一道深入Struts 2,聆听大量来之......一起来看看 《深入浅出Struts 2》 这本书的介绍吧!