顺序容器只定义了:添加和删除元素、访问首尾元素、确定容器是否为空以及获得指向首元素或尾元素之后位置的迭代器的操作,而对于查找特定元素、替换或删除一个特定值、重排元素顺序等操作,需要泛型算法来实现。
1.概述
大多数算法定义在头文件algorithm中。标准库还在头文件numeric中定义了一组数值泛型算法。
一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。
例:find在数组中查找值:
迭代器令算法不依赖于容器,但依赖于元素类型的操作(如==运算符、<运算符)。此特性基于一个非常重要的编程假定:算法永远不会改变底层容器的大小。对于插入器,后续详细讲解。
2.初识泛型算法
标准库提供了超过100个算法,它们大都通过接受首元素和尾元素之后位置的迭代器来对一个范围内的元素进行操作。理解算法的最基本方法就是了解它们是否读取元素、改变元素或重排元素顺序。
(1)只读算法
find(cb,ce,val)
count(cb,ce,val)
accumulate(cb,ce,和的初值(决定了和的类型)) //求和
对于只读取而不改变元素的算法,通常最好用cbegin()和cend()。
-
算法和元素类型
accumulate的第三个参数要求:将元素类型加到和的类型上的操作必须是可行的。否则会产生编译错误。 -
操作两个序列的算法
equal(cb1,ce1,cb2) ,假定第二个序列至少与第一个序列一样长。 //判断两个序列保存的值相等
(2)写容器元素的算法
要确保序列原大小至少不小于我们要求算法写入的元素数目,因为算法不能改变底层容器的大小。
fill(beg,end,val) //将范围内的数都设为val
-
算法不检查写操作
fill_n(dest,n,val) //将范围内的数都设为val
要保证从dest开始的序列至少包含n个元素。 -
介绍back_inserter
一种保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器。
back_inserter是定义在头文件iterator中的一个函数。back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。
①当我们使用此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中:
②常常使用back_inserter来创建一个迭代器,作为算法的目的位置来使用。
-
拷贝算法
①copy(beg,end,dest) //从输入范围将元素拷贝到dest指定的目的序列,返回其目的位置迭代器(递增后)的值。
传递给copy的目的序列至少要包含与输入序列一样多的元素。
②replace(beg,end,old_val,new_val) //替换
replace_copy(beg,end,dest,old_val,new_val) //拷贝并替换
(3)重排容器元素的算法
sort(beg,end) //排序,基于<运算符
- 消除重复元素
unique(beg,end) //重排输入序列通过覆盖来“删除”相邻的重复元素。返回指向不重复元素的尾后位置的迭代器。
3.定制操作
提供自己定义的操作来代替默认运算符。
(1)向算法传递函数
sort(beg,end,comp) //排序,基于自己定义
stable_sort(beg,end,comp) //排序,基于自己定义,而且可保证相等元素的原本相对次序在排序后保持不变。
partition(beg,end,unaryPred) //划分,满足unaryPred的元素放在序列开始,不满足的元素放在尾部。返回一个迭代器,指向最后一个满足unaryPred的元素之后的位置。
-
谓词
谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。一元谓词,二元谓词。
-
排序算法
例:单词按其长度排序,大小相同的再按字典序排列。
例:划分长度大于5的单词并输出:
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
bool cmp5(string &s) {
return s.size() >= 5;
}
int main()
{
vector<string> words = { "fox", "jumps", "over", "quick", "red", "slow", "the", "turtle" };
auto pend = partition(words.begin(), words.end(), cmp5);
//打印
for (auto iter = words.begin(); iter < pend; ++iter) {
cout << *iter << endl;
}
system("pause");
return 0;
}
(2)lambda表达式
除一元谓词和二元谓词外,有时我们希望进行的操作需要更多参数。
- 介绍lambda
可调用对象:对于一个对象或一个表达式,如果可以对其使用调用运算符,则称它为可调用的。
四种可调用对象:函数、函数指针、重载了函数调用运算符的类、lambda表达式。
一个lambda表达式表示一个可调用的代码单元,我们可以将其理解为一个未命名的内联函数。与任何函数类似,一个lambda具有一个返回类型、一个参数列表和一个函数体。但与函数不同,lambda可能定义在函数内部。
基本形式:[capture list] (parameter list) -> return type { function body }
capture list(捕获列表) 是一个lambda所在函数中定义的局部变量的列表(通常为空)。只对lambda所在函数中定义的(非static)变量使用捕获列表。lambda可以直接使用局部static变量和定义在当前函数之外的名字。
lambda必须使用尾置返回来指定返回类型。lambda不能有默认参数。
可以忽略参数列表(忽略括号和参数列表等价于指定一个空参数列表)和返回类型(忽略返回类型,lambda根据函数体中的代码推断出返回类型,根据return推断或void),但必须永远包含捕获列表和函数体。例:
//定义一个可调用对象f,它不接受参数,返回42
auto f = [] { return 42; };
//调用
cout << f() << endl;
-
向lambda传递参数
与普通函数调用类似,调用一个lambda时给定的实参被用来初始化lambda的形参。
-
使用捕获列表
空捕获列表表明此lambda不使用它所在函数中的任何局部变量。lambda通过将局部变量包含在其捕获列表中来指出将会使用这些变量。
-
调用find_if
find_if(beg,end,unaryPred) //查找第一个满足条件的元素,返回第一个满足条件的迭代器。 -
for_each算法
for_each(beg,end,unaryOp) //对输入序列中的每个元素应用可调用对象unaryOp。
例:求大于等于一个给定长度的单词有多少:
//P201:make_plural
string make_plural(size_t ctr, const string &word, const string &ending) {
return (ctr > 1) ? word + ending : word;
}
//将words按字典序排序,删除重复单词
void elimDups(vector<string> &words) {
sort(words.begin(), words.end());
auto end_unique = unique(words.begin(), words.end());
words.erase(end_unique, words.end());
}
void biggies(vector<string> &words, vector<string>::size_type sz) {
elimDups(words); //按字典序排序,去重
//按长度排序,长度相同的按字典序排序
stable_sort(words.begin(), words.end(),
[](const string &a, const string &b)
{return a.size() < b.size(); });
//获取第一个满足size>=sz元素的迭代器
auto wc = find_if(words.begin(), words.end(),
[sz](const string &a)
{return a.size() >= sz; });
//计算满足size>=sz的元素数目
auto count = words.end() - wc;
cout << count << " " << make_plural(count, "word", "s") << " of length " << sz << " or longer" << endl;
//打印
for_each(wc, words.end(), [](const string &s) {cout << s << " "; });
cout << endl;
}
(3)lambda捕获和返回
当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。
默认情况下,从lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员。lambda的数据成员也在lambda对象创建时被初始化。
-
值捕获
与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。
-
引用捕获
如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的。
我们也可以从一个函数返回lambda。如果函数返回一个lambda,则与函数不能返回一个局部变量的引用类似,此lambda也不能包含引用捕获。
尽量减少捕获的数据量,尽量避免捕获指针或引用! -
隐式捕获
在捕获列表中写一个&(引用捕获)或=(值捕获),让编译器根据lambda体中的代码来推断我们要使用哪些变量。
可以混合使用显式捕获和隐式捕获。捕获列表中的第一个元素必须是一个&或=,显式捕获的变量必须使用与隐式捕获不同的方式。 -
可变lambda
auto f = [t](){return ++t; }; //报错:表达式必须是可修改的左值
默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。在参数列表尾加上关键字mutable可以改变被捕获变量的值。
[capture list] (parameter list) mutable -> return type { function body }
size_t t = 9;
auto f = [t]() mutable {return ++t; };
cout << f() << endl; //10
cout << f() << endl; //11
cout << "t:" << t << endl; //9
t = 0;
cout << f() << endl; //12
cout << f() << endl; //13
cout << "t:" << t << endl; //0
一个引用捕获的变量是否可修改依赖于此引用指向的是否是const类型。
- 指定lambda返回类型
单一的return语句可以推断出来返回类型。默认情况下,如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void。
当我们需要为一个lambda定义返回类型时,必须使用尾置返回类型。
[](int i) {return i < 0 ? -i : i; }; //正确,推断返回int
[](int i) { if (i < 0) return -i; else return i; }; //返回void
[](int i) -> int{ if (i < 0) return -i; else return i; }; //正确,定义返回int
(4)参数绑定
如果lambda的捕获列表为空,通常可以用函数来代替它;但是对于捕获局部变量的lambda,用函数来替换它就不是那么容易了。
例:find_if调用中的lamdma比较一个string和一个给定大小,可以编写成如下函数:
bool check_size(const string &s, string::size_type sz) {
return s.size() >= sz;
}
但是,我们不能用这个函数作为find_if的一个参数。为了用check_size来代替此lambda,必须解决如何向sz形参传递一个参数的问题。
-
标准库bind函数
标准库函数bind,定义在头文件functional中。可以将bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
基本形式:auto newCallble = bind ( callble , arg_list );
其中,newCallble 本身是一个可调用对象,arg_list 是一个逗号分隔的参数列表,对应给定的callble参数。即,当我们调用newCallble 时,会调用callble,并传递给它arg_list 中的参数。
arg_list 中的参数可能包含形如_n的名字,其中n是一个整数。这些参数是“占位符”,表示newCallble 的参数,它们占据了传递给newCallble 的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1为newCallble 的第一个参数,_2为第二个参数。 -
使用placeholders名字
名字_n都定义在一个名为placeholders的命名空间中,而这个命名空间本身定义在std命名空间。为了使用这些名字,两个命名空间都要写上:
using std::placeholders::_1;
using std::placeholders::_2;
可以使用另外一种不同形式的using语句,而不是分别声明每个占位符:
using namespace namespac_name;
using namespace std::placeholders;
与bind函数一样,placeholders命名空间也定义在functional头文件中。
- 绑定check_size的sz参数
使用bind生成一个调用check_size的对象:
#include <iostream>
#include <string>
#include <functional>
using namespace std;
using namespace std::placeholders;
bool check_size(const string &s, string::size_type sz) {
return s.size() >= sz;
}
int main()
{
//check6是一个可调用对象,接受一个string类型的参数
//并用此string和值6来调用check_size:
auto check6 = bind(check_size, _1, 6);
string s = "hello";
bool b1 = check6(s);
system("pause");
return 0;
}
此bind调用只有一个占位符,表示check6只接受单一参数。占位符出现在arg_list的第一个位置,表示check6的此参数对应check_size的第一个参数。
所以,find_if可以写为:
auto wc = find_if(word.begin(), words.end(), bind(check_size, _1, sz));
从而实现string的大小与sz的比较。
-
bind的参数
我们可以用bind修正参数的值,也可以用bind绑定给定可调用对象中的参数或重新安排其顺序。 -
用bind重排参数顺序
-
绑定引用参数
默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。但是,与lambda类似,有时对有些绑定的参数我们希望以引用方式传递,或是要绑定参数的类型无法拷贝。
如果我们希望传递给bind一个对象而又不拷贝它,就必须使用标准库ref函数(定义在头文件functional中),返回包含给定的引用的对象,此对象是可以拷贝的。标准库中还有一个cref函数,生成一个保存const引用的类。
4.再探迭代器
除了为每个容器定义的迭代器之外,标准库在头文件iterator中还定义了额外几种迭代器:
- 插入迭代器:绑定到容器上,可用来向容器插入元素;
- 流迭代器:绑定到输入或输出流上,可用来遍历所关联的IO流;
- 反向迭代器:迭代器向后而不是向前移动,除了forward_list之外的标准库容器都有反向迭代器;
- 移动迭代器:移动其中的元素,后面介绍。
(1)插入迭代器
插入器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。当我们通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。
deque<int> nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
deque<int> deq1, deq2, deq3;
//back_inserter:1 2 3 4 5 6 7 8 9
copy(nums.begin(), nums.end(), back_inserter(deq1));
//注:不能对vector使用front_inserter,因为只有list,forward_list和deque支持push_front操作
//front_inserter:9 8 7 6 5 4 3 2 1
copy(nums.begin(), nums.end(), front_inserter(deq2));
//inserter:1 2 3 4 5 6 7 8 9
copy(nums.begin(), nums.end(), inserter(deq3, deq3.begin()));
(2)iostream迭代器
虽然iostream不是容器,但标准库定义了可以用于这些IO类型对象的迭代器。istream_iterator和ostream_iterator将它们对应的流当作一个特定类型的元素序列来处理。通过使用流迭代器,我们可以用泛型算法从流对象读取数据以及向其写入数据。
-
istream_iterator操作
①使用算法操作流迭代器
②istream_iterator允许使用懒惰求值 -
ostream_iterator操作
-
使用流迭代器处理类类型
(3)反向迭代器
反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。除了forward_list之外,其他容器都支持反向迭代器。我们可以通过调用rbegin、rend、crbegin和crend成员函数来获得反向迭代器。这些成员函数返回指向容器尾元素和首元素之前一个位置的迭代器。
- 反向迭代器需要递减运算符
- 反向迭代器和其他迭代器间的关系
通过调用reverse_iterator的base成员函数来完成转换,返回其对应的普通迭代器:
5.泛型算法结构
任何算法的最基本特性是它要求其迭代器提供哪些操作。算法按所要求的迭代器操作可以分为5个迭代器类别,每个算法都会对它的每个迭代器参数指明须提供哪类迭代器。
(1)5类迭代器
类似容器,迭代器也定义了一组公共操作。一些操作所有迭代器都支持,另外一些只有特定类别的迭代器才支持。例如,ostream_iterator只支持递增、解引用和赋值。vector、string和deque的迭代器除了这些操作外,还支持递减、关系和算术运算。
- 迭代器类别
(2)算法形参模式
在任何其他算法分类之上,还有一组参数规范。大多数算法具有如下4种形式之一:
- 接受单个目标迭代器的算法
- 接受第二个输入序列的算法
(3)算法命名规范
-
一些算法使用重载形式传递一个谓词
-
_if版本的算法
-
区分拷贝元素的版本和不拷贝元素的版本
6.特定容器算法
链表类型list和forward_list
链表特有的操作会改变底层的容器。