笔记会持续更新,有错误的地方欢迎指正,谢谢!
概述
容器是一个模板类,是在数据类型之上的那一层。C++提供一组算法,独立于任何特定的容器,这些算法是类型无关的,是泛型(generic)的。
比如:find算法通过迭代器去依次访问元素,找到就停止,直到尾后元素,这些都不依赖于容器所保存的元素类型或者是容器类型,它就是迭代器去访问,也就是 迭代器让算法不依赖于容器。
算法依赖于元素类型的操作。因为算法在执行时肯定要操作元素,比如我们的find算法,它至少要使用==符号来判等,所以就要求元素类型支持 ==,这里的类型是int,当然支持了。(又比如:string支持加法。)
总结:算法只会运行于迭代器之上,迭代器让算法不依赖于容器,但依赖于元素类型的操作(加减乘除等等操作)。
初识泛型算法
我们学习的泛型算法都对一个范围内的元素进行操作,这个范围由两个参数表示,前一个指向要处理的第一个元素,后一个指向尾元素的后一个位置,用这两个迭代器参数表示左闭右开区间。
算法分类:是否读取元素、改变元素、重排元素顺序。
只读算法
只读取其输入范围内的元素,从不改变元素,最好用常量迭代器。例如之前的find函数。
例子:
int sum = accumulate(vec.cbegin(), vec.cend(), 0);
//accumulate函数是对容器内元素求和,这个算法要求容器内的元素类型支持加法。
再来一个例子,string也定义了加法,所以可以对string求和:
string sum = accumulate(vec.cbegin(), vec.cend(), string(""));
//泛型,跟上面一样
写容器元素的算法
举个例子:
fill(vec.begin(), vec.end(), 0);
//将所有元素重置为0
replace算法:
replace(list.begin(), list.end(), 0, 42); //把list中所有的0改为42
//函数重载:
replace(list.cbegin(), list.cend(), back_inserter(ivec), 0, 42);
//list不变。而ivec包含list的一份拷贝,并且ivec里的0都变成了42。
重排容器元素的算法
重排算法:去除容器重复元素的方法是,先调用重排算法sort和unique,再用容器操作erase。
假定要简化一个vector,使得里面保存的每个单词只出现一次:
//假设原来的vector为a, b, d, c, a, b
void Func(vector<string> &words)
{
sort(words.begin(), words.end());
//a,a,b,b,c,d
auto end_unique = unique(words.begin(), words.end());
//a,b,c,d,a,b
//end_unique的位置就是第一个重复的元素位置(即第二个a处)
words.eraser(end_unique, wors.end());
//a,b,c,d
定制操作
很多算法都会比较元素,默认情况下,这类算法使用元素类型的<或==运算符来完成比较,但允许我们传递任何类型的可调用对象,用自定义操作代替默认运算符。
有两种情况可能需要我们多做一些工作:
1. 我们希望的排序顺序与<定义的顺序不同;
2. 我们保存的元素没有定义<运算符。
在这两种情况下,我们需要重载sort的默认行为。
向算法传递参数
谓词:一个可调用的表达式,返回结果是一个能用作条件的值,谓词可以接受一个或两个参数,分别称为一元谓词和二元谓词。
例子:
stable_sort(words.begin(), words.end(), isShorter); //按长度排序
//用stable_sort()稳定排序是为了让长度相同的单词还是保持字典序不变。
标准库中有一个算法find_if用来查找第一个具有特定大小的元素,它接受三个参数,前两个是一对迭代器表示输入范围,第三个参数是一个一元谓词,find_if算法对输入序列中每个元素调用这个谓词,它返回第一个使谓词返回非0值的元素。 看着挺好用的,那么问题在哪呢?问题就在第三个参数是一元谓词,这个谓词函数肯定要传给它两个参数,一个string和一个长度,这就有问题了,因为人家是一元谓词,无法接受两个参数,所以,here comes lambda.
lambda(兰木达)表达式
一个lambda表达式表示一个可调用的代码单元,我们可以理解为未命名的内联函数。
[capture list] (parameter list) -> return type {function body}
依次为:[捕获列表](形参列表)->返回类型{函数体}
捕获列表是lambda表达式所在函数中定义的局部变量的列表。
我们可以忽略形参列表和返回类型,但必须包含捕获列表和函数体:
auto f = [] {return 42;}
//空捕获列表 表明此lambda不使用它所在的函数中的任何局部变量。
//定义了一个可调用对象f,不接受参数,也就没有参数的括号。返回42。
cout << f() << endl; //打印42
向lambda传递参数:
lambda不能有默认参数,这是规定。
作为一个带参数的lambda例子,我们来写一个与isShorter函数功能相同的lambda:
[] (const string &a, const string &b){ return a.szie() < b.szie();}
我们可以使用此lambda来调用stable_sort函数:
stable_sort(
words.begin(), word.end(),
[] (const string &a, const string &b){ return a.szie() < b.szie();}
);
使用捕获列表:
不要忘了我们为什么要引出lambda这个概念,我们现在就来解决这个问题:编写一个可以传递给find_if的可调用表达式,我们希望这个表达式能将输入序列中的每个string的长度与lambda所在函数中的sz参数进行比较。 我们是怎么来传递多余信息呢?答案就是捕获列表,一个lambda通过将局部变量包含在其捕获列表中来使用:
[sz] (const string &a){ return a.szie() >= sz; };
//sz是lambda表达式所在函数的变量,是lambda捕获来的。
使用这个lambda就可以搞定了:
auto wc = find_if(wors.begin(), wors.end(),
[sz] (const string &a){ return a.szie() >= sz; });
lambda捕获和返回
捕获方式:
捕获是一种传参方式,分为值捕获和引用捕获。
举例:
//值捕获,前提是变量可以被拷贝。
void f1()
{
size_t v1 = 24;
auto f = [v1] { return v1; };
v1 = 0;
auto j = f(); //j = 24;
}
//引用捕获,必须保证在捕获时该变量是存在的,是否可变由是否是常量引用而定。
void f2()
{
size_t v1 = 24;
auto f = [&v1] { return v1; };
v1 = 0;
auto j = f(); //j = 0;
}
指定lambda返回类型:
到目前为止,我们所编写的lambda都只有一个return语句,所以我们还没指定过返回类型。规定:默认情况下,如果一个lambda函数体包含return之外的任何语句,则编译器假定此lambda返回void。
例子:
transform(vi.begin(), vi.end(), vi.begin(),
[](int i){ return i < 0 ? -i : i; });
//我们再来个看起来跟上面差不多的,但是是错误的版本:
transform(vi.begin(), vi.end(), vi.begin(),
[](int i)
{
if(i<0){return -i;}
else{return i;}
});
报错,因为lambda函数体包含return之外的if语句!编译器推断该lambda返回类型为void,但它返回了int。 我们可以通过指定lambda返回类型来修正它:
transform(vi.begin(), vi.end(), vi.begin(),
[](int i) -> int //就是这样指定返回类型的
{
if(i<0){return -i;}
else{return i;}
});