《C++Primer》第十一章-泛型算法-学习笔记(3)
日志:
1,2020-03-16 笔者提交文章的初版V1.0
作者按:
最近在学习C++ primer,初步打算把所学的记录下来。
传送门/推广
《C++Primer》第二章-变量和基本类型-学习笔记(1)
《C++Primer》第三章-标准库类型-学习笔记(1)
《C++Primer》第八章-标准 IO 库-学习笔记(1)
《C++Primer》第十二章-类-学习笔记(1)
泛型算法的结构
正如所有的容器都建立在一致的设计模式上一样,算法也具有共同的设计基础。理解标准算法库的设计基础有利于学习和使用算法。C++ 提供了超过一百个算法,了解它们的结构显然要比死记所有的算法更好。
算法最基本的性质
是需要使用的迭代器种类
。所有算法都指定了它的每个迭代器形参
可使用的迭代器类型。如果形参必须为随机访问迭代器
则可提供vector 或 deque 类型的迭代器,或者提供指向数组的指针。而其他容器的迭代器不能用在这类算法上。
另一种算法分类的方法,则如本章开头介绍的一样,根据对元素的操作将算法分为下面几种:
- 只读算法,不改变元素的值顺序。
- 给指定元素赋新值的算法。
- 将一个元素的值移给另一个元素的算法。
正如本节后续部分所介绍的,C++ 还提供了另外两种算法模式:一种模式由算法所带的形参定义;另一种模式则通过两种函数命名和重载的规范定义。
算法的形参模式
任何其他的算法分类都含有一组形参规范。理解这些形参规范有利于学习新的算法——只要知道形参的含义,就可专注于了解算法实现的操作。大多数算法采用下面四种形式之一:
alg (beg, end, other parms);
alg (beg, end, dest, other parms);
alg (beg, end, beg2, other parms);
alg (beg, end, beg2, end2, other parms);
其中,alg 是算法的名字,beg 和 end 指定算法操作的元素范围。我们通常将该范围称为算法的“输入范围”
。尽管几乎所有算法都有输入范围,但算法是否使用其他形参取决于它所执行的操作。这里列出了比较常用的其他形参:dest、beg2 和 end2,它们都是迭代器。这些迭代器在使用时,充当类似的角色。除了这些迭代器形参之外,有些算法还带有其他的菲迭代器形参,它们是这些算法特有的。
带有单个目标迭代器的算法
dest 形参
是一个迭代器
,用于指定存储输出数据的目标对象。算法假定无论需要写入多少个元素都是安全的。
调用这些算法时,必须确保输出容器有足够大的容量存储输出数据,这正是通常要使用插入迭代器或者 ostream_iterator来调用这些算法的原因。如果使用容器迭代器调用这些算法, 算法将假定容器里有足够多个需要的元素。
如果 dest 是容器上的迭代器,则算法将输出内容写到容器中已存在的元素上。更普遍的用法是,将 dest 与某个插入迭代器(第 11.3.1 节)或者ostream_iterator 绑定在一起。插入迭代器在容器中添加元素,以确保容器有足够的空间存储输出。ostream_iterator
则实现写输出流的功能
,无需要考虑所写的元素个数。
带第二个输入序列的算法
有一些算法带有一个 beg2 迭代器形参,或者同时带有 beg2 和 end2 迭代器形参,来指定它的第二个输入范围。这类算法通常将联合两个输入范围的元素来完成计算功能。 算法同时使用 beg2 和 end2 时,这些迭代器用于标记完整的第二个范围。也就是说,此时,算法完整地指定了两个范围
:beg 和 end 标记第一个输入范围,而 beg2 和 end2 则标记第二个输入范围。
带有 beg2 而不带 end2 的算法将 beg2 视为第二个输入范围的首元素,但没有指定该范围的最后一个元素。这些算法假定以 beg2 开始的范围至少与 beg和 end 指定的范围一样大。
与写入 dest 的算法一样,只带有 beg2 的算法也假定以 beg2开始的序列与 beg 和 end 标记的序列一样大。
算法的命名规范
标准库使用一组相同的命名和重载规范, 了解这些规范有助于更容易地学习标准库。它们包括两种重要模式:第一种模式包括测试输入范围内元素的算法
,第二种模式则应用于对输入范围内元素重新排序的算法
。
区别带有一个值或一个谓词函数参数的算法版本很多算法通过检查其输入范围内的元素实现其功能。 这些算法通常要用到标准关系操作符:== 或 <。其中的大部分算法会提供第二个版本的函数,允许程序员提供比较或测试函数取代操作符的使用。
重新对容器元素排序的算法要使用 < 操作符。这些算法的第二个重载版本带有一个额外的形参,表示用于元素排序的不同运算:
sort (beg, end); // use < operator to sort the elements
sort (beg, end, comp); // use function named comp to sort the elements
检查指定值的算法默认使用 == 操作符。系统为这类算法提供另外命名的(而非重载的)版本,带有谓词函数(第 11.2.3 节)形参。带有谓词函数形参的算法
,其名字带有后缀 _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 返回非零值的元素。
上述两个算法都在输入范围内寻找指定元素的第一个实例。其中,find 算法查找一个指定的值,而 find_if 算法则用于查找一个使谓词函数 pred 返回非零值的元素。
标准库为这些算法提供另外命名的版本,而非重载版本,其原因在于这个两种版本的算法带有相同数目的形参。对于排序算法,只要根据参数的个数就很容易消除函数调用的歧义。而对于查找指定元素的算法,不管检查的是一个值还是谓词函数,函数调用都需要相同个数的参数。此时,如果使用重载版本,则可能导致二义性(第 7.8.2 节),尽管这个可能出现的几率很低。因此,标准库为这些算法提供两种不同名字的版本,而没有使用重载。
区别是否实现复制的算法版本
无论算法是否检查它的元素值,都可能重新排列输入范围内的元素。在默认情况下,这些算法将重新排列的元素写回其输入范围。标准库也为这些算法提供另外命名的版本,将元素写到指定的输出目标。此版本的算法在名字中添加了_copy 后缀:
reverse(beg, end); //写回到输入
reverse_copy(beg, end, dest);//写到dest中
reverse 函数的功能就如它的名字所意味的:将输入序列中的元素反射重新排列。其中,第一个函数版本将自己的输入序列中的元素反向重排。而第二个版本,reverse_copy,则复制输入序列的元素,并将它们逆序存储到 dest 开始的序列中。
容器特有的算法
list 容器上的迭代器是双向的,而不是随机访问类型。由于 list 容器不支持随机访问,因此,在list容器上不能使用需要随机访问迭代器的算法。这些算法包括 sort 及其相关的算法。 还有一些其他的泛型算法,如 merge、remove、reverse 和 unique,虽然可以用在 list 上,但却付出了性能上的代价。如果这些算法利用 list 容器实现的特点,则可以更高效地执行。
如果可以结合利用 list 容器的内部结构,则可能编写出更快的算法。 与其他顺序容器所支持的操作相比,标准库为 list 容器定义了更精细的操作集合,使它不必只依赖于泛型操作。 表 11.4 列出了 list 容器特有的操作,其中不包括要求支持双向或更弱的迭代器类型的泛型算法,这类泛型算法无论是用在list 容器上,还是用在其他容器上,都具有相同的效果。
表 11.4. 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) | 将lst2 的元素移到 lst 中迭代器 iter 指向的元素前面 。在 lst2 中删除移出的元素。第一个版本将 lst2 的所有元素移到 lst 中;合并后,lst2 为空。lst 和 lst2 不能是同一个 list 对象。 |
lst.splice(iter, lst2, iter2) | 将lst2 的元素移到 lst 中迭代器 iter 指向的元素前面 。在 lst2 中删除移出的元素。lst.splice的第二个版本,只移动 iter2 所指向的元素,这个元素必须是 lst2 中的元素。 在这种情况中,lst 和lst2 可以是同一个 list 对象。也就是说,可在一个 list对象中使用 splice 运算移动一个元素。 |
lst.splice(iter, beg, end) | 将lst2 的元素移到 lst 中迭代器 iter 指向的元素前面 。在 lst2 中删除移出的元素。lst.splice的第三个版本,移动迭代器 beg 和 end 标记的范围内的元素。beg 和 end 照例必须指定一个有效的范围。这两个迭代器可标记任意 list 对象内的范围,包括 lst。 当它们指定 lst 的一段范围时,如果 iter 也指向这个范围的一个元素,则该运算未定义。 |
lst.unique() lst.unique(binaryPred) | 调用 erase 删除同一个值的团结副本 。第一个版本使用 ==操作符判断元素是否相等;第二个版本则使用指定的谓词函数实现判断 |
对于 list 对象,应该优先使用 list 容器特有的成员版本,而不是泛型算法。
大多数 list 容器特有的算法类似于其泛型形式中已经见过的相应的算法,但并不相同:
l.remove(val); // removes all instances of val from 1
l.remove_if(pred); // removes all instances for which pred is true from 1
l.reverse(); // reverses the order of elements in 1
l.sort(); // use element type < operator to compare elements
l.sort(comp); // use comp to compare elements
l.unique(); // uses element == to remove adjacent duplicates
l.unique(comp); // uses comp to remove duplicate adjacent copies
list 容器特有的算法与其泛型算法版本之间有两个至关重要的差别。
- 其中一个差别是
remove
和unique
的 list 版本修改了其关联的基础容器:真正删除了指定的元素。例如,list::unique 将 list 中第二个和后续重复的元素删除出该容器。与对应的泛型算法不同,list 容器特有的操作能添加和删除元素。 - 另一个差别是 list 容器提供的
merge
和splice
运算会破坏它们的实参。使用 merge 的泛型算法版本时,合并的序列将写入目标迭代器指向的对象,而它的两个输入序列保持不变。但是,使用 list 容器的 merge 成员函数时,则会破坏它的实参 list 对象——当实参对象的元素合并到调用 merge 函数的list 对象时,实参对象的元素被移出并删除。
小结
C++ 标准化过程做出的更重要的贡献之一是:创建和扩展了标准库。容器和算法库是标准库的基础。 标准库定义了超过一百个算法。幸运的是,这些算法具有相同的结构,使它们更易于学习和使用。
算法与类型无关:它们通常在一个元素序列上操作,这些元素可以存储在标准库容器类型、内置数组甚至是生成的序列(例如读写流所生成的序列)上。算法基于迭代器操作,从而实现类型无关性。大多数算法使用一对指定元素范围的迭代器作为其头两个实参。其他的迭代器实参包括指定输出目标的输出迭代器,或者用于指定第二个输入序列的另一个或一对迭代器。
迭代器可通过其所支持的操作来分类。 标准库定义了五种迭代器
类别:输入、输出、前向、双向和随机访问迭代器
。如果一个迭代器支持某种迭代器类别要求的运算,则该迭代器属于这个迭代器类别。
正如迭代器根据操作来分类一样,算法的迭代器形参
也通过其所要求的迭代器操作来分类。只需要读取其序列的算法通常只要求输入迭代器的操作。而写目标迭代器的算法则通常只要求输出迭代器的操作,依此类推。
查找某个值的算法通常提供第二个版本,用于查找使谓词函数
返回非零值的元素。对于这种算法,第二个版本的函数名字以_if 后缀标识
。类似地,很多算法提供所谓的复制版本
,将(修改过的)元素写到输出序列,而不是写回输入范围。这种版本的名字以 _copy
结束。
第三种模式是考虑算法是是否对元素读、写或者重新排序。算法从不直接改变它所操纵的序列的大小。(如果算法的实参是插入迭代器,则该迭代器会添加新元素,但算法并不直接这么做。)算法可以从一个位置将元素复制到另一个位置,但不直接添加或删除元素。