《C++ Primer》第10章:泛型算法 学习笔记总结

来源:互联网 发布:手机角度水平仪软件 编辑:程序博客网 时间:2024/06/06 00:16

概述

大多数算法定义在#include<algorithm>或者#include<numeric>中。迭代器算法不能依赖于容器,泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作,结果:算法永远不能改变底层容器的大小。算法可以修改容器里面的值,移动里面的元素,就是不能添加删除元素。虽然有些插入迭代器可以增删元素,当算法操作这样的迭代器时,迭代器可以完成增删效果,但是算法不能。

10.2.1只读算法

大部分标准库算法都对一个范围内的元素进行操作,我们将元素范围称为输入范围。算法的前两个参数来表示此范围,分别是迭代器。
只读算法特点:只会读取输入范围的元素,而从不改变它们。

find()

find(begin, end, val);  


accumulate()

accumulate(begin, end, initial_result);  

在头文件#include<numeric>中,前两个参数是求和的元素的范围,第三个参数是和的初值,第三个参数决定了一个假设:将元素类型加到和的类型上的操作必须是可行的:即序列中的元素的类型必须与第三个参数相匹配,或者能转换为第三个参数的类型。string类型时药特别注意了,第三个参数要是string类型,因为历史原因以及为了与 c 语言兼容,字符串字面值与标准库 string 类型不是同一种类型。字符串字面值时 const char* 类型,没有+运算符。http://blog.csdn.net/bit452/article/details/41075859 有一些说明


equal()

equal(a.begin(),a,end(), b.pos);  

可以比较两种不同类型容器中元素,元素类型也可以不一样,只要能用==比较即可。eg:可以是vector和list比较。用于确定两个序列是否保存相同的值。算法接受三个迭代器:前两个表示第一个序列中的元素范围,第三个表示第二个序列的首元素。将第一个序列的每一个元素和第二个序列的每一个对应元素进行比较,所以有个前提:第二个序列至少要与第一个序列一样长。如果第一个序列长度小于第二个序列,那也没有比较的意义了。


可以明显看出来:第二个序列的长度可以大于第一个序列,第三个参数也可以灵活调整位置。


总结:对于只读算法,只读取元素而不改变元素,最好使用cbegin()和cend()。

10.2.2写算法

算法将新值赋予序列中的元素,必须注意算法不会执行容器操作,它们不能改变容器大小,所以容器大小至少不小于算法写入的元素数目。

fill()

fill(begin, end, initail); 

fill_n(begin, num, initial); 

fill接受一对迭代器表示一个范围,接受一个值作为第三个参数,fill将给定的这个值赋予输出序列中的每个元素。

fill_n接受一个单迭代器,一个计数值,和一个值,它将给定值赋予迭代器指向的元素开始的指定个数。

千万注意的是:算法不能改变容器大小,所以编程之前要设置合理的容器大小,使容器大小足够大,容纳写入的元素。


back_inserter

插入迭代器:是一种向容器中添加元素的迭代器。back_inserter是定义在#include<iterator>中的一个函数。它接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器,当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中。所以我们的算法并没有执行容器操作,算法只是返回了一个迭代器,push_back才是改变了容器的容量。


拷贝

拷贝算法是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法。参数是三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置。

*注意*:传递给copy的目的序列至少要包含与输出序列一样多的元素。

auto it=copy(begin(a),end(a),begin(b));返回的是其目的位置迭代器(递增后)的值。

replace(begin(),end(),old_val,new_val);


10.2.3重排容器元素的算法

排序算法(<):sort(c.begin( ), c.end( )); 

消除重复算法:auto end_unique= unique(c.begin( ), c.end( ));  //并不改变容器的大小。unique并不真正删除元素,容器大小没有变化,unique返回的迭代器指向最后一个不重复之后的位置,此位置之后的元素仍然存在,但我们不知道它的值是什么。

删除元素算法:a.erase(c.begin(),c.end());//改变容器大小,直接将元素删除。


10.3.1向算法传递函数

sort算法默认内部是<比较的(从小到大),当希望按照其他顺序(>或长度)比较就要向算法传递参数。

谓词:谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。

谓词分为两类:一元谓词和二元谓词,分别是接受一个参数和两个参数。

接受谓词参数的算法对输出序列中的元素调用谓词,因此元素类型必须能转换成谓词的参数类型。

elimDups(words):将words按字典序重排,并消除重复单词


10.3.2lambra表达式

