C++ Primer学习笔记 - 第10章 初识泛型算法
10.2 初识泛型算法
10.2.1 只读算法
- find
头文件<algorithm>
find 在指定迭代器范围种,查找目标元素。返回查找到的第一个目标元素的迭代器。支持容器类型,内置数组。
要求:元素支持“==”运算符。
// 1. 在list中查找指定元素
list<string> lst;
// 向lst插入元素
// lst.push_fron("...");
// 利用find在lst中查找指定元素
string val = "a value";
std::find(lst.cbegin(), lst.end(), val);
// 2. 在内置数组中查找指定元素
int ia[] = {1,2,3,4,5,6,7};
int val = 6;
int* res = std::find(begin(ia), end(ia), val);
- accumulate
头文件<numeric>
accumulate 求累加和。返回求和结果。
除了指定迭代器范围,还需要指定初值(第三个参数)。运算时,元素会转换为初值类型。
要求:元素支持“+”运算符。
std::vector<int> vec;
// 要求vec元素类型也是int,与accumulate第三个参数0类型一致
// 字符串求和,accumulate会将每个字符串连接起来
int sum = std::accumulate(vec.cbegin(), vec.cend(), 0);
// 错误:因为const char*上没有定义 + 运算
std::string sum = std::accumulate(v.cbegin(), v.cend(), ""); // 这里""会被当作const char*类型空串
- equal
头文件<algorithm>
equal 比较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个参数分别是起始迭代器(begin())、结束迭代器(end())、初值;
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 插入迭代器,一种向容器中添加元素的迭代器,值被赋予迭代器指向的元素。
// 通过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 拷贝算法,从一个指定迭代器范围的序列元素,拷贝到另外一个序列。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 先拷贝容器内容,用指定值替换指定迭代器范围内的值。无返回值,且不会修改原容器的值。
// 替换ilst中值为0的元素为42。 因为要修改原容器的值,只能用begin, end版本,不能用cbegin, cend版本
replace(ilst.begin(), ilst.end(), 0, 42);
// il并未改变,ivec包含一份拷贝,原来值ilst中值为0的元素值ivec都变为42了
replace_copy(ilst.cbegin(), ilst.cend(), back_inserter(ivec), 0, 42);
10.2.3 重排容器元素的算法
- sort
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个参数,可传入函数作为参数,函数返回值作为两个迭代器指向的元素的比较的结果。e.g. sort(words.begin(), words.end(), isShorter());
谓词predicate,指一个可调用的表达式,返回结果是一个能用作条件的值。
标准库的谓词分为两类:一元谓词 unary predicate,只接收单一参数;二元谓词 binary predicate,意味着它们(表达式)有2个参数。
sort的第3个参数,作为谓词,起到比较2个参数作用,也是二元谓词。
10.3.2 lambda表达式
可向算法传递可调用对象(callable object)(e.g. 函数,函数指针,仿函数)。
一个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;
});
}
隐式捕获
指示编译器推断捕获列表,值捕获列表中写一个&或=,告诉编译器:
1)& 用引用捕获;
2)= 用值捕获;
// 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语句以外的语句,编译器会假定返回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表达式捕获的局部变量而作为第3个参数。如果改用函数,如何传递给函数check_size?
// 示例:将值捕获lambda改写为有名函数
// 改写前:使用lambda作为find_if第3个参数
auto wc = find_if(words.begin(), words.end(), [sz](const string& a) {
return a.size() >= sz;
}
// 定义与lambda一致的有名函数
bool check_size(const string& s, string::size_type sz) {
return s.size() >= sz;
}
// 改写后:使用有名函数作为find_if的第3个参数
auto wc = find_if(words.begin(), words.end(), check_size);
标准库bind函数
头文件<functional>
bind 可看做一个通用的函数适配器,接收一个可调用对象(函数),生成一个新的可调用对象来适应原对象的参数列表。
调用bind一般形式:
auto newCallable = bind(callable, arg_list);
newCallable新生成的可调用对象callable给定的可调用对象arg_list参数列表。arg_list中的参数可能包含形如_n的名字,其中n是一个整数,称为“占位符” ,表示newCallable的参数,数字对应传递给newCallable形参的位置。e.g._1表示newCallable的第一个参数,_2表示第二个参数,...以此类推
绑定check_size的sz参数
#include <functional>
#include <iostream>
#include <string>
#include <vector>
// 为了使用_1, _2这些,需要声明使用placeholders命名空间
using namepsace std::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参数重排序
// 按长度从小到大排序单词数组words
sort(words.begin(), words.end(), isShorter);
// 调整placeholders顺序,交互绑定的参数顺序,从而改变调用isShorter比较两个元素时返回的结果,
// 达到按长度从大到小排序单词数组的目的
sort(words.begin(), words.end(), bind(isShorter, _2, _1));
绑定引用参数
使用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语句
// 这里必须使用ref(os),如果直接用os,传递给函数会发生拷贝,
// 而我们知道ostream对象是无法拷贝的,会发生错误
for_each(words.begin(), words.end(), bind(print, ref(os), _1, c);
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()返回的是当前迭代器位置的下一个位置,
// i.e. 当前位置是',',下一个位置是' '(空格)
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
// 删除元素
// 从v1中移除奇数值元素
remove_if(v1.begin(), v1.end(), [](int i){ return i % 2; });
// 将偶数元素拷贝到v2
remove_copy_if(v1.begin(), v1,end(), back_inserter(v2), [](int i){ return i % 2; });
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
// p是一个指向lst中元素迭代器,splice函数将lst2所有元素移动到lst中p之前。
// 元素从lst2删除,lst2类型必须与lst相同,且不能是同一个链表
lst.splice(p, lst2);
// ...lst2所有元素移动到lst中p之后...
flst.splice_after(p, lst2);
// p2是一个指向lst2中位置的有效迭代器。将p2指向的元素移动到lst中,
// 包含p2及以后的元素。lst2可以与lst相同链表
lst.splice(p, lst2, p2);
// ...将p2之后的元素移动到lst中(不包含p2)。lst2可以与flst相同链表
flst.splice_after(p, lst2, p2);
// b和e必须表示lst2中的合法范围。将给定范围元素从lst2移动到lst。
// lst2跟lst可同链表,但p不能指向给定范围中的元素
lst.splice(p, lst2, b, e);
// ...将给定范围元素从lst2移动到flst。llst2跟flst可以相同链表...
flst.splic_after(p, lst2,);

浙公网安备 33010602011771号