C++ Primer 4 第十一章 泛型算法

第十一章 泛型算法

1. 概述

算法永不执行容器提供的操作:泛型算法本身从不执行容器操作,只是单独依赖迭代器和迭代器操作实现。算法基于迭代器及其操作实现,而并非基于容器操作。这个事实也许比较意外,但本质上暗示了:使用“普通”的迭代器时,算法从不修改基础容器的大小。正如我们所看到的,算法也许会改变存储在容器中的元素的值,也许会在容器内移动元素,但是,算法从不直接添加或删除元素。

2. 初窥算法

使用泛型算法必须包含 algorithm 头文件: #include <algorithm>

标准库还定义了一组泛化的算术算法(generalized numeric algorithm),其命名习惯与泛型算法相同。使用这些算法则必须包含 numeric 头文件: #include <numeric>

除了少数例外情况,所有算法都在一段范围内的元素上操作,我们将这段范围称为“输入范围(input range)”。带有输入范围参数的算法总是使用头两个形参标记该范围。这两个形参是分别指向要处理的第一个元素和最后一个元素的下一位置的迭代器。

不检查写入操作的算法:fill_n 函数带有的参数包括:一个迭代器、一个计数器以及一个值。该函数从迭代器指向的元素开始,将指定数量的元素设置为给定的值。fill_n 函数假定对指定数量的元素做写操作是安全的。初学者常犯的错误的是:在没有元素的空容器上调用 fill_n 函数(或者类似的写元素算法)。

     vector<int> vec; // empty vector

     // disaster: attempts to write to 10 (nonexistent) elements in vec

     fill_n(vec.begin(), 10, 0);

这个 fill_n 函数的调用将带来灾难性的后果。我们指定要写入 10 个元素,但这些元素却不存在——vec 是空的。其结果未定义,很可能导致严重的运行时错误。

插入迭代器:确保算法有足够的元素存储输出数据的一种方法是使用插入迭代器。插入迭代器是可以给基础容器添加元素的迭代器。通常,用迭代器给容器元素赋值时,被赋值的是迭代器所指向的元素。而使用插入迭代器赋值时,则会在容器中添加一个新元素,其值等于赋值运算的右操作数的值。

back_inserter 函数是迭代器适配器。与容器适配器一样,迭代器适配器使用一个对象作为实参,并生成一个适应其实参行为的新对象。在本例中,传递给 back_inserter 的实参是一个容器的引用。back_inserter 生成一个绑定在该容器上的插入迭代器。在试图通过这个迭代器给元素赋值时,赋值运算将调用 push_back 在容器中添加一个具有指定值的元素。使用 back_inserter 可以生成一个指向 fill_n 写入目标的迭代器:

     vector<int> vec; // empty vector

     // ok: back_inserter creates an insert iterator that adds elements to vec

     fill_n (back_inserter(vec), 10, 0); // appends 10 elements to vec

现在,fill_n 函数每写入一个值,都会通过 back_inserter 生成的插入迭代器实现。效果相当于在 vec 上调用 push_back,在 vec 末尾添加 10 个元素,每个元素的值都是 0。

3. 再谈迭代器

标准库所定义的迭代器不依赖于特定的容器。事实上,C++ 语言还提供了另外三种迭代器:

         1)插入迭代器:这类迭代器与容器绑定在一起,实现在容器中插入元素的功能。

         2)iostream 迭代器:这类迭代器可与输入或输出流绑定在一起,用于迭代遍历所关联的 IO 流。

         3)反向迭代器:这类迭代器实现向后遍历,而不是向前遍历。所有容器类型都定义了自己的 reverse_iterator 类型,由 rbegin 和 rend 成员函数返回。

         上述迭代器类型都在 iterator 头文件中定义。

插入迭代器:back_inserter 函数是一种插入器。插入器是一种迭代器适配器,带有一个容器参数,并生成一个迭代器,用于在指定容器中插入元素。通过插入迭代器赋值时,迭代器将会插入一个新的元素。C++ 语言提供了三种插入器,其差别在于插入元素的位置不同。

         1)back_inserter,创建使用 push_back 实现插入的迭代器。

         2)front_inserter,该函数将创建一个迭代器,调用它所关联的基础容器的 push_front 成员函数代替赋值操作。有当容器提供 push_front 操作时,才能使用 front_inserter。在 vector 或其他没有 push_front 运算的容器上使用 front_inserter,将产生错误。

         3)inserter,使用 insert 实现插入操作。除了所关联的容器外,inserter 还带有第二实参:指向插入起始位置的迭代器:inserter 函数总是在它的迭代器实参所标明的位置前面插入新元素。

     list<int>::iterator it = find (ilst.begin(), ilst.end(), 42);

     replace_copy (ivec.begin(), ivec.end(),inserter (ilst, it), 100, 0);

