后台开发工程师技术能力体系之编程语言6——泛型算法

泛型算法

1.概述

  标准库并未给每个容器添加大量功能,而是提供了一组算法,这些算法中的大多数都独立于任何特定的容器,这些算法是通用的(或泛型的),它们可用于不同类型的容器和不同类型的元素。大多数算法都定义在头文件algorithm中,标准库还在头文件numeric中定义了一组数值泛型算法。一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。通常情况下,算法遍历范围,对其中每个元素进行一些处理。
  例如我们需要查找某个元素是否在容器中,可以调用标准库算法find,find的前两个参数是表示元素范围的迭代器第三个参数是一个值,find将范围中的每个元素与给定值进行比较,并返回第一个等于给定值的元素的迭代器,如果范围中无匹配元素,则find返回第二个参数来表示搜索失败。由于find操作的是迭代器,因此我们可以用同样的find函数在任何容器中查找值。

string val = "a value"; //要查找的值
auto result = find(lst.cbegin(),lst.cend(),val);

int ia[] = {0,1,2,3,4,5,6};
int val = 4;
int* result = find(begin(ia),end(ia),val); //同样适用于数组并返回指针类型的结果

2.只读算法

  一些算法只会读取其输入范围内的元素,而从不改变元素,这类算法称为只读算法,find就是这样一种算法,此外还有count函数、accumulate函数、equal函数对于只读取而不改变元素的算法,通常最好使用cbegin()和cend(),但是,如果你计划使用算法返回的迭代器来改变元素的值,就需要使用begin()和end()的结果作为参数
  accumulate函数用于对某个范围的元素进行求和,它接收三个参数,前两个指出了需要求和的元素的范围第三个参数是和的初值equal函数用于确定两个序列是否保存相同的值,它将第一个序列中的每个元素与第二个序列中的对应元素进行比较,如果所有对应元素都相等则返回true,否则返回false。它接受三个迭代器参数:前两个表示第一个序列中元素的范围第三个表示第二个序列的首元素。由于equal利用迭代器完成操作,因此我们可以通过equal来比较两个不同类型的容器中的元素,而且元素类型也不必一样,只要能用==来比较两个元素类型即可。

3.写容器元素的操作

  一些算法将新值赋予序列中的元素,当我们使用这类算法时,必须确保序列原大小至少不小于我们要求算法写入的元素数目。记住,算法不会执行容器操作,因此它们自身不可能改变容器的大小。
  例如fill算法接受一对迭代器表示一个范围,还接受一个值作为第三个参数fill将给定的这个值赋予输入序列中的每个元素。此外fill_n算法接受一个单迭代器、一个计数值和一个值,它将这个给定值赋予迭代器指向的元素开始的指定个元素

fill(vec.begin(),vec.end(),0); //将每个元素重置为0

vector<int> vec; //空vector
fill_n(vec.begin(),vec.size(),0); //将所有元素重置为0
fill_n(vec.begin(),10,0); //错误,vector并没有10个元素

  一种保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器。插入迭代器是一种向容器中添加元素的迭代器,通常情况下,当我们通过一个迭代器向容器元素赋值时,值被赋予迭代器指向的元素。而当我们通过一个插入迭代器赋值时,一个与赋值号右侧值相等的元素被添加到容器中。例如back_inserter函数接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中

vector<int> vec; //空向量
auto it = back_inserter(vec); //通过插入迭代器将元素添加到vec中
*it = 42; //vec中现在有一个元素,值为42

vector<int> vec; //空向量
fill_n(back_inserter(vec),10,0); //正确,使用插入迭代器添加10个元素到vec

  拷贝(copy)算法是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法。此算法接受三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置。此算法将输入范围中的元素拷贝到目的序列中。传递给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); //把a1的内容拷贝给a2,ret指向拷贝到a2的尾元素之后的位置

  replace算法读入一个序列,并将其中所有等于给定值的元素都改为另一个值。此算法接受4个参数:前两个是迭代器,表示输入序列后两个一个是要搜索的值,另一个是新值。它将所有等于第一个值的元素替换为第二个值:

replace(ilist.begin(),ilist.end(),0,42); //将所有值为0的元素改为42

  如果我们希望保留原序列不变,可以调用replace_copy,此算法接受额外第三个迭代器参数,指出调整后序列的保存位置

replace_copy(ilst.cbegin(),ilst.cend(),back_inserter(ivec),0,42); //使用back_inserter按需增长目标序列

  经过调整后,ilst并未改变,ivec包含一个ilst的一份拷贝,不过原来在ilst中值为0的元素在ivec中都变为42。

