C++primer第十章 泛型算法 10.4 再探迭代器 10.5 泛型算法结构

  • 除了为每个容器定义的迭代器之外,标准库在头文件iterator中还定义了额外几种迭代器。这些迭代器包括以下几种。
  • 插入迭代器(insert iterator):这些迭代器被绑定到一个容器上,可用来向容器插入元素。
  • 流迭代器(stream iterator):这些迭代器被绑定到输入或输出流上,可用来遍历所关联的IO流。
  • 反向迭代器 (reverse iterator ): 这些迭代器向后而不是向前移动 。除了 forward_list之外的标准库容器都有反向迭代器。
  • 移动迭代器(move iterator):这些专用的迭代器不是拷贝其中的元素,而是移动它们。我们将在13.6.2节 (第 480页)介绍移动迭代器。

10.4.1插入迭代器

  • 插入器是一种迭代器适配器(参见9.6节,第 329页),它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。当我们通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。表 10.2列出了这种迭代器支持的操作

  • 插入器有三种类型,差异在于元素插入的位置:
  • back_inserter (参见10.2.2节,第 341页)创建一个使用push_back的迭代器。
  • front_inserter创建一个使用push_front的迭代器。
  • inserter创建一个使用insert的迭代器。此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前。
  • 只有在容器支持push_front的情况下,我们才可以使用front_inserter ; 类 似 的 ,只 有 在 容 器 支 持 push_back的 情 况 下 ,我们才能使用 back_inserter
  • 理解插入器的工作过程是很重要的:当调用inserter(c, iter)时,我们得到一个迭代器,接下来使用它时,会将元素插入到iter原来所指向的元素之前的位置。即,如 果 it是由inserter生成的迭代器,则下面这样的赋值语句
  • *it = val;        其效果与下面代码一样
  • it = c.insert (it, val) ; // it 指向新加入的元素
  • ++it; / / 递增it使它指向原来的元素
  • front_inserter生成的迭代器的行为与inserter生成的迭代器完全不一样。当 我们使用front_inserter时,元素总是插入到容器第一个元素之前。即使我们传递给 inserter的位置原来指向第一个元素,只要我们在此元素之前插入一个新元素,此元素就不再是容器的首元素了:
  • list<int> 1st = {1,2,3,4}; list<int> lst2, lst3; // 空 list
  • / / 拷贝完成之后,lst2包含4 3 2 1        copy(1st.cbegin()r lst.cend(), front_inserter(lst2));
  • / / 拷 贝完成之后,1st3包 含 1 2 3 4     copy(1st.cbegin(), 1st.cend(), inserter(lst3, lst3.begin()));
  • 当调用front_ insert( c )时 ,我们得到一个插入迭代器,接下来会调用 push_front. 当每个元素被插入到容器c 中时,它变为c的新的首元素。因此,front_insert生成的迭代器会将插入的元素序列的顺序颠倒过来,而insert 和back_insert则不会。

10.4.2 iostream 迭代器

  • 虽然iostream 类型不是容器,但标准库定义了可以用于这些IO类型对象的迭代器( 参 见 8.1 节 ,第 2 7 8 页 )。istream_iterator ( 参见表 1 0 .3 )读取输入流 ,ostream _iterator (参见表10.4节,第 361页)向一个输出流写数据。这些迭代器将它们对应的流当作一个特定类型的元素序列来处理。通过使用流迭代器,我们可以用泛型算法从流对象读取数据以及向其写入数据。

