C++学习笔记十一 -泛型算法
一、概述:
1.因为它们实现共同的操作,所以称之为“算法”;而“泛型”指的是它们可以操作在多种容器类型上——不但可作用于 vector 或 list 这些标准库类型,还可用在内置数组类型、甚至其他类型的序列上,这些我们将在本章的后续内容中了解。自定义的容器类型只要与标准库兼容,同样可以使用这些泛型算法。解算法的最基本方法是了解该算法是否读元素、写元素或者对元素进行重新排序。
2.大多数算法是通过遍历由两个迭代器标记的一段元素来实现其功能。典型情况下,算法在遍历一段元素范围时,操纵其中的每一个元素。算法通过迭代器访问元素,这些迭代器标记了要遍历的元素范围。
3.泛型算法本身从不执行容器操作,只是单独依赖迭代器和迭代器操作实现。算法基于迭代器及其操作实现,而并非基于容器操作。这个事实也许比较意外,但本质上暗示了:使用“普通”的迭代器时,算法从不修改基础容器的大小。正如我们所看到的,算法也许会改变存储在容器中的元素的值,也许会在容器内移动元素,但是,算法从不直接添加或删除元素.算法不直接修改容器的大小。
4.除了少数例外情况,所有算法都在一段范围内的元素上操作,我们将这段范围称为“输出范围(input range)”。带有输入范围参数的算法总是使用头两个形参标记该范围。这两个形参是分别指向要处理的第一个元素和最后一个元素的下一位置的迭代器。
二、初窥算法:
1.accumulate算法:容器内的元素类型必须与第三个实参的类型匹配,或者可转换为第三个实参的类型。
// sum the elements in vec starting the summation with the value 42 int sum = accumulate(vec.begin(), vec.end(), 42);
2.泛型算法都是在标记容器(或其他序列)内的元素范围的迭代器上操作的。标记范围的两个实参类型必须精确匹配,而迭代器本身必须标记一个范围:它们必须指向同一个容器中的元素(或者超出容器末端的下一位置),并且如果两者不相等,则第一个迭代器通过不断地自增,必须可以到达第二个迭代器。
3.find_first_of:这个算法带有两对迭代器参数来标记两段元素范围,在第一段范围内查找与第二段范围中任意元素匹配的元素,然后返回一个迭代器,指向第一个匹配的元素。如果找不到元素,则返回第一个范围的 end 迭代器。
4.写入到输入序列的算法本质上是安全的——只会写入与指定输入范围数量相同的元素。
fill(vec.begin(), vec.end(), 0); // reset each element to 0
fill_n 函数带有的参数包括:一个迭代器、一个计数器以及一个值。在没有元素的空容器上调用 fill_n 函数是错误的.对指定数目的元素做写入运算,或者写到目标迭代器的算法,都不检查目标的大小是否足以存储要写入的元素。
三、back_inserter插入迭代器:
1.确保算法有足够的元素存储输出数据的一种方法是使用插入迭代器。插入迭代器是可以给基础容器添加元素的迭代器。通常,用迭代器给容器元素赋值时,被赋值的是迭代器所指向的元素。而使用插入迭代器赋值时,则会在容器中添加一个新元素,其值等于赋值运算的右操作数的值。
2.copy算法:
A.假设 ilst 是一个存放 int 型数据的 list 对象,可如下将它 copy 给一个 vector 对象:
vectorivec; // empty vector // copy elements from ilst into ivec copy (ilst.begin(), ilst.end(), back_inserter(ivec)); B.调用该函数后,ilst 没有改变,ivec 存储 ilst 一份副本,而 ilst 内所有的 0 在 ivec 中都变成了 42。replace_copy (ilst.begin(), ilst.end(), back_inserter(ivec), 0, 42);3.排序算法:sort(b,e) //b为起始迭代器, e为指定范围最后一个元素的下一个迭代器sort(b,e,谓词函数) //b为起始迭代器, e为指定范围最后一个元素的下一个迭代器,谓词函数用来自定义比较两个元素的大小这个谓词函数必须接受两个实参,实参的类型必须与元素类型相同,并返回一个可用作条件检测的值。除了 sort 之外,标准库还定义了 stable_sort 算法,stable_sort 保留相等元素的原始相对位置。4.unique算法:使用该算法前,要先对元素进行排序.该算法,将容器中重复的元素,放在容器的尾部,并返回一个迭代器,表示无重复的值范围的结束。
5.find_if 函数带有一对迭代器形参,指定其操作的范围。该函数还带有第三个形参,表明用于检查范围内每个元素的谓词函数。find_if 返回一个迭代器,指向第一个谓词函数返回非零值的元素。如果这样的元素不存在,则返回第二个迭代器实参。
用法形式如下:find_if(b,e,谓词函数)
6.count_if 函数带有一对迭代器形参,指定其操作的范围。该函数还带有第三个形参,表明用于检查范围内每个元素的谓词函数。count_if 返回一个sizetype类型的数值,代表所有谓词函数返回非零值的元素的个数。
四、再论迭代器:
1.插入迭代器:插入器是一种迭代器适配器,带有一个容器参数,并生成一个迭代器,用于在指定容器中插入元素。通过插入迭代器赋值时,迭代器将会插入一个新的元素。C++ 语言提供了三种插入器,其差别在于插入元素的位置不同。
A.back_inserter,创建使用 push_back 实现插入的迭代器。
B.front_inserter,使用 push_front 实现插入。只有当容器提供 push_front 操作时,才能使用 front_inserter。在 vector 或其他没有 push_front 运算的容器上使用 front_inserter,将产生错误。front_inserter 的使用将导致元素以相反的次序出现在目标对象中,这点非常重要。
C.inserter,使用 insert 实现插入操作。除了所关联的容器外,inserter 还带有第二实参:指向插入起始位置的迭代器。
inserter 适配器提供更普通的插入形式。这种适配器带有两个实参:所关联的容器和指示起始插入位置的迭代器。
// position an iterator into ilst list::iterator it = find (ilst.begin(), ilst.end(), 42); // insert replaced copies of ivec at that point in ilst replace_copy (ivec.begin(), ivec.end(),inserter (ilst, it), 100, 0);
五、iostream 迭代器:流迭代器都是类模板:任何已定义输入操作符(>> 操作符)的类型都可以定义 istream_iterator。类似地,任何已定义输出操作符(<< 操作符)的类型也可定义 ostream_iterator。在创建流迭代器时,必须指定迭代器所读写的对象类型.
1.istream_iterator:用于读取输入流istream_iterator<T> in(strm);创建从输入流 strm 中读取 T 类型对象的 istream_iterator 对象
istream_iterator<T> in; istream_iterator 对象的超出末端迭代器
it1 == it2 it1 != it2; 比较两上 istream_iterator 对象是否相等(不等)。迭代器读取的必须是相同的类型。如果两个迭代器都是 end 值,则它们相等。对于两个都不指向流结束位置的迭代器,如果它们使用同一个输入流构造,则它们也相等
*it; 返回从流中读取的值
it->mem; 是 (*it).mem 的同义诩。返回从流中读取的对象的 mem 成员
++it it++; 通过使用元素类型提供的 >> 操作从输入流中读取下一个元素值,使迭代器向前移动。通常,前缀版本使用迭代器在流中向前移动,并返回对加 1 后的迭代器的引用。而后缀版本使迭代器在流中向前移动后,返回原值
2.ostream_iterator :则用于写输出流
ostream_iterator<T> in(strm); 创建将 T 类型的对象写到输出流 strm 的 ostream_iterator 对象
ostream_iterator<T> in(strm, delim); 创建将 T 类型的对象写到输出流 strm 的 ostream_iterator 对象,在写入过程中使用 delim 作为元素的分隔符。delim 是以空字符结束的字符数组
用法示例:
istream_iteratorcin_it(cin); // reads ints from cin istream_iterator end_of_stream; // end iterator value // initialize vec from the standard input: vector vec(cin_it, end_of_stream); sort(vec.begin(), vec.end()); // writes ints to cout using " " as the delimiter ostream_iterator output(cout, " "); // write only the unique elements in vec to the standard output unique_copy(vec.begin(), vec.end(), output);
3.流迭代器的限制:
A.不可能从 ostream_iterator 对象读入,也不可能写到 istream_iterator 对象中。
B.一旦给 ostream_iterator 对象赋了一个值,写入就提交了。赋值后,没有办法再改变这个值。此外,ostream_iterator 对象中每个不同的值都只能正好输出一次。
C.ostream_iterator 没有 -> 操作符
六、反向迭代器:反向迭代器是一种反向遍历容器的迭代器。也就是,从最后一个元素到第一个元素遍历容器。反向迭代器将自增(和自减)的含义反过来了:对于反向迭代器,++ 运算将访问前一个元素,而 -- 运算则访问下一个元素。
1.容器定义了 rbegin 和 rend 成员,分别返回指向容器尾元素和首元素前一位置的反向迭代器。与普通迭代器一样,反向迭代器也有常量(const)和非常量(nonconst)类型。
2.对于反向迭代器,++ 运算将访问前一个元素,而 -- 运算则访问下一个元素。虽然颠倒自增和自减这两个操作符的意义似乎容易使人迷惑,但是它让程序员可以透明地向前或向后处理容器。例如,为了以降序排列 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());3.反向迭代器需要使用自减操作符:标准容器上的迭代器既支持自增运算,也支持自减运算。但是,流迭代器却不然,由于不能反向遍历流,因此流迭代器不能创建反向迭代器。4.普通迭代器与反向迭代器之间的关系是为了适应左闭合范围这个性质的,所以,对于vector line, [line.rbegin(), rcomma) 和 [rcomma.base(), line.end()) 标记的是 line 中的相同元素。
七、const迭代器:使用const迭代器则无法用来修改容器中的元素。1.同时要注意const迭代器在使用过程中的细微差别:用来指定范围的两个迭代器,如果一个该容器的const迭代器,另一个是普通的迭代器,则无法编译通过,因为两个迭代器的类型不同。2.如果该容器是 const 对象,则返回的迭代器是 const_iterator 类型;否则,就是普通的 iterator 类型。
八、五种迭代器:根据算法要求它的迭代器提供什么类型的操作,对算法分类。算法要求的迭代器操作分为五个类别:Input iterator(输入迭代器): 读,不能写;只支持自增运算;相等和不等操作符(==,!=);解引用操作符;
Output iterator(输出迭代器) 写,不能读;只支持自增运算;解引用操作符;
Forward iterator(前向迭代器) 读和写;只支持自增运算;相等和不等操作符(==,!=);解引用操作符;
Bidirectional iterator(双向迭代器) 读和写;支持自增和自减运算;相等和不等操作符(==,!=);解引用操作符;
Random access iterator(随机访问迭代器) 读和写;关系操作符 <、<=、> 和 >= ;支持自增和自减运算;相等和不等操作符(==,!=);解引用操作符;下标操作符 iter[n] ;两个迭代器之间的减法操作符(--),得到两个迭代器间的距离 ;迭代器与整型数值 n 之间的加法和减法操作符 +、+=、- 和 -=,结果是迭代器在容器中向前(或退回)n 个元素。
1.在处理算法时,最好将关联容器上的迭代器视为支持自减运算的输入迭代器,而不是完整的双向迭代器。
2.向算法传递无效的迭代器类别所引起的错误,无法保证会在编译时被捕获到
九、泛型算法的结构:
1.根据对元素的操作将算法分为下面几种:
A.只读算法,不改变元素的值顺序。
B.给指定元素赋新值的算法。
C.将一个元素的值移给另一个元素的算法。
C++ 还提供了另外两种算法模式:一种模式由算法所带的形参定义;另一种模式则通过两种函数命名和重载的规范定义。
2.算法的形参模式:大多数算法采用下面四种形式之一:
alg (beg, end, other parms); alg (beg, end, dest, other parms);//dest 形参是一个迭代器,用于指定存储输出数据的目标对象。算法假定无论需要写入多少个元素都是安全的。 alg (beg, end, beg2, other parms); alg (beg, end, beg2, end2, other parms);//算法同时使用 beg2 和 end2 时,这些迭代器用于标记完整的第二个范围:带有 beg2 而不带 end2 的算法将 beg2 视为第二个输入范围的首元素,但没有指定该范围的最后一个元素。这些算法假定以 beg2 开始的范围至少与 beg 和 end 指定的范围一样大。3.区别带有一个值或一个谓词函数参数的算法版本:A.这些算法通常要用到标准关系操作符:== 或 <。其中的大部分算法会提供第二个版本的函数,允许程序员提供比较或测试函数取代操作符的使用.B.重新对容器元素排序的算法要使用 < 操作符。这些算法的第二个重载版本带有一个额外的形参,表示用于元素排序的不同运算:sort (beg, end); // use < operator to sort the elements sort (beg, end, comp); // use function named comp to sort the elementsC.检查指定值的算法默认使用 == 操作符。系统为这类算法提供另外命名的(而非重载的)版本,带有谓词函数形参。带有谓词函数形参的算法,其名字带有后缀 _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 true4.区别是否实现复制的算法版本:无论算法是否检查它的元素值,都可能重新排列输入范围内的元素。在默认情况下,这些算法将重新排列的元素写回其输入范围。标准库也为这些算法提供另外命名的版本,将元素写到指定的输出目标。此版本的算法在名字中添加了 _copy 后缀:reverse(beg, end); reverse_copy(beg, end, dest);十、容器特有的算法:list 容器上的迭代器是双向的,而不是随机访问类型。由于 list 容器不支持随机访问,因此,在此容器上不能使用需要随机访问迭代器的算法。这些算法包括 sort 及其相关的算法。还有一些其他的泛型算法,如 merge、remove、reverse 和 unique,虽然可以用在 list 上,但却付出了性能上的代价。如果这些算法利用 list 容器实现的特点,则可以更高效地执行。1.list 容器特有的算法与其泛型算法版本之间有两个至关重要的差别。其中一个差别是 remove 和 unique 的 list 版本修改了其关联的基础容器:真正删除了指定的元素。例如,list::unique 将 list 中第二个和后续重复的元素删除出该容器。与对应的泛型算法不同,list 容器特有的操作能添加和删除元素。2.另一个差别是 list 容器提供的 merge 和 splice 运算会破坏它们的实参。使用 merge 的泛型算法版本时,合并的序列将写入目标迭代器指向的对象,而它的两个输入序列保持不变。但是,使用 list 容器的 merge 成员函数时,则会破坏它的实参 list 对象——当实参对象的元素合并到调用 merge 函数的 list 对象时,实参对象的元素被移出并删除。3.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 删除同一个值的团结副本。第一个版本使用 == 操作符判断元素是否相等;第二个版本则使用指定的谓词函数实现判断