一个lambda表达式表示一个可调用的代码单元,我们可以将其理解为一个未命名的内联函数。和任何函数类似,一个lambda具有一个返回类型,一个参数列表和一个函数体,但和函数不同,lambda可能定义在函数的内部。

形式:[捕获列表](参数列表) -> 返回类型 { 函数体 }

捕获列表:是一个lambda所在函数中定义的局部变量的列表(通常为空)。返回类型使用尾置返回类型,其他和普通函数一样。可以忽略参数列表和返回类型(等价指定一个空的),但是必须永远包含捕获列表和函数体。

向lambda传递参数

lambda不能含有默认参数,形参实参类型要匹配。
空捕获列表表示不用所在函数里面的任何局部变量。
编写例子:[](const string&s1, const string&s2) { return s1.size( ) >= s2.size( ); }
调用:
stabel_sort(words.begin(),words.end(),[](const stirng &a,const string &b){return a.size()

使用捕获列表

虽然一个lambda可以出现在一个函数中,使用其局部变量,但他只能使用那些明确指名的变量。一个lambda通过将局部变量包含在其捕获列表中指出将来会使用这些变量。捕获列表指引lambda在其内部包含访问局部变量所需的信息。

[sz] (const string&s) { s.size() >= sz; };

find_if

可以查找第一个长度大于等于sz的元素,并返回一个指向此元素的迭代器。

auto wc = find_if(words.begin(), words.end(), [sz] (const string&s){ return s.size() >= sz; });  //获取一个迭代器,指向第一个满足的元素
若不存在,返回word.end()的一个拷贝

for_each

接受一个可调用的对象,并对输入序列中每个元素调用此对象。
for_each(wc,words.end(),[](const string &s){cout<<s<<" "});
捕获列表为空,是因为我们只对lambda所在函数中定义的(非static)变量使用捕获列表。捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和在它所在函数之外声明的名字。

lambda捕获和返回

捕获的两种方式:值或引用

值捕获:采用值捕获的前提是变量可以拷贝,和参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。
引用捕获:采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的,lambda捕获的都是局部变量,这些变量在函数结束后就不复存在了。如果lambda可能在函数结束后执行,捕获的引用指向的局部变量已经消失。此时再使用就出错了。引用捕获有时是必要的,如果我们的参数包含了流,但是流是不能被拷贝的,所以捕获os的唯一方法就是捕获引用(或指向os的指针)。
建议:尽量保持lambda的变量捕获简单化
1,捕获一个变量如int,string或其他的非指针类型,通常可以采用简单的值捕获方式,在此情况下,只需要关注变量在捕获时是否包含我们所需要的值就行了。
2,捕获一个指针或迭代器或流或我们选择了引用的捕获方式,就必须保证在lambda执行时,绑定到迭代器、指针或引用的对象存在。并且要保证对象具有预期的值。
我们应该尽可能减少捕获的数据量,来避免潜在的捕获导致的问题,而且,如果可能,应该避免捕获指针或引用。

隐式捕获

除了显示列出我们希望使用的所在函数的变量外,还可以让编译器根据lambda体中的代码来推断我们要使用哪些变量,在捕获列表写一个&或=,&告诉编译器采用引用方式捕获,=告诉编译器采用值传递方式捕获。当我们混用隐式捕获和显示捕获时,捕获列表的第一个元素必须是&或=。
当混合使用隐式捕获和显式捕获时,显式捕获的变量必须使用与隐式捕获不同的方法,即:如果隐式捕获是引用方式,则显式捕获命名变量必采用值方式。
捕获列表的几种形式
[ ]                             空捕获列表,所以lambda也不能使用所在函数中的变量
[ names]                  逗号分隔的名字列表,函数中定义变量。有值捕获和引用捕获两种。默认是值捕获,捕获列表中的变量都被拷贝,所以lambda不能改变捕获变量的值
[&]                           隐式引用捕获
[=]                           隐式值捕获
[&, identifier_list]     隐式变量采用引用捕获,值捕获采用列表形式
[=, identifier_list]     隐式变量采用值捕获,引用捕获采用列表形式

可变lambda

值捕获:默认情况下,对于一个被值拷贝的变量,lambda不会改变其值。(lambda的捕获变量是在创建时候被拷贝的,而不是调用时候)。若想通过lambda的函数体改变被捕获的变量的值,就要在参数列表后加上mutable。
引用捕获:不用加mutable可以直接修改绑定的变量值。

指定lambda返回类型

默认情况下,如果一个lambda体包含了return之外的任何语句,则编译器假定返回void。但有时默认推到类型可能是错误的。lambda定义返回类型时,必须使用尾置返回类型。
transform(ivec.begin(), ivec.end(), ivec.begin(),[](int i)->int { return i < 0 ? -i : i; });  

10.3.4参数绑定

对于那种只在一两个地方使用的简单操作,lambda表达式是最有用的。很复杂时通常还是使用函数更好。
1,如果lambda的捕获列表为空,通常可以函数来代替它。
2,如果lambda的捕获列表有局部变量,用函数来替换就不容易了。

标准库bind函数

一元谓词函数只能有一个参数,如果一个算法只接受一元谓词,但是传递两个参数才能完成,这个时候就不能直接用lambda。
bind()定义在#include<functional>.可以把bind函数看作一个通用的函数适配器,它接受一个可调用的对象,生成一个新的可调用的对象来适应原有的参数列表。
bind的一般形式:auto newCallable = bind(callable, arg_list);
newCallable本身是一个可调用的对象,arg_list是一个逗号分割的参数列表,对应给定callable的参数。
也就是说:当我们调用newCallable时,newCallable会调用callable,并传递给它arg_list中的参数。
arg_list可能包含形如_n的名字,n是一个整数,这些参数是占位符,表示了newCallable的参数,它们占据了传递给newCallable的参数的位置。
数值n表示生成的可调用对象中参数的位置。
auto check6 = bind(check_size, _1, 6);  //check是只接受一个参数的函数,那个6是check_size的参数
调用时:
string s="hello";
bool b1=check6(s);//check6会调用check_size(s,6)

bind的参数

我们可以使用bind修正参数的值,可以用bind绑定给定可调用中的参数或重新安排顺序。
auto g=bind(f,a,b,_2,c,_1);
过程是:  g(_1,_2),映射到   f(a,b,_2,c,_1)

绑定引用参数

和lambda类似,有时我们对有些绑定的参数希望以引用方式传递,或是绑定的参数无法拷贝,比如说流参数。
for_each(word.begin(), word.end(), bind(print, os, _1, " "));        //错误:os是流,我们不能拷贝一个流  for_each(word.begin(), word.end(), bind(print, ref(os), _1, " "));   //希望传递给bind一个对象而又不拷贝它,就必须使用ref函数。  
函数ref( )返回一个对象,包含它的引用,此对象是可以拷贝的。标准库还有cref函数,生成一个保存const 引用的类。

10.4再探迭代器

定义再头文件#include<iterator>,迭代器包含以下几种:
<1.插入迭代器
<2.流迭代器
<3.反向迭代器
<4. 移动迭代器                              

10.4.1插入迭代器

插入迭代器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。我们通过一个插入迭代器进行赋值时,带迭代器用容器操作来向指定位置插入元素。
插入迭代器有三种,差异在于元素插入的位置:
1.back_inserter   创建一个使用push_back的迭代器(容器必须支持push_back)
2.front_inserter  创建一个使用push_front的迭代器(容器必须支持push_front)
3.inserter        创建一个使用insert的迭代器,此函数接受两个参数,第二个参数必须是指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前。

反向迭代器

反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。
对于反向迭代器,iter++是向 后移动,iter--是向前移动。
反向迭代器也有const和非const版本。
除了forward_list和流以外,其他的容器都定义了反向迭代器。不可能在流中反向移动。

泛型算法结构

任何算法的最基本结构就是它要求迭代器提供哪些操作。

算法所要求的迭代器操作可以分为5个迭代器类别

输入迭代器:只读,不写,单遍扫描,只能递增

输出迭代器:只写,不读,单遍扫描,只能递增

前向迭代器:可以读写,多遍扫描,只能向前

双向迭代器:可以读写,多遍扫描,可以递增递减

随机访问迭代器:可读写,多遍扫描,支持全部迭代器运算。

大多数算法具有下面4种形式之一

alg(beg, end, other args);

alg(beg, end, des, other args);

alg(beg, end, beg2, other args);

alg(beg, end, beg2, end2, other args);

alg是算法的名字,beg和end表示算法所操作的范围,几乎所有的算法都接受一个输入范围,

是否有其他的参数依赖于要执行的操作。

des是目的位置,beg2end2是第二个范围。

特定容器的

比如通用版本的sort要求随机访问迭代器,因此不能用于list和forward_list。所以STL定义了他们单独的函数。

这些链表版本的性能比对应的通用版本好的多。

对于list和forward_list应该优先使用成员函数版本而不是通用算法。

链表特有版本会彻底改变底层的容器。


                                             
1 0
原创粉丝点击