istreamjterator操作

  • 当创建一个流迭代器时 ,必须指定迭代器将要读写的对象类型一个istream_iterator使用>>来读取流。因此,istream_iterator 要读取的类型必须定义了输入运算符。当创建一个istream_iterator时,我们可以将它绑定到一个流。 当然,我们还可以默认初始化迭代器,这样就创建了一个可以当作尾后值使用的迭代器。

  • 此循环从cin读取int值,保存在vec中。在每个循环步中,循环体代码检查in_iter 是否等于eof 。eof被定义为空的istream_iterator,从而可以当作尾后迭代器来使 用。对于一个绑定到流的迭代器,一旦其关联的流遇到文件尾或遇到IO错误,迭代器的值就与尾后迭代器相等。
  • 此程序最困难的部分是传递给push_back的参数,其中用到了解引用运算符和后置递增运算符。该表达式的计算过程与我们之前写过的其他结合解引用和后置递增运算的表达式一样(参见4.5节,第 131页)。后置递增运算会从流中读取下一个值,向前推进,但返回的是迭代器的旧值。迭代器的旧值包含了从流中读取的前一个值,对迭代器进行解引用就能获得此值。
  • 我们可以将程序重写为如下形式,这体现了 istream_iterator更有用的地方:

  • accumulate函数 http://c.biancheng.net/view/682.html
  • 本例 我们用一对表示元素范围的迭代器来构造veco 这 两 个 迭代器是 istream_iterator,这意味着元素范围是通过从关联的流中读取数据获得的。这个构 造函数从cin中读取数据,直至遇到文件尾或者遇到一个不是int的数据为止。从流中 读取的数据被用来构造vec

10.4.3反向迭代器

  • 反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增(以及递减)操作的含义会颠倒过来。递增一个反向迭代器(++it)会移动到前一个元素;递减一个迭代器(--it)会移动到下一个元素。 除了 forward_list之外,其他容器都支持反向迭代器。我们可以通过调用rbegin、 rend、crbegin和 crend成员函数来获得反向迭代器。这些成员函数返回指向容器尾 元素和首元素之前一个位置的迭代器。与普通迭代器一样,反向迭代器也有const和非const版本。

反向迭代器需要递减运算符

  • 不必惊讶,我们只能从既支持++也支持--的迭代器来定义反向迭代器。毕竟反向迭代器的目的是在序列中反向移动。除了 forward_list之外,标准容器上的其他迭代器都 既支持递增运算又支持递减运算。但是,流迭代器不支持递减运算,因为不可能在一个流中反向移动。因此,不可能从一个forward_list或一个流迭代器创建反向迭代器。

反向迭代器和其他迭代器间的关系

  • 假定有一个名为line的 string,保存着一个逗号分隔的单词列表,我们希望打印line中的第一个单词。使用find可以很容易地完成这一任务:

  • 从技术上讲,普通迭代器与反向迭代器的关系反映了左闭合区间(参见9.2.1节,第296 页 ) 的 特 性 。关 键 点 在 于 [line.crbegin (), rcomma)和 [rcomma.base (), line.cend ) 指 向 line中相同的元素范围 。为了实现这一点 ,recomma和 rcomma.base ()必须生成相邻位置而不是相同位置,crbegin ()和 cend ()也是如此
  • 反向迭代器的目的是表示元素范围,而这些范围是不对称的,这导致一个重要的结果:当我们从一个普通迭代器初始化一个反向迭代器,或是给一个反向迭代器赋值时,结果迭代器与原迭代器指向的并不是相同的元素.

1 0 .5 泛型算法结构

  • 任何算法的最基本的特性是它要求其迭代器提供哪些操作。某些算法,如 find,只 要求通过迭代器访问元素、递增迭代器以及比较两个迭代器是否相等这些能力。其他一些算法,如 sort,还要求读、写和随机访问元素的能力。算法所要求的迭代器操作可以分为 5 个迭代器类别(iterator category),如表 10.5所示。每个算法都会对它的每个迭代器参数指明须提供哪类迭代器。

  • 第二种算法分类的方式(如我们在本章开始所做的)是按照是否读、写或是重排序列中的元素来分类。附录A 按这种分类方法列出了所有算法。
  • 算法还共享一组参数传递规范和一组命名规范,我们在介绍迭代器类别之后将介绍这些内容。

