C++primer第十章 泛型算法 10.1 概述 10.2 初识泛型算法

  • 大多数算法都定义在头文件algorithm中。标准库还在头文件numeric中定义了 一组数值泛型算法
  • 一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围(参见9.2.1节,第296页)来进行操作。通常情况下,算法遍历范围,对其中每个元素进行一些处理。例如,假定我们有一个int的vector,希望知道vector中是否包含一个特定值。回答这个问题最方便的方法是调用标准库算法find

  • 传递给find的前两个参数是表示元素范围的迭代器,第三个参数是一个值。find将范围中每个元素与给定值进行比较。它返回指向第一个等于给定值的元素的迭代器。如果范围中无匹配元素,则 find返回第二个参数来表示搜索失败。因此,我们可以通过比较返回值和第二个参数来判断搜索是否成功。我们在输出语句中执行这个检测,其中使用了条件运算符(参见4.7节,第 134页)来报告搜索是否成功。
  • 由于find操作的是迭代器,因此我们可以用同样的find函数在任何容器中查找值。 例如,可以用find在一个string的 list中查找一个给定值:

 

  • 此例中我们使用了标准库begin和 end函数(参见3.5.3节,第 106页)来获得指向ia 中首元素和尾元素之后位置的指针,并传递给find。还可以在序列的子范围中查找,只需将指向子范围首元素和尾元素之后位置的迭代器指针)传递给find。例如,下面的语句在ia[l]、 ia[2]和 ia[3]中查找给定元素:
  • / / 在 从 ia[l]开始,直 至 (但不包含)ia[4]的范围内查找元素  auto result = find(ia + 1, ia + 4, val);

算法如何工作

  • 为了弄清这些算法如何用于不同类型的容器,让我们更近地观察一下find。find的工作是在一个未排序的元素序列中查找一个特定元素。概念上,find应执行如下步骤:
  • 1.访问序列中的首元素。
  • 2.比较此元素与我们要查找的值。
  • 3.如果此元素与我们要查找的值匹配,find返回标识此元素的值。
  • 4.否则,find前进到下一个元素,重复执行步骤2和3。
  • 5.如果到达序列尾,find应停止。
  • 6.如果find到达序列末尾,它应该返回一个指出元素未找到的值。此值和步骤3返回的值必须具有相容的类型。
  • 这些步骤都不依赖于容器所保存的元素类型。因此,只要有一个迭代器可用来访问元素,find就完全不依赖于容器类型(甚至无须理会保存元素的是不是容器)。

迭代器令算法不依赖于容器,……

  • 在上述find函数流程中,除了第2步外,其他步骤都可以用迭代器操作来实现:利用迭代器解引用运算符可以实现元素访问;如果发现匹配元素,find可以返回指向该元素的迭代器;用迭代器递增运算符可以移动到下一个元素;尾后迭代器可以用来判断find是否到达给定序列的末尾;find可以返回尾后迭代器(参见9.2.1节,第296页)来表示未找到给定元素。

……,但算法依赖于元素类型的操作

  • 虽然迭代器的使用令算法不依赖于容器类型,但大多数算法都使用了一个(或多个)元素类型上的操作。例如,在步骤2中,find用元素类型的==运算符完成每个元素与给定值的比较。其他算法可能要求元素类型支持<运算符。不过,我们将会看到,大多数算法提供了一种方法,允许我们使用自定义的操作来代替默认的运算符。

 

关键概念:算法水远不会执行容器的操作

  • 泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作。泛型算法运行于迭代器之上而不会执行容器操作的特性带来了一个令人惊讶但非常必要的编程假定:算法永远不会改变底层容器的大小。算法可能改变容器中保存的元的值,也可能在容器内移动元素,但永远不会直接添加或删除元素。
  • 如我们将在10.4.1节(第358页)所看到的,标准库定义了一类特殊的迭代器,称为插入器(inserter).与普通迭代器只能遍历所绑定的容器相比,插入器能做更多的事情。当给这类迭代器赋值时,它们会在底层的容器上执行插入操作。因此,当一个算法操作一个这样的迭代器时,迭代器可以完成向容器添加元素的效果,但算法自身永远不会做这样的操作