iostream 迭代器:istream_iterator 用于读取输入流,而 ostream_iterator 则用于写输出流。这些迭代器将它们所对应的流视为特定类型的元素序列。使用流迭代器时,可以用泛型算法从流对象中读数据(或将数据写到流对象中)。

iostream 迭代器的构造函数:

         istream_iterator<T> in(strm); 创建从输入流 strm 中读取 T 类型对象的 istream_iterator 对象

         istream_iterator<T> in; istream_iterator 对象的超出末端迭代器

        ostream_iterator<T> in(strm);创建将 T 类型的对象写到输出流 strm 的 ostream_iterator 对象

        ostream_iterator<T> in(strm, delim);创建将 T 类型的对象写到输出流 strm 的 ostream_iterator 对象,在写入过程中使用 delim 作为元素的分隔符。delim 是以空字符结束的字符数组

流迭代器只定义了最基本的迭代器操作:自增、解引用和赋值。此外,可比较两个 istream 迭代器是否相等(或不等)。而 ostream 迭代器则不提供比较运算。

流迭代器的定义:流迭代器都是类模板:任何已定义输入操作符(>> 操作符)的类型都可以定义 istream_iterator。类似地,任何已定义输出操作符(<< 操作符)的类型也可定义 ostream_iterator。在创建流迭代器时,必须指定迭代器所读写的对象类型:

     istream_iterator<int> cin_it(cin);    // reads ints from cin

     istream_iterator<int> end_of_stream;  // end iterator value

     // writes Sales_items from the ofstream named outfile

     // each element is followed by a space

     ofstream outfile;

     ostream_iterator<Sales_item> output(outfile, " ");

         ostream_iterator 对象必须与特定的流绑定在一起。在创建 istream_iterator 时,可直接将它绑定到一个流上。另一种方法是在创建时不提供实参,则该迭代器指向超出末端位置。ostream_iterator 不提供超出末端迭代器。在创建 ostream_iterator 对象时,可提供第二个(可选的)实参,指定将元素写入输出流时使用的分隔符。分隔符必须是 C 风格字符串。因为它是 C 风格字符串,所以必须以空字符结束;否则,其行为将是未定义的。

istream_iterator 对象上的操作:构造与流绑定在一起的 istream_iterator 对象时将对迭代器定位,以便第一次对该迭代器进行解引用时即可从流中读取第一个值。

使用 istream_iterator 对象将标准输入读到 vector 对象中。

     istream_iterator<int> in_iter(cin);

     istream_iterator<int> eof;

     while (in_iter != eof)

             vec.push_back(*in_iter++);

ostream_iterator 对象的使用:可使用 ostream_iterator 对象将一个值序列写入流中,其操作的过程与使用迭代器将一组值逐个赋给容器中的元素相同:

     ostream_iterator<string> out_iter(cout, "\n");

     istream_iterator<string> in_iter(cin), eof;

     while (in_iter != eof)

        *out_iter++ = *in_iter++;

流迭代器的限制:

         1)不可能从 ostream_iterator 对象读入,也不可能写到 istream_iterator 对象中。

         2)一旦给 ostream_iterator 对象赋了一个值,写入就提交了。赋值后,没有办法再改变这个值。此外,ostream_iterator 对象中每个不同的值都只能正好输出一次。

         3)ostream_iterator 没有 -> 操作符。

与算法一起使用流迭代器:从标准输入读取一些数,再将读取的不重复的数写到标准输出:

     istream_iterator<int> cin_it(cin);    // reads ints from cin

     istream_iterator<int> end_of_stream;  // end iterator value

     vector<int> vec(cin_it, end_of_stream);

     sort(vec.begin(), vec.end());

     ostream_iterator<int> output(cout, " ");

     unique_copy(vec.begin(), vec.end(), output);

反向迭代器:反向迭代器是一种反向遍历容器的迭代器。也就是,从最后一个元素到第一个元素遍历容器。反向迭代器将自增(和自减)的含义反过来了:对于反向迭代器,++ 运算将访问前一个元素,而 -- 运算则访问下一个元素。