10.5.1 5 类迭代器 

  • 类似容器,迭代器也定义了一组公共操作。一些操作所有迭代器都支持,另外一些只有特定类别的迭代器才支持。例如,ostream_iterator只支持递增、解引用和赋值。 vector、string和 deque的迭代器除了这些操作外,还支持递减、关系和算术运算。 迭代器是按它们所提供的操作来分类的,而这种分类形成了一种层次。除了输出迭代器之外,一个高层类别的迭代器支持低层类别迭代器的所有操作。
  • C++标准指明了泛型和数值算法的每个迭代器参数的最小类别。例如,find算法在一个序列上进行一遍扫描,对元素进行只读操作,因此至少需要输入迭代器。replace 函数需要一对迭代器,至少是前向迭代器。类似的,replace_copy的前两个迭代器参数也要求至少是前向迭代器。其第三个迭代器表示目的位置,必须至少是输出迭代器。其他的例子类似。对每个迭代器参数来说,其能力必须与规定的最小类别至少相当。向算法传递一个能力更差的迭代器会产生错误。

迭代器类别

  • 输入迭代器(input iterator):可以读取序列中的元素。一个输入迭代器必须支持
  • 用于比较两个迭代器的相等和不相等运算符(==、!=)
  • 用于推进迭代器的前置和后置递增运算(++)
  • 用于读取元素的解引用运算符(*); 解引用只会出现在赋值运算符的右侧
  • 箭头运算符( - > ) , 等价于(* it).member,即,解引用迭代器,并提取对象的成员
  • 输入迭代器只用于顺序访问。对于一个输入迭代器,
  • *it++保证是有效的,但递增它可能 导致所有其他指向流的迭代器失效。其结果就是,不能保证输入迭代器的状态可以保存下来并用来访问元素。因此,输入迭代器只能用于单遍扫描算法。算法find 和 accumulate 要求输入迭代器;而 istream_iterator是一种输入迭代器。
  • 输出迭代器(output iterator):可以看作输入迭代器功能上的补集—— 只写而不读元素。输 出迭代器必须支持
  • 用于推进迭代器的前置和后置递增运算(++)
  • 解引用运算符(*), 只出现在赋值运算符的左侧(向一个已经解引用的输出迭代器赋值,就是将值写入它所指向的元素)
  • 我们只能向一个输出迭代器赋值一次。类似输入迭代器,输出迭代器只能用于单遍扫描算法。用作目的位置的迭代器通常都是输出迭代器。例如,copy函数的第三个参数就是输出迭代器。ostream_iterator类型也是输出迭代器。
  • 前向迭代器(forward iterator):可以读写元素。这类迭代器只能在序列中沿一个方向移动。 前向迭代器支持所有输入和输出迭代器的操作,而且可以多次读写同一个元素。因此,我们可以保存前向迭代器的状态,使用前向迭代器的算法可以对序列进行多遍扫描。算法replace要求前向迭代器,forward_list上的迭代器是前向迭代器。
  • 双向迭代器( bidirectional iterator):可以正向/反向读写序列中的元素。除了支持所有前向迭代器的操作之外,双向迭代器还支持前置和后置递减运算符(-- )。算法reverse要求双向迭代器,除了 forward_list之外,其他标准库都提供符合双向迭代器要求的迭代器。
  • 随机访问迭代器(random-access iterator):提供在常量时间内访问序列中任意元素的能力。 此类迭代器支持双向迭代器的所有功能,此外还支持表3.7 (第 99页)中的操作:
  • 用于比较两个迭代器相对位置的关系运算符(<、<=、>和>=)
  • 迭代器和一个整数值的加减运算(+、+=、-和= ), 计算结果是迭代器在序列中前进 (或后退)给定整数个元素后的位置
  • 用于两个迭代器上的减法运算符(-), 得到两个迭代器的距离
  • 下标运算符(iter [n])与* (iter [n])等价,算法sort要求随机访问迭代器。array、deque、string和 vector的迭代器都是随机访问迭代器,用于访问内置数组元素的指针也是。