10.2初识泛型算法

  • 标准库提供了超过100个算法。幸运的是,与容器类似,这些算法有一致的结构。比起死记硬背全部100多个算法,理解此结构可以帮助我们更容易地学习和使用这些算法。在本章中,我们将展示如何使用这些算法,并介绍刻画了这些算法的统一原则。附录A按操作方式列出了所有算法。
  • 除了少数例外,标准库算法都对一个范围内的元素进行操作。我们将此元素范围称为“输入范围”。接受输入范围的算法总是使用前两个参数来表示此范围,两个参数分别是指向要处理的第一个元素和尾元素之后位置的迭代器。
  • 虽然大多数算法遍历输入范围的方式相似,但它们使用范围中元素的方式不同。理解算法的最基本的方法就是了解它们是否读取元素、改变元素或是重排元素顺序。

10.2.1只读算法

  • 一些算法只会读取其输入范围内的元素,而从不改变元素。find就是这样一种算法,我们在10.1节练习(第337页)中使用的count函数也是如此。另一个只读算法是accumulate,它定义在头文件numeric中。accumulate函数接受三个参数,前两个指出了需要求和的元素的范围,第三个参数是和的初值。假定vec是一个整数序列,则:
  • intsum=accumulate(vec.cbegin(),vec.cend(),0);    //对vec中的元素求和,和的初值是0。这条语句将sum设置为vec中元素的和,和的初值被设置为0。
  • accumulate的第三个参数的类型决定了函数中使用哪个加法运算符以及返 回值的类型。

