10.2 初识泛型算法
10.2.1 只读算法
- find
头文件algorithm
find 在指定迭代器范围种,查找目标元素。返回第一个出现目标元素的迭代器。
支持容器类型,也支持内置数组。要求元素支持“==”运算符。
// 1. 在list中查找指定元素
list<string> lst;
// 向lst插入元素
// lst.push_fron("...");
// 利用find在lst中查找指定元素
string val = "a value";
find(lst.cbegin(), lst.end(), val);
// 2. 在内置数组中查找指定元素
int ia[] = {1,2,3,4,5,6,7};
int val = 6;
int* res = find(begin(ia), end(ia), val);
- accumulate
头文件numeric
accumulate 求累加和。返回求和结果。
除了指定迭代器范围,还需要指定初值(第三个参数)。运算时,元素会转换为初值类型。
要求元素支持“+”运算符。
int sum = accumulate(vec.cbegin(), vec.cend(), 0); // 要求vec元素类型也是int,与第三个参数0类型一致
// 字符串求和,accumulate会将每个字符串连接起来
string sum = accumulate(v.cbegin(), v.cend(), string("");
// 错误:因为const char*上没有定义 + 运算
string sum = accumulate(v.cbegin(), v.cend(), ""); // 这里""会被当作const char*类型空串
- equal
头文件algorithm
比较2个容器保存的元素序列是否相同,只有所有指定范围元素都相当时,才返回true。返回比较结果bool类型结果。
前2个参数是第一个序列的迭代器范围,第3个参数是第二个序列的首元素对应迭代器。
要求:
1)元素类型相同,能支持 == 运算符;
2)第一个序列元素数目 >= 1第一个序列元素数目;
// roster2元素数目 >= roster1元素数目
equal(roster1.cbegin(0, roster1.cend(), roster2.cbegin());
10.2.2 写容器的算法
- fill
头文件algorithm
在指定迭代器范围返回填充指定元素值。
fill有2个版本:
1)fill() 3个参数分别是起始迭代器(cbeging()),结束迭代器(cend()),初值
2)fill_n() 3个参数分别是起始迭代器,元素个数,初值;
第1)种是安全方式;
第2)种方式,如果要填充元素个数 > 容器范围,会导致未定义行为
vector<int> vec(10); // 构造10个元素的vector
fill(vec.begin(), vec.end(), 1); // 将vec所有元素填充为1
fill_n(vec.begin(), vec.size(), 0); // 将vec 10个元素都填充为0
fill_n(vec.begin(), 11, 0); // 11 > vec.size(),将导致未定义行为
- back_inserter
头文件iterator
插入迭代器,一种像容器中添加元素的迭代器,值被赋予迭代器指向的元素。
// 通过back_inserter插入元素 <=> push_back
vector<int> vec; // 空向量
auto it = back_inserter(vec); // it指向vec尾部
*it = 42; // vec现在有一个元素,值为42
// back_inserter结合fill_n,在指定位置 插入指定个数元素
fill_n(back_inserter(vec), 10, 0); // 在vec末尾添加10个元素
- copy
头文件algorithm
拷贝算法,从一个指定迭代器范围的序列元素,拷贝到另外一个序列。copy返回的是目的位置迭代器(递增后)的值,而非起始拷贝的位置值。
int a1[] = {0,1,2,3,4,5,6};
int a2[sizeof(a1) / sizeof(a1[0])];
// ret指向拷贝元素到a2后的尾元素之后位置
auto ret = copy(begin(a1), end(a1), a2); // 将a1内容拷贝到a2
- replace
头文件algorithm
replace用指定值,替换指定迭代器范围内的值。无返回值。会修改原容器的值。
replace_copy 先拷贝容器内容,用指定值,替换指定迭代器范围内的值。无返回值。不会修改原容器的值。
replace(ilst.begin(), ilst.end(), 0, 42); // 替换ilst中值为0的元素为42。 因为要修改原容器的值,只能用begin, end版本,不能用cbegin, cend版本
replace_copy(ilst.cbegin(), ilst.cend(), back_inserter(ivec), 0, 42); // il并未改变,ivec包含一份拷贝,原来值ilst中值为0的元素值ivec都变为42了
10.2.3 重排容器元素的算法
- sort
对指定迭代器范围元素进行排序,默认是递增顺序。
stable_sort() 稳定排序,除了对元素序列进行排序,还能保持两个“相等”元素的原有顺序。
vector<int> ivec = {8,2,78,11,5};
sort(ivec.begin(), ivec.end()); // 排序
// 改变排列顺序,由递增改为递减
sort(ivec.begin(), ivec.end(), greater<int>()); // 从大到小排列
sort(ivec.begin(), ivec.end(), less<int>()); // 从小到大排列
// 指定比较函数 - 谓词
// 按单词长短排序words
bool isShorter(const string &s1, const string &s2) {
return s1.size() < s2.size();
}
sort(words.begin(), words.end(), isShorter);
- unique
unique算法排序序列,重复元素“删除”(实际上是放到容器末尾)。返回的是一个指向不重复值范围的下一个位置的迭代器。如果有重复元素,unique返回值相当于移动到末尾的第一个重复值元素的位置。
注意:unique并不会排序。
// 排序words,并删除重复元素
void elimDups(vector<string>& words) {
sort(words.begin(), words.end());
auto end_unique = unique(words.begin(), words.end()); // end_unique是指向不重复区域之后一个位置的迭代器
words.erase(end_unique, words.end()); // [end_unique, words.end() ) 都是重复元素
}
10.3 定制操作
10.3.1 向算法传递参数
- 谓词
sort接收的第3个参数,可以传入函数作为参数,函数返回值作为两个迭代器指向的元素的比较的结果。如上面的例子sort(words.begin(), words.end(), isShorter());
谓词 predicate指一个可调用的表达式,其返回结果是一个能用作条件的值。
标准库的谓词分为两类:一元谓词 unary predicate,只接收单一参数;二元谓词 binary predicate,意味着它们(表达式)有2个参数。
sort的第3个参数,作为谓词,起到比较2个参数作用,也是二元谓词。
10.3.2 lambda表达式
可以向算法传递可调用对象(callable object)(可以理解成函数,或者函数指针)。
一个lambda表达式表示一个可调用代码单元,可以理解成一个未命名的内联函数。
一个lambda表达式类似于函数,具有一个返回类型,一个参数列表,一个函数体。lambda表达式形式:
[capture list](parameter list) -> return type { function body}
capture list 捕获列表,是一个lambda所在函数中定义的局部变量的列表,通常为空。
parameter list 参数列表,return type 函数返回类型, function body 函数体,跟普通函数类似。不过,lambda必须使用尾置返回。
参数列表、返回类型都可以忽略,但是必须包含捕获列表和函数体。
如果只有一个return,没有包含具体返回值,编译器无法推断出具体返回类型, 则返回类型为void。
auto f = []{ return 42; }
cout << f() << endl; // 打印42
向lambda传递参数
- lambda不能有默认参数;
- lambda调用的实参数目永远与形参数目相等;
// 示例:将前面的isShorter函数转化成lambda表达式
bool isShorter(const string& s1, const string& s2) {
return s1.size() < s2.size();
}
// lambda表达式
[](const string& s1, const string& s2) {
return s1.size() < s2.size();
}
使用捕获列表
lambda在函数中,可以使用其局部变量,不过需要先捕获(包含中捕获列表中)。
如find_if第3个参数,可以用谓词来过滤出符合条件的元素
[sz](const string& a) { // 捕获局部变量sz
return a.size() >= sz;
}
// 错误使用示范
[](const string& a) {
return a.size() >= sz; // 错误:未捕获sz
}
// 调用find_if,返回一个迭代器,指向第一个长度不小于sz的元素
// find_if 第三参数返回非0值,才能通过find_if的过滤
auto wc = find_if(words.begin(), words.end(), [sz](const string& a) {
return a.size() >= sz;
}
// 调用for_each算法,对指定迭代器范围每个元素,都调用lambda表达式
for_each(wc, words.end(), [](const string& s) {
cout << s << " ";
cout << endl;
})
10.3.3 lambda 捕获和返回
值捕获
变量的捕获方式也分为值捕获和引用捕获。被捕获的变量的值,是在lambda创建时拷贝,而不是调用时。
值捕获的变量值,值lambda函数结束后就不存在了。
引用捕获
需要确保引用捕获的对象值lambda执行的时候存在。
// 示例:值捕获
void fcn1() {
size_t v1 = 42;
auto f = [v1]{ return v1; }
v1 = 0;
auto j = f(); // j == 42,因为f lambda表达式捕获v1时,创建了临时拷贝
}
// 示例:引用捕获
void fcn2() {
size_t v1 = 42;
auto f = [&v1]{ return v1; }
v1 = 0;
auto j = f(); // j == 0,因为f lambda表达式捕获了v1的引用
}
引用捕获有时是必要的。
例如,希望biggies函数接受一个ostream的引用,用来输出数据,并接受一个字符作为分隔符。
void biggies(vector<string> &words, vector<string>::size_type sz, ostream &os = cout, char c = ' ') {
for_each(words.cbegin(), words.cend(),
[&os, c](const string& s){
os << s << c;
});
}
隐式捕获
指示编译器推断捕获列表,可以值捕获列表中写一个&或=:
& 告诉编译器采用捕获引用方式;
= 采用值捕获方式;
// sz为隐式捕获,值捕获方式,重写传递给find_if的lambda
wc = find_if(words.begin(), words.end(), [=](const string& s){
return s.size() >= sz;
});
// 混合隐式捕获和显式捕获
void biggies(vector<string>& words, ostream& os = cout, char c = ' ') {
for_each(words.begin(), words.end(),
[&, c](const string& s){ os << s << c; }); // os隐式捕获,c显示捕获
for_each(words.begin(), words.end(),
[=, &os](const string& s){ os << s << c; }); // c隐式捕获,os显式捕获
}
可变lambda
值拷贝的变量,默认情况下,lambda不会改变其值。如果改变被捕获变量的值,必须值参数列表首加上关键字mutable。
引用捕获的变量,如果不是const类型,就可以值lambda中修改。
// 示例:lambda修改值捕获变量,需要加mutable关键字
void fcn3() {
size_t v1 = 42;
auto f = [v1]() mutable { return ++v1; };
v1 = 0;
auto j = f(); // j为43
}
// 示例:lambda修改引用捕获变量,要求非const类型
void fcn4() {
size_t v1 = 42;
auto f = [&v1] { return ++v1;};
v1 = 0;
auto j = f(); // j为1
}
指定lambda返回类型
默认情况下,lambda如果包含除return语句以外的语句,编译器会假定此lambda返回void。
如果需要包含return以外语句,需要使用尾置返回类型。
// 下面的示例是让
// 错误示范:lambda使用return以外语句
transform(vi.begin(), vi.end(), vi.begin(),
[](int i){
if (i < 0) return -i ;
else return i;
});
// 正确示范1:不包含return以外语句
transform(vi.begin(), vi.end(), vi.begin(), [](int i){ return i < 0 ? -i : i; });
// 正确示范2:使用尾置返回类型,包含return以外语句
transform(vi.begin(), vi.end(), vi.begin(),
[](int i) -> int { // 注意这里的 -> int ,指名lambda返回int
if (i < 0) return -i ;
else return i;
});
10.3.4 参数绑定
lambda表达式对于在一两个地方使用简单操作很合适,但是需要在很多地方重复使用,还是(有名)函数更合适。但是对于lambda捕获的局部变量,用函数替换lambda的时候,如何进行呢?
例如,下面的sz,是在上面的find_if的第三个参数接受的lambda表达式中捕获的局部变量。如果改用函数,如何传递给函数check_size?
bool check_size(const string& s, string::size_type sz) {
return s.size() >= sz;
}
标准库bind函数
头文件functional
可以看做一个通用的函数适配器,接收一个可调用对象(函数),生成一个新的可调用对象来适应原对象的参数列表。
调用bind一般形式:
auto newCallable = bind(callable, arg_list);
newCallable 新生成的可调用对象
callable 给定的可调用对象
arg_list 参数列表。arg_list中的参数可能包含形如_n的名字,其中n是一个整数,称为“占位符” ,表示newCallable的参数,数字对应传递给newCallable形参的位置。
_1表示newCallable的第一个参数,_2表示第二个参数...
绑定check_size的sz参数
#include <functional>
#include <iostream>
#include <string>
#include <vector>
// 为了使用_1, _2这些,需要声明使用placeholders命名空间
using namepsace std::placeholders; // 声明使用placeholders命名空间
// 也可以声明只使用_1
using std::placeholders::_1;
bool check_size(const string& s, string::size_type sz) {
return s.size() >= sz;
}
void fcn5() {
vector<string> words = {"the", "red", "fox", "jumps", "over", "the", "slow", "red", "turtle"};
string::size_type sz = 6;
auto check6 = bind(check_size, _1, sz); // check6 是一个可调用对象
string s = "hello";
bool b1 = check6(s); // check6(s)会调用check_size(s,sz)
auto wc = find_if(words.begin(), words.end(), [sz](const string& a){ return a.size() >= sz; });
// 等价于下面的语句
wc = find_if(words.begin(), words.end(), bind(check_size, _1, sz));
}
bind参数
auto g = bind(f, a, b, _2, c, _1);
g(x, y);
// 调用g(x, y)将映射为调用下面的
f(a, b, y, c, x);
用bind参数重排序*
sort(words.begin(), words.end(), isShorter); // 按长度从小到大排序单词数组words
sort(words.begin(), words.end(), bind(isShorter, _2, _1)); // 通过调整placeholders顺序,交互绑定的参数顺序,从而改变调用isShorter比较两个元素时返回的结果,达到按长度从大到小排序单词数组
绑定引用参数
使用ref(),通过bind(f, a, b, _1)
传递a、b到可调用对象,都是拷贝,如果想传递引用,需要使用ref(a), ref(b)。如果要使用const 引用,需要用cref(a)。
ref函数会返回给定对象的引用。
char c = ' ';
// 使用lambda的等价语句,打印每个words元素
for_each(words.begin(), words.end(), [&os, c](const string& s) {os << s << c; });
// 等价的bind语句
for_each(words.begin(), words.end(), bind(print, ref(os), _1, c); // 这里必须使用ref(os),如果直接用os,传递给函数会发生拷贝,而我们知道ostream对象是无法拷贝的,会发生错误
ostream &print(ostream &os, const string& s, char c) {
return os << s << c;
}
10.4 再探迭代器
头文件iterator
- 插入迭代器 insert iterator : 迭代器被绑定到一个容器上,可用来向容器插入数据;
- 流迭代器 stream iterator: 迭代器被绑定到输入或输出流上,可用来遍历所关联的IO流;
- 反向迭代器 reverse iterator: 迭代器向后而不是向前移动。除了forward_list之外的标准容器库都有反向迭代器;
- 移动迭代器 move iterator:迭代器不是拷贝其中的元素,而是移动它们。
10.4.1 插入迭代器
插入迭代器有3种,根据元素插入位置区分:
- back_inserter 创建一个使用push_back的迭代器。只有容器支持push_back操作时,才能使用back_inserter。
- front_inserter 创建一个使用push_front的迭代器。同样需要容器支持push_front。
- inserter 创建一个使用insert的迭代器。接受2个参数,第1个是容器,第2个是迭代器。
我们知道,insert(pos, args)会将元素插入到pos位置之前,所以inserter(c, iter)也会将元素插入到iter之前,并返回新插入元素位置(iter的前一个位置)。
// 如果it是通过inserter获取的迭代器
// 那么下面2条语句等价
*it = val;
// <=>
it = c.insert(it, val); // it指向新加入的元素
++it; // 递增it,指向原来的元素
// front_inserter, inserter使用示例
list<int> lst = {1, 2, 3, 4};
list<int> lst2, lst3;
copy(lst.cbegin(), lst.cend(), front_inserter(lst2)); // lst2 == 4,3,2,1
copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin())); // lst3 == 1,2,3,4
10.4.2 iostream迭代器
iostream类型容器,而是输入输出流,标准库定义了可以用于这些IO类型对象的迭代器。
istream_iterator 读取输入流,ostream_iterator向一个输出流写数据。通过流迭代器,可以用泛型算法从流对象读取数据以及向其写入数据。
istream_iterator操作
输入流迭代器istream_iterator使用 >> (输入运算符)读取流,要读取的的类型必须定义了输入运算符。
默认初始化迭代器,可以当作尾后值使用的迭代器。
// 示例1: 通过istream_iterator从cin读入数据int数据, 直到输入结束或者遇到导致输入结束的非数字字符, 将读取到的数据存入vector并打印
istream_iterator<int> int_it(cin);
istream_iterator<int> int_eof;
vector<int> vec;
while(int_it != int_eof) {
vec.push_back(*int_it++);
}
for (const auto &d : vec) {
cout << d << " ";
}
cout << endl;
// 示例2: 通过istream_iterator从ifstream读入string数据, 并打印
ifstream in("records.txt");
istream_iterator<string> str_it(in); // 从"afile"读取字符串
istream_iterator<string> str_eof;
while (str_it != str_eof) {
cout << *str_it++ << endl;
}
ostream_iterator操作
ostream_iterator跟istream_iterator类似,必须指定要写到输出流的元素类型。ostream_iterator out对象,*out, ++out, out++等价(都不对out做任何事情),都返回out。
vector<int> vec = {1,3,5,7,9,11};
// 下面2个示例效果一样,都是将数组vec通过流迭代器打印出来
// 示例1:通过for循环,将数组内容写到流迭代器指向位置(标准输出流cout)
ostream_iterator<int> out_iter(cout, " ");
for (auto e : vec) {
*out_iter++ = e; // <=> out_iter = e;
}
cout << endl;
// 示例2:通过copy和vec迭代器范围,将数组内容写到流迭代器指向位置(标准输出流cout)
copy(vec.begin(), vec.end(), out_iter);
cout << endl;
10.4.3 反向迭代器
正向迭代器
vec.cbegin() ... vec.cend()
vec.crend() ... vec.crbegin()
反向迭代器
反向迭代器是在容器中从尾元素向首元素反向移动的迭代器。
// 示例1:逆序打印vector
vector<int> vec = {1,2,3,4,5,6,7,8};
// 从容器末尾向前开始打印,打印结果:8 7 6 5 4 3 2 1
for (auto r_iter : vec.crbegin(); r_iter != vec.cend(); ++r_iter) {
cout << *r_iter << "" ;
}
cout << endl;
// 示例2:逆序排序
sort(vec.begin(), vec.end()); // 递增排序
sort(vec.rbegin(), vec.rend()); // 递减排序
反向迭代器需要递减运算符
除了forward_list和流迭代器不支持递减运算,其他标准容器都支持递减运输,也就支持反向迭代器。此时,如果需要将反向迭代器转化成普通迭代器,需要使用.base()函数进行转换。
反向迭代器和其他迭代器之间的关系
直接使用反向迭代器,会导致每个元素之间的顺序也是反向的。
注意:在普通迭代器和反向迭代器的转换过程中,要非常小心,仔细观察位置是否与预期一致。
string line = "hello, this is martin zhang, I'm 18 years old, who are you?";
// 打印','前第一个单词
auto comma = find(line.cbegin(), line.cend(), ',');
cout << string(line.cbegin(), comma) << endl; // 打印hello
// 打印','后最后一个单词,但是单词本身也会反序打印
auto rcomma = find(line.crbegin(), line.crend(), ',');
cout << string(line.crbegin(), rcomma) << endl; // 打印?uoy era ohw
// 打印','后最后一个单词,单词本身正序打印
// 使用.base()将反向迭代器转化成普通迭代器,注意.base()返回的是当前迭代器位置的下一个位置,也就是说,当前位置是',',下一个位置是' '(空格)
cout << string(rcomma.base(), line.cend()) << endl; // 打印 who are you?
10.5 泛型算法结构
10.5.1 5类迭代器
-
输入迭代器 input iterator
只读不写,单遍扫描,只能递增。还支持相等性 判定运算符(== , !=),支持解引用运算符()(只出现在赋值语句右侧)和箭头运算符(->,等价于(it).member,解引用迭代器)。
只能用于顺序访问,典型应用:find, accumulate算法,istream_iterator输入流迭代器 -
输出迭代器 output iterator
输入迭代器的补集,只写不读,单向扫描,只能递增。支持解引用(*)(只出现在赋值语句左侧。
典型应用:copy算法,ostream_iterator输出流迭代器 -
前向迭代器 forward iterator
可读写,多遍扫描,只能递增,支持所有输入、输出迭代器的操作。
典型应用:replace算法,forward_list的迭代器 -
双向迭代器 bidirectional iterator
可正反读写序列中元素,多遍扫描,可递增递减,支持所有前向迭代器。
典型应用:除forward_list外,其他标准库都提供符合要求双向迭代器 -
随机访问迭代器 random-access iterator
可读写,多遍扫描,可递增递减,支持全部迭代器操作。支持常量时间访问任意元素。支持<, <=, >, >=, +,+=,-,-=,下标运算等。支持两个迭代器-,得到距离。
典型应用:sort算法,array, deque, string, vector迭代器
10.5.2 算法形参模式
大多数算法具有下面4种形式之一:
alg(beg, end, other args);
alg(beg, end, dest, other args);
alg(beg, end, beg2, other args);
alg(beg, end, beg2, end2, other args);
alg: 算法名;
beg, end: 算法所操作的输入范围;
dest, beg2, end2, 都是迭代器参数,指定目的第二个范围的角色;
args: 额外的非迭代器参数
10.5.3 算法命名规范
// 重新整理给定序列,将相邻重复元素删除
unique(begin, end); // 使用 == 比较元素
unique(begin, end, comp); // 使用comp比较元素,当comp为真时,删除第二个元素
// _if版本算法,接受一个谓词代替元素值
find(beg, end, val); // 查找输入范围中val第一次
find_if(beg, end, pred); // 查找输入范围中,第一个令谓词pred为真的元素
// 反转序列
reverse(beg, end); // 反转输入范围中的序列元素
reverse_copy(beg, end, dest); // 反转序列,拷贝到dest
// 删除元素
remove_if(v1.begin(), v1.end(), [](int i){ return i % 2; }); // 从v1中移除奇数值元素
remove_copy_if(v1.begin(), v1,end(), back_inserter(v2), [](int i){ return i % 2; }); // 将偶数元素拷贝到v2
10.6 特定容器算法
list, forward_list特定成员函数算法
// 都返回void
list<XXX> lst;
forward_list<XXX> flst;
// 合并链表
lst.merge(lst2); // lst2合并到lst。要求lst和lst2有序,元素从lst2删除(lst2合并后为空)。使用运算符 <
lst.merge(lst2, comp); // ... 使用谓词comp
// 删除元素
lst.remove(val); // 调用erase删除与给定值相等(==)的元素
lst.remove_if(pred); // ...满足谓词pred的元素
lst.reverse(); // 反转lst元素顺序
lst.sort(); // 链表排序,使用<
lst.sort(comp); // ..,使用comp进行比较
lst.unique(); // 调用erase删除连续的重复元素(==)
lst.unique(pred); // (满足pred)
// splice
lst.splice(p, lst2); // p是一个指向lst中元素迭代器,splice函数将lst2所有元素移动到lst中p之前。元素从lst2删除,lst2类型必须与lst相同,且不能是同一个链表
flst.splice_after(p, lst2); // ...lst2所有元素移动到lst中p之后...
lst.splice(p, lst2, p2); // p2是一个指向lst2中位置的有效迭代器。将p2指向的元素移动到lst中 (包含p2及以后的元素)。lst2可以与lst相同链表
flst.splice_after(p, lst2, p2); // ...将p2之后的元素移动到lst中(不包含p2)。lst2可以与flst相同链表
lst.splice(p, lst2, b, e); // b和e必须表示lst2中的合法范围。将给定范围元素从lst2移动到lst。lst2跟lst可以相同链表,但p不能指向给定范围中的元素
flst.splic_after(p, lst2,) // ...将给定范围元素从lst2移动到flst。llst2跟flst可以相同链表...