1 0 .5 .2 算法形参模式

  • 在任何其他算法分类之上,还有一组参数规范。理解这些参数规范对学习新算法很有帮助一一通过理解参数的含义,你可以将注意力集中在算法所做的操作上。大多数算法具有如下4 种形式之一:

  • 其中alg是算法的名字,beg和 end表示算法所操作的输入范围。几乎所有算法都接受一 个输入范围,是否有其他参数依赖于要执行的操作。这里列出了常见的一种dest、 beg2和 end2,都是迭代器参数。顾名思义,如果用到了这些迭代器参数,它们分别承 担指定目的位置和第二个范围的角色。除了这些迭代器参数,一些算法还接受额外的、非迭代器的特定参数。

接受单个目标迭代器的算法

  • dest参数是一个表示算法可以写入的目的位置的迭代器。算法假定(assume):按其需要写入数据,不管写入多少个元素都是安全的。向输出迭代器写入数据的算法都假定目标空间足够容纳写入的数据“
  • 如果dest是一个直接指向容器的迭代器,那么算法将输出数据写到容器中已存在的元素内。更常见的情况是,dest被绑定到一个插入迭代器(参 见 10.4.1节,第 358页) 或是一个ostream_iterator (参 见 10.4.2节,第 359页)。插入迭代器会将新元素添加到容器中,因而保证空间是足够的。ostream_iterator会将数据写入到一个输出流, 同样不管要写入多少个元素都没有问题。

接受第二个输入序列的算法

  • 接受单独的beg2或是接受beg2和end2的算法用这些迭代器表示第二个输入范围。 这些算法通常使用第二个范围中的元素与第一个输入范围结合来进行一些运算。
  • 如果一个算法接受beg2和 end2,这两个迭代器表示第二个范围。这类算法接受两个完整指定的范围:[beg, end)表示的范围和[beg2 end2)表示的第二个范围。 只接受单独的beg2(不接受end2)的算法将beg2作为第二个输入范围中的首元素。 此范围的结束位置未指定,这些算法假定从beg2开始的范围与beg和 end所表示的范 围至少一样大。

10.5.3算法命名规范

  • 除了参数规范,算法还遵循一套命名和重载规范。这些规范处理诸如:如何提供一个操作代替默认的 <或=运算符以及算法是将输出数据写入输入序列还是一个分离的目的位置等问题。

一些算法使用重载形式传递一个谓词

  • 接受谓词参数来代替<或=运算符的算法,以及那些不接受额外参数的算法,通常都是重载的函数。函数的一个版本用元素类型的运算符来比较元素;另一个版本接受一个额外谓词参数,来代替<或=:
  • unique (beg, end) ; / / 使 用 = = 运算符比较元素
  • unique (beg, end, comp) ; // 使用 comp比较元素
  • 两个调用都重新整理给定序列,将相邻的重复元素删除。第一个调用使用元素类型的==运算符来检查重复元素;第二个则调用comp来确定两个元素是否相等。由于两个版本的 函数在参数个数上不相等,因此具体应该调用哪个版本不会产生歧义(参见6.4节,第208页)。

_ if版本的算法

  • 接受一个元素值的算法通常有另一个不同名的(不是重载的)版本,该版本接受一个谓词(参见10.3.1节,第 344页)代替元素值。接受谓词参数的算法都有附加的前缀:
  • find (beg, end, val) ; / / 查找输入范围中val第一次出现的位置
  • find_if (beg, end, pred) ; / / 查找第一个令pred为真的元素
  • 这两个算法都在输入范围中查找特定元素第一次出现的位置。算法find查找一个指定值; 算法f ind_if查找使得pred返回非零值的元素。 这两个算法提供了命名上差异的版本,而非重载版本,因为两个版本的算法都接受相同数目的参数。因此可能产生重载歧义,虽然很罕见,但为了避免任何可能的歧义,标准库选择提供不同名字的版本而不是重载。

区分拷贝元素的版本和不拷贝的版本

  • 默认情况下,重排元素的算法将重排后的元素写回给定的输入序列中。这些算法还提供另一个版本,将元素写到一个指定的输出目的位置。如我们所见,写到额外目的空间的算法都在名字后面附加一个_copy (参见10.2.2节,第 341页)