算法和元素类型

  • accumulate将第三个参数作为求和起点,这蕴含着一个编程假定:将元素类型加到和的类型上的操作必须是可行的。即,序列中元素的类型必须与第三个参数匹配,或者能够转换为第三个参数的类型。在上例中,vec中的元素可以是int,或者是double, long long或任何其他可以加到int上的类型。 下面是另一个例子,由 于 string定义了+运算符,所以我们可以通过调用 accumulate来将vector中所有string元素连接起来:
  • string sum = accumulate (v . cbegin () , v . cend () , string (“”));
  • 此调用将v中每个元素连接到一个string上,该string初始时为空串。注意,我们通过第三个参数显式地创建了一个string。将空串当做一个字符串字面值传递给第三个参数是不可以的,会导致一个编译错误。
  • stringsum=accumulate(v.cbegin(),v.cend(),"**);   //错误:const char*上没有定义+运算符
  • 原因在于,如果我们传递了一个字符串字面值,用于保存和的对象的类型将是const char*。如前所述,此类型决定了使用哪个+运算符。由于const char*并没有+运算符,此调用将产生编译错误。
  • 对于只读取而不改变元素的算法,通常最好使用cbegin () 和 cend ( ) (参见 'Pm^ 9.2.3节,第298页 ) 但是,如果你计划使用算法返回的迭代器来改变元素的值,就需要使用begin ()和 e n d ()的结果作为参数

操作两个序列的算法

  • 另一个只读算法是equal,用于确定两个序列是否保存相同的值。它将第一个序列中的每个元素与第二个序列中的对应元素进行比较。如果所有对应元素都相等,则返回true,否则返回false。此算法接受三个迭代器:前两个(与以往一样)表示第一个序列中的元素范围,第三个表示第二个序列的首元素
  • equal(rosterl.cbegin(),rosterl.cend(),roster2.cbegin());   //roster2中的元素数目应该至少与rosterl-样多
  • 由于equal利用迭代器完成操作,因此我们可以通过调用equal来比较两个不同类型的容器中的元素。而且,元素类型也不必一样,只要我们能用==来比较两个元素类型即可。
  • 例如,在此例中,rosterl可以是vector<string>,而roster2是list<const char*>
  • 但是.equal基于一个非常重要的假设:它假定第二个序列至少与第一个序列一样长。此算法要处理第一个序列中的每个元素,它假定每个元素在第二个序列中都有一个与之对应的元素。
  • 那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长

10.2.2写容器元素的算法

  • -些算法将新值赋予序列中的元素。当我们使用这类算法时,必须注意确保序列原大小至少不小于我们要求算法写入的元素数目。记住,算法不会执行容器操作,因此它们自身不可能改变容器的大小。
  • 一些算法会自己向输入范围写入元素。这些算法本质上并不危险,它们最多写入与给定序列一样多的元素。
  • 例如,算法fill接受一对迭代器表示一个范围,还接受一个值作为第三个参数。fill 将给定的这个值赋予输入序列中的每个元素
  • fill (vec.begin () , vec.end() , 0) ; // 将每个元素重置为 0
  • fill(vec.begin(), vec.begin() + vec.size()/2, 10);   / / 将容器的一个子序列设置为10
  • 由于fill向给定输入序列中写入数据,因此,只要我们传递了一个有效的输入序列,写 入操作就是安全的

关键概念:迭代器参数

  • 一些算法从两个序列中读取元素“构成这两个序列的元素可以来自于不同类型的容器。例如,第一个序列可能保存于一个vector中,而第二个序列可能保存于一个list,deque、内置数组或其他容器中。而且,两个序列中元素的类型也不要求严格匹配。算法要求的只是能够比较两个序列中的元素,例如,对equal算法,元素类型不要求相同,但是我们必须能使用==来比较来自两个序列中的元素...
  • 操作两个序列的算法之间的区别在于我们如何传递第二个序列。一些算法,例如equal,接受三个迭代器:前两个表示第一个序列的范围,第三个表示第二个序列中的首元素其他算法接受四个迭代器:前两个表示第一个序列的元素范围,后两个表示第二个序列的范围。
  • 用一个单一迭代器表示第二个序列的算法都假定第二个序列至少与第一个一样长。
  • 确保算法不会试图访问第二个序列中不存在的元素是程序员的责任。例如,算法equal会将其第一个序列中的每个元素与第二个序列中的对应元素进行比较。如果第二个序列是第一个序列的一个子集,则程序会产生一个严重错误--equal会试图访问第二个序列中末尾之后(不存在)的元素

算法不检查写操作

  • 一些算法接受一个迭代器来指出一个单独的目的位置。这些算法将新值赋予一个序列中的元素,该序列从目的位置迭代器指向的元素开始。例如,函数fill_n接受一个单迭代器、一个计数值和一个值。它将给定值赋予迭代器指向的元素开始的指定个元素。我们可以用fill_n将一个新值赋予vector中的元素:
  • vector<int> vec; // 空 vector     / / 使用vec,赋予它不同值
  • fill_n (vec .begin () , vec. size () , 0) ; // 将所有元素重置为 0
  • 函数fill_n假定写入指定个元素是安全的。即,如下形式的调用
  • fill_n(dest, n, val)   //fill_n假定dest指向一个元素,而从dest开始的序列至少包含n 个元素
  • 一个初学者非常容易犯的错误是在一个空容器上调用(或类似的写元素的算法):
  • vector<int> vec; // 空向量           
  • (vec.begin(), 10, 0);      / / 灾难:修 改 vec中的10个 (不存在) 元素,这个调用是一场灾难。我们指定了要写入10个元素,但vec中并没有元素,它是空的。 这条语句的结果是未定义的。
  • 向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素 

介绍backjnserter

  • 一种保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器(insertiterator).插入迭代器是一种向容器中添加元素的迭代器。通常情况,当我们通过一个迭代器向容器元素赋值时,值被赋予迭代器指向的元素。而当我们通过一个插入迭代器赋值时,一个与赋值号右侧值相等的元素被添加到容器中。
  • 我们将在10.4.1节中(第358页)详细介绍插入迭代器的内容。但是,为了展示如何用算法向容器写入数据,我们现在将使用back_inserter,它是定义在头文件iterator中的一个函数
  • back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中:
  • vector<int>vec;//空向量  auto it=back_inserter(vec);//通过它赋值会将元素添加到vec中*it=42;//vec中现在有一个元素,值为42
  • 我们常常使用back_inserter来创建一个迭代器,作为算法的目的位置来使用。例如:vector<int>vec;//空向量   //正确:back_inserter创建一个插入迭代器,可用来向vec添加元素
  • fill_n(back_inserter(vec),10,0);//添加10个元素到vec
  • 在每步迭代中,向给定序列的一个元素赋值。由于我们传递的参数是back_inserter返回的迭代器,因此每次赋值都会在vec上调用push_back。最终,这条fill_n调用语句向vec的末尾添加了10个元素,每个元素的值都是0.

拷贝算法

  • 拷贝(copy)算法是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法。此算法接受三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置。此算法将输入范围中的元素拷贝到目的序列中。传递给copy的目的序列至少要包含与输入序列一样多的元素,这一点很重要。
  • 我们可以用copy实现内置数组的拷贝,如下面代码所示:
  • intal[]={0,1,2,3,4,5,6,7,8,9};
  • inta2[sizeof(al)/sizeof(*al)];//a2与al大小一样
  • //ret指向拷贝到a2的尾元素之后的位置
  • auto ret=copy(begin(al),end(al),a2);//把al的内容拷贝给a2
  • 此例中我们定义了一个名为a2的数组,并使用sizeof确保a2与数组al包含同样多的元素(参见4.9节,第 139页)。接下来我们调用copy完成从al到 a2的拷贝。在调用 copy后,两个数组中的元素具有相同的值。 copy返回的是其目的位置迭代器(递增后)的值。即,ret恰好指向拷贝到a2的 尾元素之后的位置
  • 多个算法都提供所谓的“拷贝”版本。这些算法计算新元素的值,但不会将它们放置在输入序列的末尾,而是创建一个新序列保存这些结果。
  • 例如,replace算法读入一个序列,并将其中所有等于给定值的元素都改为另一个值。此算法接受4个参数:前两个是迭代器,表示输入序列,后两个一个是要搜索的值,另一个是新值。它将所有等于第一个值的元素替换为第二个值:
  • replace (i1st.begin(), ilst.end(), 0, 42); / / 将所有值为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。

1 0 .2 .3 重排容器元素的算法

  • 某些算法会重排容器中元素的顺序,一个明显的例子是sort。调用sort会重排输 入序列中的元素,使之有序,它是利用元素类型的〈运算符来实现排序的。
  • 例如,假定我们想分析一系列儿童故事中所用的词汇。假定已有一个vector,保存了多个故事的文本。我们希望化简这个vector,使得每个单词只出现一次,而不管单词 在任意给定文档中到底出现了多少次
  • 为了便于说明问题,我们将使用下面简单的故事作为输入:

消除重复单词 

  • 为了消除重复单词,首先将vector排序,使得重复的单词都相邻出现。一旦vector 排序完毕,我们就可以使用另一个称为unique的标准库算法来重排vector,使得不重复的元素出现在vector的开始部分。由于算法不能执行容器的操作,我们将使用vector 的erase成员来完成真正的删除操作:

  • 节,第 311页)。我们删除从end_unique开始直至words末尾的范围内的所有元素。 这个调用之后,words包含来自输入的8 个不重复的单词。 值得注意的是,即使words中没有重复单词,这样调用era se也是安全的。在此情况下,unique会返回words.end () 因此,传递给erase 的两个参数具有相同的值:
  • words.end迭代器相等意味着传递给e ra se 的元素范围为空。删除一个空范围没有什么不良后果,因此程序即使在输入中无重复元素的情况下也是正确的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值