泛型算法
- 泛型算法中的大多数都独立于任何特定的容器,它们可用于不同类型的容器和不同类型的元素。
概述
- 大多数算法都定义在头文件algorithm中,标准库在头文件numeric中定义了一组数值泛型算法;
- 算法不直接操作容器,而是遍历有两个迭代器指定的一个元素范围;
- 迭代器令算法不依赖于容器,但算法依赖于元素类型的操作(如元素需支持==、<运算符等);
- 算法可能改变容器中保存元素的值,也可能在容器内移动元素,但是永远不会直接添加或删除元素,也永远不会改变底层容器的大小,;
- 标准库定义了一类特殊的迭代器(插入器),给这类迭代器赋值会在底层的容器上执行插入操作。
初识泛型算法
-
只读算法
-
find、count、accumulate、equal等:
// 如果存在此元素,则返回指向该元素的迭代器 auto result = find(vec.cbegin(), vec.cend(), val); // 对vec中的元素求和,和的初值是0 int sum = accumulate(vec.cbegin(), vec.cend(), 0); //string定义了+运算符 string sum = accumulate(vec.cbegin(), vec.cend(), string("")); //错误:const char*上没有定义+运算符 string sum = accumulate(vec.cbegin(), vec.cend(), ""); // roster2 中的元素数目应该至少与roster1一样多 equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());
-
注意使用cbegin和cend,因为不需要改变元素值;
-
只接受一个单一迭代器表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。
-
-
写容器元素的算法
-
fill算法
fill(vec.begin(), vec.end(), 10); // 将每个元素重置为10 fill_n(dext, n, val); // 假定目标容器足够大,能容纳要写入的元素
-
插入迭代器:通过一个插入迭代器赋值时,一个与赋值号右侧值相等的元素被添加到容器中;
- back_inserter:定义在头文件iterator中,接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器,当通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中。
vector<int> vec; auto it = back_inserter(vec); //通过它赋值会将元素添加到vec中 *it =42; // vec中现在有一个元素,值为42 vector<int> vec; fill_n(back_insetrer(vec), 10, 0); // 添加10个元素到vec
-
拷贝算法:向另一个目的位置迭代器指向的输出序列中的元素写入数据,必须保证第二个序列的元素个数至少等于第一个序列
int a1[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; int a2[sizeof(a1)/sizeof(*a1)]; // ret指向拷贝到a2的尾与元素之后的位置 auto ret = copy(begin(a1), end(a1), a2);//把a1的内容拷贝给a2
-
copy返回的是其目的位置迭代器(递增后)的值;
-
replace算法:将所有给定值的元素都改为另一个值,此算法可接受一个额外的迭代器,调用后,原序列保持不变,ivec包含list的一份拷贝,不过原来list中0的元素改为42
// 将所有值为0的元素改为42 replace(list.begin(), list.end(), 0, 42); // 此算法接受一个额外的迭代器 replace(list.begin(), list.end(), back_inserter(ivec), 0, 42);
-
-
-
重排容器元素的算法
-
消除重复元素:sort+unique+erase
void elimDups(vector<string> &words) { sort(words.begin(). words.end()); // unique重排输入范围,使得每个单词只出现一次 // 排列在范围前部的,返回指向不重复区域之后的一个位置的迭代器 auto end_unique = unique(words.begin(), words.end()); // 使用向量操作erase删除重复元素 words.erase(end_unique, word.end()); }
- stable_sort函数:长度相同按字典序进行排序;
-
reverse(b, e):将迭代器b和e之间的元素进行翻转。
-
定制操作
-
向算法传递函数
-
谓词:一个可调用的表达式,其返回结果是一个能用作条件的值。谓词可以分为一元谓词(只接受一个参数)和二元谓词(有两个参数),元素类型必须能转换为谓词的参数类型;
-
sort函数接受第三个参数,此参数是一个谓词:
// 比较函数,用来按长度排序单词 bool isShorter(const string &s1, const string &s2){ return s1.size() < s2.size(); } sort(words.begin(), words.end(), isShorter);
-
-
lambda表达式
-
一个lambda表达式表示一个可调用的代码单元,可以将其理解为一个未命名的内联函数;
-
一个lambda表达式具有一个返回类型,一个参数列表和一个函数体,可以定义在函数内部;
-
lambda只有在其捕获一个它所在函数中的局部变量,才能在函数体内使用该变量:
- [ capture list ] ( parameter list ) -> return type { function body}
- 可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体;
-
向lambda传递参数
- lambda不能有默认参数;
- 捕获列表只用于局部非static变量,lamdba可以直接使用局部static变量和在它所在函数之外声明的名字;
-
find_if和for_each
void biggies(vector<string> &words, vector<string>::size_type sz){ elimDups(words); // 删除重复元素 stable_sort(words.begin(), words.end(), isShorter); // 按长度排序,长度相同的单词维持字典序 // 获取迭代器,返回第一个满足size() >= sz的元素 auto wc = find_if(words.begin(), words.end(), [sz] (const string &a) {return a.size >= sz;}); for_each(wc, words.end(), [] (const string &s){cout << s << " "}); cout << endl; }
-
值捕获、引用捕获和隐式捕获
- 值捕获,被捕获的变量的值是在 lambda 创建时拷贝而非调用时拷贝;
- 引用捕获:定义 lambda 时可以采用引用方式捕获变量,当以应用方式捕获一个变量时,必须保证在 lambda 执行时变量时存在的;
- 隐式捕获:让编译器根据 lambda 体中的代码来推断我们要使用哪些变量,为了指示编译器推断捕获列表,应在捕获列表中写一个&或=,其中**&告诉编译器采用捕获引用方式**,=则表示采用值捕获方式。
void fcn1() { size_t v1 = 42; auto f = [v1] { return v1; };//值捕获 v1 = 0; auto j = f(); //j为42,f保存了我们创建它时v1的拷贝 } void fcn2() { size_t v1 = 42; auto f2 = [&v1] { return v1; };//引用捕获,对象f2包含v1的引用 v1 = 0; auto j = f2(); //j 为0,f2 保存 v1 的引用,而非拷贝 } //sz 为隐式捕获,值捕获方式 wc = find_if(words.begin(), words.end(), [=](const string &s){ return s.size() >= sz; });
-
-
参数绑定
-
标准库 bind 函数:bind 标准库函数,定义在头文件 functional 中,可以将 bind 函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表;
auto newCallable = bind(callable, arg_list);
-
newCallable 本身是一个可调用对象,arg_list 是一个逗号分隔的参数列表,对应给定的 callable 的参数;
-
调用 newCallable 时,newCallable 会调用 callable,并传递给它 arg_list 中的参数;
-
bind 的参数列表中,形如
_n
的参数称为“占位符”,表示 newCallable 的参数,它们占据了传递给 newCallable 的参数的“位置”,_1
则表示是 newCallable 的第一个参数://check6 是一个可调用对象,接受一个 string 类型的参数 //并用此 string 和值6来调用 check_size auto check6 = bind(check_size, _1, 6);
bind 调用中只有一个占位符,表示 check6 只接受单一参数,而占位符出现在 arg_list 的第一个位置,表明 check6 的此参数对应 check_size 的第一个参数;
//基于lambda的find_if调用 auto wc = find_if(words.begin(), words.end(), [sz](const string &a) {return a.size() >= sz; }); bool check_size(const string &s, string::size_type sz) { return s.size() >= sz; } //使用check_size的版本 auto wc = find_if(words.begin(), words.end(), bind(check_size, _1, sz));
-
bind 的参数:我们可以用 bind 绑定给定可调用对象中的参数或重新安排其顺序:
auto g = bind(f, a, b, _2, c, _1); //意为:g(_1, _2) ----> f(a, b, _2, c, _1)
-
再探迭代器
-
-
除了为每个容器定义的迭代器之外,标准库在头文件 iterator 中额外定义了几种迭代器。
-
插入迭代器:这些迭代器被绑定在一个容器上,可以用来向容器插入元素;
- back_inserter:创建一个使用push_back的迭代器;
- front_inserter:创建一个使用push_front的迭代器;
- **inserter:**创建一个使用insert的迭代器。
-
流迭代器:这些迭代器被绑定在输入流或输出流上,可用来遍历所关联的 IO 流,尽管 iostream 类型不是容器,但通过使用流迭代器,我们可以用泛型算法从流对象读取数据以及向流写入数据。
-
istream_iterator:读取输入流
-
可以为任何定义了输入运算符(>>)的类型创建 istream_iterator 对象;
-
当创建一个流迭代器时,必须指定迭代器将要读写的对象类型;
-
可以默认初始化迭代器,这样就创建了一个当作尾后值使用的迭代器;
istream_iterator<int> in_iter(cin), eof; vector<int> vec(in_iter, eof);
-
-
ostream_iterator:向一个输出流写数据
-
可以对任何具有输出运算符(<< 运算符)的类型定义 ostream_iterator;
-
当创建一个 ostream_iterator 时,可以提供可选的第二参数,它是一个字符串,在输出每个元素后都会打印此字符串,但是该字符串必须是一个 C 风格字符串,即一个字符串字面值常量或者一个指向空字符结尾的字符数组的指针;
ostream_iterator<int> out_iter(cout, " "); for(auto e : vec) *out_iter++ = e; //赋值语句实际上就是将元素写到 cout cout << endl;
-
-
反向迭代器
- 除了 forward_list 之外的标准容器库都有反向迭代器;
- 递增和递减操作的含义颠倒;
- 可以通过调用 rbegin、rend、crbegin、crend 成员函数来获得反向迭代器,这些成员函数分别指向容器尾元素和首元素之前一个位置的迭代器;
- 反向迭代器和其他迭代器之间的关系:通过调用 reverse_iterator 的 base 成员函数可以将反向迭代器转换成普通迭代器,但要注意转换前后的迭代器变化,指向的并不是同一位置。
-
泛型算法结构
-
5类迭代器
类别 特点 功能 使用场合 输入迭代器 只读,不写,单遍扫描,只能递增 支持== != ++ * ->操作 find、accumulate 输出迭代器 只写,不读,单遍扫描,只能递增 支持++ *操作 copy 前向迭代器 可读写,多遍扫描,只能递增 支持所有输入输出迭代器操作 replace,forword_list 双向迭代器 可读写,多遍扫描,可递增递减 支持所有输入输出迭代器操作和 – reverse 随机访问迭代器 可读写,多遍扫描,支持全部迭代器运算 支持双向迭代器的所有功能和关系运算符、加减运算、减法运算和下标运算 算法sort、容器array、deque、string和vector -
算法参数模型
大多数算法具有如下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都是迭代器参数,dest参数是一个表示算法可以写入的目的位置的迭代器
-
算法命名规范
-
一些算法使用重载形式传递一个谓词:
unique(beg, end); // 使用==运算符比较元素 unique(beg, end, 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){ return i % 2; }); // 从v1中删除奇数元素 remove_copy_if(v1.begin(), v1.end(), back_inserter(v2), [](int){ return i % 2; }); // 将偶数元素从v1拷贝到v2,v1不变
-
特定容器算法
- 对于list和forword_list,应该优先使用成员函数版本的算法,而不是通用算法,因为代价太大。
lst.merge(lst2) | 将来自lst2的元素合并入lst,lst和lst2必须都是有序的 |
---|---|
lst.merge(lst2, comp) | 元素将从lst2中删除。在合并之后,lst2变为空。第一个版本使用<运算符;第二个版本使用给定的比较操作 |
lst.remove(val) | 调用erase删除掉与给定值相等的每个元素 |
lst.remove_if(pred) | 调用erase删除掉令一元谓词为真的每个元素 |
lst.reverse( ) | 反转lst中元素的顺序 |
lst.ort( ) | 使用<或给定比较操作排序元素 |
lst.sort(comp) | |
lst.unique( ) | 调用erase删除同一个值的连续拷贝,使用== |
lst.unique(pred) | 调用erase删除同一个值的连续拷贝,使用给定的二元谓词 |