1 0 .6 特定容器算法

  • 与其他容器不同,链表类型list 和 forward_list定义了几个成员函数形式的算法,如 表 10.6所示。特别是,它们定义了独有的sort , merge、remove, reverse 和 uniqueo通用版本的sort 要求随机访问迭代器,因此不能用于list和fordward_list.因为这两个类型分别提供双向迭代器和前向迭代器。
  • 链表类型定义的其他算法的通用版本可以用于链表,但代价太高。这些算法需要交换输入序列中的元素。-个链表可以通过改变元素间的链接而不是真的交换它们的值来快速“交换”元素。因此,这些链表版本的算法的性能比对应的通用版本好得多。
  • 对于list和fordward_list,应该优先使用成员函数版本的算法而不是通用算法

splice成员

  • 链表类型还定义了 splice算法,其描述见表10.7。此算法是链表数据结构所特有的, 因此不需要通用版本

链表特有的操作会改变容器

  • 多数链表特有的算法都与其通用版本很相似,但不完全相同。链表特有版本与通用版本间的一个至关重要的区别是链表版本会改变底层的容器。例如,remove的链表版本会 删除指定的元素。unique的链表版本会删除第二个和后继的重复元素。 类似的,merge和 splice会销毁其参数。例如,通用版本的merge将合并的序列 写到一个给定的目的迭代器;两个输入序列是不变的。而链表版本的merge函数会销毁 给定的链表—— 元素从参数指定的链表中删除,被合并到调用merge的链表对象中。在 merge之后,来自两个链表中的元素仍然存在,但它们都已在同一个链表中。

小结

  • 标准库定义了大约100个类型无关的对序列进行操作的算法。序列可以是标准库容器类型中的元素、一个内置数组或者是(例如)通过读写一个流来生成的。算法通过在迭代器上进行操作来实现类型无关。多数算法接受的前两个参数是一对迭代器,表示一个元素范围。额外的迭代器参数可能包括一个表示目的位置的输出迭代器,或是表示第二个输入范围的另一个或另一对迭代器。
  • 根据支持的操作不同,迭代器可分为五类:输入、输出、前向、双向以及随机访问迭代器。如果一个迭代器支持某个迭代器类别所要求的操作,则属于该类别。如同迭代器根据操作分类一样,传递给算法的迭代器参数也按照所要求的操作进行分类。仅读取序列的算法只要求输入迭代器操作。写入数据到目的位置迭代器的算法只要求输出迭代器操作,依此类推。
  • 算法从不直接改变它们所操作的序列的大小。它们会将元素从一个位置拷贝到另一个位置,但不会直接添加或删除元素。虽然算法不能向序列添加元素,但插入迭代器可以做到。一个插入迭代器被绑定到一个容器上。当我们将一个容器元素类型的值赋予一个插入迭代器时,迭代器会将该值添加到容器中。
  • 容 器 forward_list和 list 对一些通用算法定义了自己特有的版本。与通用算法不同,这些链表特有版本会修改给定的链表