容器还定义了 rbegin 和 rend 成员,分别返回指向容器尾元素和首元素前一位置的反向迭代器。与普通迭代器一样,反向迭代器也有常量(const)和非常量(nonconst)类型。

         vector<int> vec;

     for (vector<int>::size_type i = 0; i != 10; ++i)

         vec.push_back(i); // elements are 0,1,2,...9

下面的 for 循环将以逆序输出这些元素:

     vector<int>::reverse_iterator r_iter;

     for (r_iter = vec.rbegin(); // binds r_iter to last element

          r_iter != vec.rend();  // rend refers 1 before 1st element

          ++r_iter)              // decrements iterator one element

         cout << *r_iter << endl;    // prints 9,8,7,...0

为了以降序排列 vector,只需向 sort 传递一对反向迭代器:

     // sorts vec in "normal" order

     sort(vec.begin(), vec.end());

     // sorts in reverse: puts smallest element at the end of vec

     sort(vec.rbegin(), vec.rend());

由于不能反向遍历流,因此流迭代器不能创建反向迭代器。

反向迭代器与其他迭代器之间的关系:反向迭代器用于表示范围,而所表示的范围是不对称的,这个事实可推导出一个重要的结论:使用普通的迭代器对反向迭代器进行初始化或赋值时,所得到的迭代器并不是指向原迭代器所指向的元素。[line.rbegin(), rcomma) 和 [rcomma.base(), line.end()) 标记的是 line 中的相同元素。

五种迭代器:算法要求的迭代器操作分为五个类别,分别对应五种迭代器:

         1)Input iterator(输入迭代器)读,不能写;只支持自增运算。要求在这个层次上提供支持的泛型算法包括 findaccumulate。标准库 istream_iterator 类型输入迭代器。

         2)Output iterator(输出迭代器)写,不能读;只支持自增运算。输出迭代器一般用作算法的第三个实参,标记起始写入的位置。

         3)Forward iterator(前向迭代器)读和写;只支持自增运算

         4)Bidirectional iterator(双向迭代器)读和写;支持自增和自减运算

         5)Random access iterator(随机访问迭代器)读和写;支持完整的迭代器算术运算

除了输出迭代器,其他类别的迭代器形成了一个层次结构:需要低级类别迭代器的地方,可使用任意一种更高级的迭代器。对于需要输入迭代器的算法,可传递前向、双向或随机访问迭代器调用该算法。调用需要随机访问迭代器的算法时,必须传递随机访问迭代器。map、set 和 list 类型提供双向迭代器,而 string、vector 和 deque 容器上定义的迭代器都是随机访问迭代器都是随机访问迭代器,用作访问内置数组元素的指针也是随机访问迭代器。istream_iterator 是输入迭代器,而 ostream_iterator 则是输出迭代器。

关联容器与算法:尽管 map 和 set 类型提供双向迭代器,但关联容器只能使用算法的一个子集。问题在于:关联容器的键是 const 对象。因此,关联容器不能使用任何写序列元素的算法。只能使用与关联容器绑在一起的迭代器来提供用于读操作的实参。在处理算法时,最好将关联容器上的迭代器视为支持自减运算的输入迭代器,而不是完整的双向迭代器。

C++ 标准为所有泛型和算术算法的每一个迭代器形参指定了范围最小的迭代器种类。例如,find(以只读方式单步遍历容器)至少需要一个输入迭代器。replace 函数至少需要一对前向迭代器。replace_copy 函数的头两个迭代器必须至少是前向迭代器,第三个参数代表输出目标,必须至少是输出迭代器。对于每一个形参,迭代器必须保证最低功能。将支持更少功能的迭代器传递给函数是错误的;而传递更强功能的迭代器则没问题。向算法传递无效的迭代器类别所引起的错误,无法保证会在编译时被捕获到。

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);

         调用这些算法时,必须确保输出容器有足够大的容量存储输出数据,这正是通常要使用插入迭代器或者 ostream_iterator 来调用这些算法的原因。如果使用容器迭代器调用这些算法,算法将假定容器里有足够多个需要的元素。

算法的命名规范:标准库使用一组相同的命名和重载规范。它们包括两种重要模式:第一种模式包括测试输入范围内元素的算法,第二种模式则应用于对输入范围内元素重新排序的算法。