4.重排容器元素的算法

  某些算法会重排容器中元素的顺序,一个典型的例子就是sort函数,调用sort会重排输入序列中的元素,使之有序,它是利用元素类型的<运算符来实现排序的。
  unique算法重排输入序列,将相邻的重复项“消除“,并返回一个指向不重复值范围末尾的迭代器。相邻的重复元素并不是真正被“删除“了,它只是覆盖相邻的重复元素,使得不重复元素出现在序列的开始部分。unique返回的迭代器指向最后一个不重复之后的位置。此位置之后的元素仍然存在,但我们不知道它们的值是什么(实测结果为和之前的值一样)。标准库算法对迭代器而不是容器进行操作,因此,算法不能(直接)添加或删除元素,为了真正地删除后面重复的元素,我们必须使用容器操作,例如erase

// 消除重复单词
void elimDups(vector<string> &words)
{
	// 按字典序排序words,以便查找重复单词
	sort(words.begin(),words.end());
	// unique重排输入范围,使得每个单词只出现一次
	// 排序在范围的前部,返回指向不重复区域之后一个位置的迭代器
	auto end_uniqe = unique(words.begin(),words.end());
	//使用向量操作erase删除重复单词
	words.erase(end_unique,words.end());
}

5.定制操作

  很多算法都会比较输入序列中的元素,默认情况下,这类算法使用元素类型的<或==运算符完成比较。标准库还为这些算法定义了额外的版本,允许我们提供自定义的操作来代替默认运算符。自定义的操作通常有两种,一是向算法传递函数二是向算法传递lambda表达式
  向算法传递的函数称为谓词,谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法使用的谓词分为两类:一元谓词(意味着它们只接受单一参数)和二元谓词(意味着它们有两个参数)。接受谓词参数的算法对输入序列中的元素调用谓词,因此元素类型必须能转换为谓词的参数类型。例如,我们为sort算法提供自定义的比较函数:

// 比较函数,用来按长度排序单词,二元谓词
bool isShorter(const string &s1,const string &s2)
{
	return s1.size() < s2.size();
}
// 按长度由短至长排序words
sort(words.begin(),words.end(),isShorter);

  此外,如果我们想要维持相等元素的原有顺序,则可以使用稳定排序算法stable_sort算法
  根据算法接受一元谓词还是二元谓词,我们传递给算法的谓词必须严格接受一个或两个参数,但是有时我们希望进行的操作需要更多参数,超出了算法对谓词的限制。例如find_if算法的前两个参数是一对迭代器,表示一个范围,第三个参数是一个一元谓词,它返回第一个使谓词返回非0值的元素,如果没有则返回尾后迭代器。传递给find_if的任何函数都必须严格接受一个参数,如果想要给它传递更多的参数就需要使用lambda表达式。
  对于一个对象或一个表达式,如果可以对其使用调用运算符,则称它为可调用的,可调用对象一共有四种:函数、函数指针、重载了函数调用运算符的类以及lambda表达式。一个lamdba表达式表示一个可调用的代码单元,我们可以将其理解为一个未命名的内联函数。与函数类似,一个lambda具有一个返回类型、一个参数列表和一个函数体,但与函数不同的是,lambda可能定义在函数内部。lambda表达式具有如下形式:[capture list] (parameter list) -> return type {function body},其中,capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表,此外,lambda必须使用尾置返回来指定返回类型,同时lambda表达式不能有默认参数。我们可以忽略参数列表和返回类型,但必须包括捕获列表和函数体。如果忽略返回类型,lambda根据函数体中的代码推断出返回类型,如果函数体只是一个return语句,则返回类型从返回的表达式的类型推断出来,否则返回类型为void。

auto f = [] {return 42;}; //定义了一个可调用对象f,它不接受参数,返回42
cout << f() << endl; //lambda的调用方式与普通函数类似

// 使用lambda表达式,按长度由短至长排序
sort( words.begin(),words.end(),[](const string &s1,const string &s2)
								{return s1.size() < s2.size();} );

  虽然ambda可以出现在一个函数中,并使用其非static局部变量,但它只能使用那些明确指明的变量,也就是捕获列表中声明的变量。此外,一个lambda可以直接使用局部static变量和声明在当前函数之外的名字,而不需通过捕获列表。例如,通过使用lambda表达式,我们就能通过捕获列表向find_if传递更多的参数。

auto wc  = find_if(words.begin(),words.end(), [sz](const string &a) 
												{ reutrn a.size() >= sz;} };

  当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。可以这样理解,当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。默认情况下,从lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员。
  类似值传递,捕获列表中变量的捕获方式也可以是值或引用。采用值捕获时,与普通的值传递不同,被捕获的变量的值是在lambda创建时拷贝而不是调用是拷贝

{
	size_t v1 = 42;
	auto f = [v1] {return v1;}; //v1为42
	v1 = 0;
	auto j = f(); //j为42而不是0
}

  类似引用传递,当以引用方式捕获一个变量时。必须保证在lambda执行时变量是存在的。

发布了19 篇原创文章 · 获赞 6 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/hjc132/article/details/105649978