术语表

  • back_inserter这是一个迭代器适配器,它接受一个指向容器的引用,生成一个插入迭代器,该插入迭代器用push_back向指定容器添加元素。
  • 双向迭代器 ( bidirectional ite ra to r)支持前向迭代器的所有操作,还具有用--在序列中反向移动的能力。
  • 二元谓词(binary predicate)接受两个参数的谓词。
  • bind标准库函数,将一个或多个参数绑定到一个可调用表达式.bind定义在头文件functional中。
  • 可调用对象(callable object) 可以出现在调用运算符左边的对象。函数指针、lambda以及重载了函数调用运算符的类的 对象都是可调用对象。
  • 捕获列表 (capture list) lambda表达式的—部分,指出lambda表达式可以访问所在 上下文中哪些变量。
  • cref标准库函数,返回一个可拷贝的对象, 其中保存了一个指向不可拷贝类型的const对象的引用。
  • 前向迭代器(forward iterator)可以读写元素,但不必支持--的迭代器。front_inserter迭代器适配器,给定一个容器,生成一个用push_front向容器开始位置添加元素的插入迭代器。
  • 泛型算法(generic algorithm) 类型无关的算法。
  • 输入迭代器(input ite ra to r)可以读但不能写序列中元素的迭代器。
  • 插入迭代器 (insertite ra to r)迭代器适配器,生成一个迭代器,该迭代器使用容器操作向给定容器添加元素
  • 插 入 器 (inserter)迭代器适配器,接受一个迭代器和一个指向容器的引用,生成一个插入迭代器,该插入迭代器用insert 在给定迭代器指向的元素之前的位置添加元素。
  • istreamjterator读取输入流的流迭代器。迭代器类别(iterator category)根据所支持的操作对迭代器进行的分类组织。迭代器类别形成一个层次,其中更强大的类别支持更弱类别的所有操作。算法使用迭代器类别来指出迭代器参数必须支持哪些操作。只要迭代器达到所要求的最小类别,它就可以用于算法。例如,一些算法只要求输入迭代器。这类算法可处理除只满足输出迭代器要求的迭代器之外的任何迭代器。而要求随机访问迭代器的算法只能用于支持随机访问操作的迭代器。
  • lambda 表 达 式 (lambda expression) 可调用的代码单元.一个lambda类似一个未 命名的内联函数。一个lambda以一个捕获列表开始,此列表允许lambda访问所在函 数中的变量。类似函数,lambda有一个(可 能为空的)参数列表、一个返回类型和一个函数体» lambda可以忽略返回类型• 如 果函数体是一个单一的re tu rn 语句,返 回类型就从返回对象的类型推断。否则,忽略的返回类型默认定为void。
  • 移动迭代器(move iterator)迭代器适配器,生成一个迭代器,该迭代器移动而不是拷贝元素。移动迭代器将在第13章中进行介绍。
  • ostream_iterator写输出流的迭代器。 输出迭代器(outputiterator)可以写元素,但不必具有读元素能力的迭代器。
  • 谓 词 (predicate)返回可以转换为bool类型的值的函数。泛型算法通常用来检测元素。标准库使用的谓词是一元(接受一个参数)或二元(接受两个参数)的。
  • 随机访问迭代器(random-access iterator)支持双向迭代器的所有操作再加上比较迭代器值的关系运算符、下标运算符和迭代器匕的算术运算,因此支持随机访问元素。
  • ref标准库函数,从一个指向不能拷贝的类型的对象的引用生成一个可拷贝的对象。
  • 反向迭代器(reverse ite ra to r)在序列中反向移动的迭代器。这些迭代器交换了++和一的含义。
  • 流迭代器(stream ite ra to r)可以绑定到一个流的迭代器。
  • —元 谓 词 (unary predicate)接受一个参数的谓词。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当涉及到C++数据结构算法的书籍推荐时,以下是一些经典的选择: 1.《算法导论》(Introduction to Algorithms):由Thomas H. Cormen等人编写的这本书是计算机科学领域中最经典的教材之一。它详细介绍了各种常见的数据结构算法,并提供了丰富的示例和习题。 2.《数据结构算法分析:C++语言描述》(Data Structures and Algorithm Analysis in C++):由Mark Allen Weiss编写的这本书以C++语言为基础,深入讲解了各种数据结构算法的实现和分析。它还提供了大量的示例和习题,帮助读者理解和应用所学知识。 3.《C++数据结构算法》(Data Structures and Algorithms in C++):由Adam Drozdek编写的这本书以C++语言为基础,介绍了各种常见的数据结构算法。它通过清晰的解释和示例代码帮助读者理解和实现这些数据结构算法。 4.《C++ Primer》:由Stanley B. Lippman、Josée Lajoie和Barbara E. Moo编写的这本书是学习C++语言的经典教材之一。虽然它不是专门讲解数据结构算法的书籍,但它提供了对C++语言的全面介绍,为学习和实现数据结构算法打下了坚实的基础。 5.《STL源码剖析》(Inside the C++ Object Model):由Stanley B. Lippman编写的这本书深入剖析了C++标准模板库(STL)的实现原理和设计思想。了解STL的内部工作原理对于理解和应用数据结构算法非常有帮助。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值