很多算法通过检查其输入范围内的元素实现其功能。这些算法通常要用到标准关系操作符:== 或 <。其中的大部分算法会提供第二个版本的函数,允许程序员提供比较或测试函数取代操作符的使用。

检查指定值的算法默认使用 == 操作符。系统为这类算法提供另外命名的(而非重载的)版本,带有谓词函数形参。带有谓词函数形参的算法,其名字带有后缀 _if:

     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

上述两个算法都在输入范围内寻找指定元素的第一个实例。其中,find 算法查找一个指定的值,而 find_if 算法则用于查找一个使谓词函数 pred 返回非零值的元素。

区别是否实现复制的算法版本:无论算法是否检查它的元素值,都可能重新排列输入范围内的元素。在默认情况下,这些算法将重新排列的元素写回其输入范围。标准库也为这些算法提供另外命名的版本,将元素写到指定的输出目标。此版本的算法在名字中添加了 _copy 后缀:

     reverse(beg, end);

     reverse_copy(beg, end, dest);

第一个函数版本将自己的输入序列中的元素反向重排。而第二个版本,reverse_copy,则复制输入序列的元素,并将它们逆序存储到 dest 开始的序列中。

5. 容器特有的算法

list 容器上的迭代器是双向的,而不是随机访问类型。由于 list 容器不支持随机访问,因此,在此容器上不能使用需要随机访问迭代器的算法。这些算法包括 sort 及其相关的算法。还有一些其他的泛型算法,如 merge、remove、reverse 和 unique,虽然可以用在 list 上,但却付出了性能上的代价。如果这些算法利用 list 容器实现的特点,则可以更高效地执行。与其他顺序容器所支持的操作相比,标准库为 list 容器定义了更精细的操作集合,使它不必只依赖于泛型操作。

list 容器特有的操作:

         lst.merge(lst2) lst.merge(lst2, comp):将 lst2 的元素合并到 lst 中。这两个 list 容器对象都必须排序。lst2 中的元素将被删除。合并后,lst2 为空。返回 void 类型。第一个版本使用 < 操作符,而第二个版本则使用 comp 指定的比较运算

         lst.remove(val) lst.remove_if(unaryPred):调用 lst.erase 删除所有等于指定值或使指定的谓词函数返回非零值的元素。返回 void 类型

        lst.reverse():反向排列 lst 中的元素

        lst.sort:对 lst 中的元素排序

        lst.splice(iter, lst2)

        lst.splice(iter, lst2, iter2)

        lst.splice(iter, beg, end):将 lst2 的元素移到 lst 中迭代器 iter 指向的元素前面。在 lst2 中删除移出的元素。第一个版本将 lst2 的所有元素移到 lst 中;合并后,lst2 为空。lst 和 lst2 不能是同一个 list 对象。第二个版本只移动 iter2 所指向的元素,这个元素必须是 lst2 中的元素。在这种情况中,lst 和 lst2 可以是同一个 list 对象。也就是说,可在一个 list 对象中使用 splice 运算移动一个元素。第三个版本移动迭代器 beg 和 end 标记的范围内的元素。beg 和 end 照例必须指定一个有效的范围。这两个迭代器可标记任意 list 对象内的范围,包括 lst。当它们指定 lst 的一段范围时,如果 iter 也指向这个范围的一个元素,则该运算未定义。

        lst.unique() lst.unique(binaryPred):调用 erase 删除同一个值的团结副本。第一个版本使用 == 操作符判断元素是否相等;第二个版本则使用指定的谓词函数实现判断

大多数 list 容器特有的算法类似于其泛型形式中已经见过的相应的算法,但并不相同。对于 list 对象,应该优先使用 list 容器特有的成员版本,而不是泛型算法。

list 容器特有的算法与其泛型算法版本之间有两个至关重要的差别。其中一个差别是 remove 和 unique 的 list 版本修改了其关联的基础容器:真正删除了指定的元素。例如,list::unique 将 list 中第二个和后续重复的元素删除出该容器。与对应的泛型算法不同,list 容器特有的操作能添加和删除元素。另一个差别是 list 容器提供的 merge 和 splice 运算会破坏它们的实参。使用 merge 的泛型算法版本时,合并的序列将写入目标迭代器指向的对象,而它的两个输入序列保持不变。但是,使用 list 容器的 merge 成员函数时,则会破坏它的实参 list 对象——当实参对象的元素合并到调用 merge 函数的 list 对象时,实参对象的元素被移出并删除。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值