泛型算法(generic algorithm)
前言:在网上找了一堆C++泛型算法的资料,总感觉看起来很没有条理,于是乎搬出书架上的<C++ Primer 5th>.
重温并且全面的总结一下知识点. 来自<C++ Primer>第十章 泛型算法。
称它们为"算法", 是因为它们实现了一些经典算法的公共接口, 如排序和搜索;
称它们为"泛型的", 是因为它们可以用于不用类型的元素和多种容器类型(不仅包括标准库类型, 如vector或list, 还包括内置的数组类型),以及我们将看到的,还能用于其他类型的序列.
一、概述
大多数算法都是定义在头文件algorithm, 标准库还在头文件numeric中定义了一组数值泛型算法.
一般情况下,算法并不直接操作容器,而是通过两个迭代器遍历一个元素范围.
1.例如, 我们有一个int的vector,希望知道vector中是否包含一个特定值.最方便的方法就是调用标准库算法find:
[代码实现]
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main()
{
vector<int> vec(10,0); /*假设初始化为10个0*/
int val=42;
/*如果在vec中找到想要的元素,则返回结果指向它,否则返回结果为vec.cend()*/
auto result = find(vec.cbegin(),vec.cend(),val);
cout<<"The value "<<val<<( result == vec.cend()? "is not present":"is prensent")<<endl;
return 0;
}
其中find(beg,end,val),前两个参数表示元素范围的迭代器,第三个参数是一个值,注意题中的cbegin为const_iterator,即只读不写.find将范围中的每个元素与给定值比较,返回指向第一个等于给定值的元素的迭代器,否则返回第二个参数表示搜索失败,即返回vec.cend().
另一个注意的是auto,自动类型推断,详情百度;
string val="a value";
auto result=find(val.cbegin(),val.cend(),val);
3.类似的,由于指针就向内置数组上的迭代器一样,我们可以用find在数组中查找值:
int ia[]={27,210,12,47,109,83};
int valu=83;
int *result=find(begin(ia),end(ia),val);
4.还可以在序列的子范围中查找,只需将指向子范围首元素和尾元素之后位置的迭代器(指针)传递给find.例如,下面的语句在ia[1]、ia[2]、ia[3]中查找给定元素:
auto result=find(ia+1,ia+4,val);
接下来再来看一下find是如何工作的:
1.访问序列中的首元素.
2.比较此元素与我们要查找的值.
3.如果此元素与我们要查找的值匹配,find返回标识此元素的值.
4.否则,find前进到下一个元素,重复步骤2和3.
5.如果到达序尾,find应该停止.
6.如果find到达序列末尾,它应该返回一个指出元素未找到的值.此值和步骤3返回的值必须具有相容的类型迭代器令算法不依赖于容器,但算法依赖于元素类型的操作,算法永远不会改变底层容器的大小.算法可能改变容器中保存的元素的值,也可能在容器内移动元素,但永远不会直接添加或删除元素.
二、初始泛型算法
标准库提供了超过100个算法,幸运的是,与容器类似,这些算法有一致的结构,除了少数例外,标准库算法都对一个范围内的元素进行操作.理解算法的最基本的方法就是了解他们是否读取元素、改变元素或是重排元素顺序.
1.只读算法
一些算法只会读取其输入范围内的元素,而从不改变元素.find就是这样一种算法,count函数(返回给定值在序列中出现的次数)也是如此.
1)另一个只读算法是accumulate,他定义在头文件numeric中.accumulate函数接受三个参数,第三个参数是和的初值.假定vec是一个整数序列,
则:
/*accumulate的第三个参数的类型决定了函数中使用哪个加法运算符和返回类型*/
int sum = accumulate(vec.cbeing(),vec.cend(),0);
这条语句将sum设置为vec中元素的和,和的初值被设置为0;2)算法和元素类型
另一个例子,由于string定义了+运算符,所以我们可以通过调用accumulate来将vector中所有string元素连接起来:
/*注意第三个参数显示地创建一个string, 若直接将空串当一个字面值传递是会导致编译错误的*/
string sum = accumulate(v.cbegin(),v.cend(),string(""));
3)操作两个序列的算法
另一个只读算法equal(beg1,end1,beg2),用于确定两个序列是否保存相同的值.前两个参数表示一个序列中的元素范
围,第三个参数表示第二个序列的首元素:
equal(roster1.cbegin(),roster1.cend(),roster2.cbegin()); /*roster2中的元素至少与roster1一样多*/
那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长.
2.写容器元素的算法
一些算法将新值赋予序列中的元素.当我们使用这类算法时,必须注意确保序列原大小至少不小于我们要求算法写入的元素数目.
1)算法fill(beg,end,val)接受一对迭代器表示一个范围,还接受一个值作为第三个参数,fill将给定的这个值赋予输入序列中的每个元素:fill(vec.begin(),vec.end(),0); /*将每个元素置为0*/
fill(vec.begin(),vec.begin() + vec.size()/2 , 10);/*将容器的一个子序列设置为10*/
2)算法不检查写操作
算法fill_n(dest, n, val)接受一个单迭代器、一个计数值和一个值:
vector<int> vec; /*空vector*/
fill_n(vec.begin(),vec.size(),0); /*将所有元素都重置为0*/
fill_n(dest,n,val); /*函数fill_n假定dest指向一个元素,而从dest开始的序列至少包含n个元素*/
所有需要注意一点:
vector<int> vec; /*空向量*/
fill_n(vec.begin(),10,0); /*这条语句的结果是未定义的,因为vec中并没有元素*/
3)介绍back_inserter
一种保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器(insert iterator).back_inserter定义在头文件iterator中的一个函数 ,back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器.当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中:
vector<int> vec;/*空向量*/auto it=back_inserter(vec); /*通过对(*it)的赋值会将元素添加到vec中*/
*it=42; /*vec中现在有一个元素,值为42*/
我们常常使用back_inserter来创建一个迭代器,作为算法的目的位置来使用.
例如:
vector<int> vec; /*空向量*/
fill_n(back_inserter(vec),10,0); /*添加10个元素到vec,每个元素的值都是0*/
4)拷贝算法
拷贝(copy)算法是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法.copy(beg1,end1,beg2)前两个表示一个输入范围,第三个表示目的序列的起始位置.此算法将输入范围的元素拷贝到目的序列中,传递给copy的目的序列至少要包含于输入序列一样多的元素.
我们可以用copy实现内置数组的拷贝,如下面代码所示:
int a1[]={0,1,2,3,4,5,6,7,8,9};
int a2[sizeof(a1)/sizeof(*a1)]; /*a2与a1大小一样*/
auto ret = copy(begin(a1),end(a1),a2);/*ret指向拷贝到a2的尾元素之后的位置*/
copy返回的是其目的位置迭代器的值.即,ret恰好指向拷贝到a2的尾元素之后的位置
例如,replace(beg,end,old,new)算法读入一个序列,并将其中所有等于给定值的元素都改为另一个值,此算法接受4个参数:前两个是迭代器,表示输入序列,后两个一个是要搜索的值,另一个是新值.它将所有等于第一个值的元素替换为第二个值:
replace(ilst.begin(), ilst.end(), 0, 42); /*将所有值为0的元素都改为42*/
如果我们希望保留原序列,可以调用replce_copy(beg, end, result, old, new)此算法接受额外第三个迭代器参数,指出调整后序列的保存位置:
replace_copy(ilst.cbegin(), ilst.cend(),back_inserter(ivec), 0, 42);/*使用back_inserter按需要增长目标序列*/
某些算法会重排容器中的元素的顺序,一个明显的例子就是sort(beg, end).它是利用元素类型的<运算符来实现排序的.
例如,假定我们想分析一系列儿童故事中所用的词汇,假定已有一个vector,保存了多个故事的文本.我们希望简化这个vector,使得每个单词只出现一次,而不管单词在任意给定文档中到底出现了多少次.
为了方便说明问题,我们将使用下面简单的故事作为输入:
the quick red fox jumps over the slow red turtle
给定输入,我们的程序应该生成如下vector:
fox jumps over quick red slow the turtle1)消除重复单词
为了消除重复单词,首先将vector排序,使得重复的单词相邻出现,一旦vector排序完毕,我们就可以使用另一个称为unique(beg,end)的标准库算法来重排vector,
使得不重复的元素出现在vector的开始部分.由于算法不能执行容器的操作,我们将使用vector的erase成员来完成真正的删除操作:
void elimDups(vector<string> &words)
{
sort(words.begin(),words.end());/*按字典序排序words,以便查找重复单词*/
/*排列在范围前部的是不重复的区域,返回指向不重复区域之后一个位置的迭代器*/
auto end_unique = unique(words.begin(),words.end());
words.erase(end_unique,words.end());/*使用向量操作erase删除重复区域的单词*/
}
其中sort算法接受两个迭代器,表示要排序的元素范围.在此例中,sort后,word的排序如下所示:
fox jumps over quick red red show the the turtle
注意,单词 red 和 the 各出现了两次.
2)使用unique
words排序完毕后,我们希望将每个单词只保存一次.unique算法重排输入序列,将相邻的重复项"清除"(注意并没有删除),并返回一个指向不重复值范围的
迭代器.调用unique后,vector将变为:
fox jumps over quick red slow the turtle ??? ???
end_unique/*最后一个不重复元素之后的位置*/
words的大小并未改变,它仍有10个元素,但顺序改变了.
3)使用容器操作删除元素
为了真正地删除无用元素,我们必须使用容器操作,本例中使用了erase,删除了从end_unique开始直至words末尾的范围内的所有元素.
值得注意的是,即使words中没有重复单词,这样调用erase也是安全的.因为unique会返回words.end(),传递给erase两个相同的参数,删除一个空范围没有什么
不良后果.
Tip:find(),count(),accumulate(),equal(),fill(),fill_n(),back_inserter(),copy(),replace_copy(),sort(),unique()
这些的具体实现可以在 http://www.cplusplus.com/reference/algorithm/这里找到.
关于定制操作、再探迭代器、泛型算法结构、特定容器算法将在后篇介绍