泛型算法
泛型算法不会执行容器的操作,它们只是运行于迭代器之上,并执行迭代器的操作。算法永远不会改变底层容器的大小,会对其遍历、查找、移动,但是永远不会直接进行删除或者添加元素。
初始泛型算法
标准库中提供了超过100个算法,我们不需对其进行死记硬背,它们大部分都有一些共同特点:处理一定范围内的数据,前两个参数为迭代器表示范围,称之为输入范围;但是它们使用数据的方式不同,是这些算法的主要区别。
只读算法
- find:三个参数,前两个表示查找范围(迭代器),最后一个为比较的值,如果找到返回该值第一次出现的迭代器位置,未找到则返回第二个迭代器,可以用之进行比较,判断是否找到。
- count:三个参数,前两个表示范围(迭代器),最后一个为待计数的值,返回在输入范围内值出现的次数。
- accumulate:三个参数,前两个表示累和范围(迭代器),最后一个表示和的初值。
- equal:三个参数,都是迭代器,前两个表示第一个容器的范围,第三个为待比较第二个容器的起始位置迭代器,并且第二个指向的容器至少应该和第一个容器一样大,确保算法不会访问第二个序列中不存在的元素。
写容器元素的元素
一些算法将新值赋予序列中的元素,当我们使用该类算法时应确保序列原大小至少不小于我们要求算法写入的元素数目。
- fill(c.begin(),c.end(),0):前两个迭代器指定范围,第三个参数指定替换的值。
- fill_n(c.begin(),c.size(),value):第一个迭代器指示替换的起始位置,第二个数指示替换的位数,第三个为替换成的值。
- back_inserter:接受一个容器的引用,返回一个与该容器绑定的插入迭代器。
vector<int> vec;
auto it=back_inserter(vec);
*it = 42;//vec中现在有一个元素42
- 拷贝算法:copy(c.begin(),c.end(),d)//将c中内容拷贝到d中,确保d至少和c的大小相同。copy返回的是指向d的最后一个元素下一个位置的迭代器。
重排容器元素的算法
sort:两个参数,都是迭代器,对两个迭代器内的元素进行排序,使用定义的<运算符。
unique:去除重复的元素,但是不会真的会删除容器内的元素,因为算法是不能直接操作容器进行删减的,只是将重复的元素移动到容器尾部,但是具体是什么不知道,最后还是要使用erase
来删除一定范围内的元素。
一个简单的程序进行重排演示:
//使用标准库算法对一段文本进行去重排序
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;
void elimDups(vector<string> &v){
sort(v.begin(),v.end());
cout<<"排序结果为: ";
for(auto i:v){
cout<<i<<" ";
}
cout<<endl;
auto unique_sort=unique(v.begin(),v.end());
v.erase(unique_sort,v.end());
cout<<"去除重复单词后排序为: ";
for(auto j:v){
cout<<j<<" ";
}
cout<<endl;
}
int main(){
vector<string> words={"the","quick","red","fox","jumps","over",
"the","slow","red","turtle"};
elimDups(words);
return 0;
}
定制操作
向算法传递函数
有关谓词的一些定制操作,谓词是一个可调用的表达式,返回结果是一个能作为条件的值,接受谓词参数的算法对输入序列中的元素调用谓词,因此元素类型必须能够转换为谓词的参数类型。
lambda表达式
一个lambda表达式表示一个可调用的代码单元,我们可以将其理解为未命名的内联函数,形式和一般的函数类似形式如下:
[capture list](parameter list)->return type{ function body}
可以忽略参数列表和返回类型,但是捕获列表和函数体必须包括。lambda必须使用尾置返回,捕获列表中指明该lambda表达式要使用的局部变量。
- 不存在默认参数,形参个数和实际传入参数个数必须相等,如果没指定返回类型则根据return表达式推断,否则则为void。
- 使用捕获列表:只有在捕获列表中指明的局部变量才能在lambda表达式中进行使用
lambda捕获和返回
- 值捕获
- 引用捕获:lambda表达式返回的是对一个对象的引用,注意方式和函数参数中的引用相同,避免产生局部变量的返回引用,避免对IO对象的引用,要注意IO对象是不允许拷贝和引用的。
- 隐式捕获:在使用隐式捕获时只需要在捕获列表中进行声明,
&或者=
,前者表示使用引用捕获,后者表示值捕获,由编辑器自己推断捕获列表中的内容。 - 可变lambda:一般由捕获列表的得到的值捕获,是一种拷贝是不允许进行改变的,如果想要改变,需要在参数列表尾添加
mutable
关键字,具体示例如下:
void f(){
size_t value=42;
//f1将可以改变value的值
auto f1=[value]()mutable{return ++value;};
}
而对于一个引用型捕获能否改变则取决于该引用指向的位置是一个常量引用还是一个普通引用。
- 返回值:对于返回值的问题,如果只有一个单一的返回语句,将根据实际情况进行返回,但是如果存在一个return语句之外的语句,默认返回是void,将不能返回具体的内容;如果需要指定返回类型,则需要使用尾置返回类型。
void f2(){
int val=3;
auto test_v=[val]()->int{if val>2 return val;else return val-1;}
- 参数绑定:由于之前说过一个检查大小的函数需要传入两个参数,字符串和检查的大小,但是find_if接受的为一元谓词因此只能使用lambda完成该工作,现在可以使用bind函数(定义在functional头文件中),完成此类工作:
bind(function,arglist)。具体使用如下:
auto check6=bind(check_size,_1,6)
string ss="hello";
check6(ss);//等价于调用check_size(ss,6)
也就是返回的为一个可调用对象,使用形如_1、_2…作为占位符,同时将后续的一个参数绑定到一个可调用对象上。
名字**_n都定义在名字空间placehoders中,该名字空间定义在std中,但是如果要使用它必须两个名字空间都要进行包含。
bind进行参数重排,可以使用形如auto g=bind(f,a,b,_2,c,_1)
这样在调用时g(5,6)就相当于调用f(a,b,6,c,5),这就是所谓的参数重排与绑定。但是需要注意的是这种绑定是一种拷贝的形式,会把绑定的值进行拷贝然后返回一个对象,如果需要绑定的参数不能进行拷贝或者不希望进行拷贝,则需要使用ref**函数(定义在functional头文件中),使用ref(os)
,返回一个对象,包含给定的引用,这是一个可以拷贝的对象。
迭代器再探
插入迭代器
三种类型:back_iterater、front_iterater、inserter分别创建使用push_back、push_front、insert的迭代器。?
流迭代器
istream_iterator:指定要读写的对象类型,如果进行默认初始化则是初始化了一个尾置迭代器,也可以直接进行初始化,如istream_iterator<int> val(cin)
.
ostream_iterator:?
反向迭代器
除了forward_list容器之外都存在反向迭代器,从容器的尾部开始,用v.crbegin()和v.crend()来获取两个迭代器,不带c的为非const版本。另外也不能在一个流迭代器上创建反向迭代器,反向迭代器必须保证容器支持++和–运算符。
另外,如果我们使用的是反向迭代器从一个函数中返回的也将是一个反向迭代器,比如以下代码:
//反向找到第一个‘,’,如果不存在则返回v.crend(),存在则返回第一个逗号的反向迭代器
auto rcomma=find(v.crbegin(),v.crend(),',');
//将逆序输出单词
cout<<string(v.crbegin(),rcomma)<<endl;
//如果想要顺序输出,需要使用reverse_iterator中的
//base成员函数对其进行转换为普通的迭代器
cout<<string(rcomma.base(),v.end())<<endl;//顺序输出单词
反向迭代器和其对应的普通迭代器指向的位置是不同的,但是都保证了左闭合区间的特性,最后使得正向读取和反向读取时实际的范围是相同的。
泛型算法的结构
5类迭代器
算法形参模式
算法命名规范
特定容器算法
list和forward_list有很多成员函数版本的算法,对于存在成员函数版本的算法,应优先使用成员函数版本的如下所示:
那些特有版本的方法和通用版本的根本区别是:通用版本不会改变操作的容器,比如merge在通用版本中会将原来的一个容器中元素和另一个容器中的元素进行融合然后到一个目的容器中,但是原容器是不会改变的,而链表特有的版本会改变底层容器内的内容,删除原容器中内容。