C++ Primer10.generic
泛型算法
generic algorithm
查找特定元素,替换或删除一个特定值,重排元素顺序等操作,标准库并未给每个容器都定义成员函数来实现这些操作,而是定义了一组泛型算法,他们实现了一些经典算法的公共借口,如排序和搜索。因为他们可以用于不同类型的元素和多种容器类型,所以他们是泛型的。
大多数算法都定义在头文件 altorithm 中。标准库还在头文件 numeric 中定义了一组数值泛型算法。
概述
标准库 find
int val = 42;
auto result = find(vec.cbegin(), vec.cend(), val);
cout << "The value " << val << (result == vec.cend()
? " is not present" : " is present") << endl;
find 的前两个参数指定了迭代器范围,返回查找到的元素的指针,如果没找到则返回第二个参数。result 的类型是元素指针类型。
查找范围不包含第二个参数。
int ia[] = {27, 210, 12, 47, 109, 83};
int val = 83;
int* result = find(begin(ia), end(ia), val);
初识泛型算法
只读算法
count : 计数
accumulate : 累加
// accumulate 函数执行元素的 + 操作;最后参数表示累加的初始值
int isum = accumulate(iv.cbegin(), iv.cend(), 0);
string ssum1 = accumulate(sv.cbegin(), sv.cend(), ""); // 错误
string ssum2 = accumulate(sv.cbegin(), sv.cend(), string("")); // 正确
const char*没有 + 操作
equal
第二个序列只接受单一迭代器,这种算法都假定第二个序列与第一个序列一样长。
equal(v1.cbegin(), v1.cend(), v2.cbegin());
操作两个序列的算法,不要求两个序列类型一样,比如 list 和 deque,但要求其中元素能用 == 进行比较。
equal迭代器接受三个迭代器,而其他算法接受4个迭代器
写容器元素的算法
fill
fill(v.begin(), v.end(), 0); // 全部重置为0
fill(v.begin(), v.begin() + v.size()/2, 10); // 前一半元素置为10
back_inserter
插入迭代器,通过它可以向容器插入元素
vector<int> vec;
auto it = back_inserter(vec); // 对it赋值会向 vec 插入元素
* it = 42;
vector<int> vec1;
fill_n(back_inserter(vec1), 10, 0); // 添加10个元素到 vec1,值为0
拷贝算法
int a1[] = {0, 1, 2, 3, 4, 5, 6,7,8,9};
int a2[sizeof(a1)/sizeof(*a1)]; // a2 与 a1 大小相同
auto ret = copy(begin(a1), end(a1), a2);
copy 返回的是目的位置迭代器的值,即,ret 指向拷贝到 a2 的尾元素之后的位置
// 将所有值为0的元素改成42
replace(list.begin(), list.end(), 0, 42);
// 保留原序列不变
replace_copy(list.cbegin(), list.cend(), back_inserter(ivec), 0, 42);
重排
// 排序然后去重
string words[] = {"the", "red", "fox", "jumps", "over", "the", "red", "slow", "turtle", "quick"};
sort(words.begin(), words.end());
auto end_unique = unique(words.begin(), words.end());
words.erase(end_unique, words.end());
unique 将不同元素排在容器的前面,重复元素放在后面,返回一个迭代器,指向最后一个不重复元素之后的位置,该位置和之后位置的元素内容可能被清除。
unique 不能操作容器,所以它不能改变容器大小,需要用 erase 删除元素。
定制操作
改变默认的比较运算符 <
向算法传递函数
bool isShorter(const string s1, const string s2){
return s1.size() < s2.size();
}
sort(words.begin(), words.end(), isShorter);
https://www.cnblogs.com/wildricky/p/15532952.html
stable_sort()的比较函数的参数不能使用引用(可以用常引用或者常量或者值传递)。sort()在数据小于16的时候采用插入排序,大于16且划分均匀(靠一个划分元素数目的定值判断)时采用快速排序,在划分不均匀的时候采用堆排序。
stable_sort()为了保证数据有序性采用归并排序,归并排序利用inplace_merge实现(一个序列的前一部分和后一部分分别有序,然后将其整个排序)而inplace_merge又分两种情况,有缓冲区和无缓冲区。无缓冲区的排序比较复杂,STL源码剖析也没写。所以只考虑有缓冲区。
lambda 表达式 (for_each)
lambda 表达式规则
[capture list](parameter list) -> return type { function body }
capture list (捕获列表)表示 lambda 所在函数中定义的局部变量的列表(通常为空);
return type 表示返回类型,lambda 必须使用尾置返回来指定返回类型
可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体:
auto f = [] { return 42; };
// f 不接受参数
cout << f() << endl;
使用捕获列表:
[sz](const string &a) { return a.size() >= sz; };
比较大小改为 lambda 表达式:
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; });
// 打印长度大于给定值的单词,用空格隔开
for_each(wc, words.end(), [](const string &s){ cout << s << " "; });
lambda 捕获和返回
当用 auto 定义一个用 lambda 初始化的变量时,定义了一个从 lambda 生成的类型的对象。
从 lambda 生成的类都包含一个对应该 lambda 所捕获的变量的数据成员,在 lambda 对象创建时被初始化。
捕获方式分为值捕获和引用捕获,值捕获的变量在 lambda 创建时拷贝,而不是调用时拷贝。
引用捕获
void func2(){
size_t v1 = 42;
auto f2 = [&v1] { return v1; };
v1 = 0;
auto j = f2();
}
引用捕获的方式是在变量名前加 &。
当lambda 返回 v1 时,返回的是 v1 指向的对象的值。
cin,cout 无法进行值传递,此时需要用到引用传递。
ostream& os = cout;
char c = ' ';
for_each(words.begin(), words.end()
[&os, c](const string &s) { os << s << c; });
对于引用捕获,要注意 lambda 创建和执行的时机,确保在执行时捕获的变量没有发生变化
size_t v1 = 42;
auto f1 = [v1] { return v1; };
auto f2 = [&v1] { return v1; };
v1 = 50;
auto i = f1(); // i: 42
auto j = f2(); // j: 50
隐式捕获
让编译器根据 lambda 表达式中的代码来推断我们要使用那些变量。在放括号中写一个 & 或 =.
& 表示引用捕获;= 表示值捕获。
wc = find_if(words.begin(), words.end(),
[=](const string& s)
{ return s.size() >= sz; });
混合使用隐式捕获和显式捕获:
ostream& os = cout;
char c = ' ';
// os使用引用捕获,其他使用值捕获
for_each(words.begin(), words.end()
[=, &os](const string &s) { os << s << c; });
// c 使用值捕获,其他使用引用捕获
for_each(words.begin(), words.end()
[&, c](const string &s) { os << s << c; });
当使用混合捕获时,显式捕获必须使用与隐式捕获不同的方式
可变 lambda (mutable)
改变被捕获的变量的值:
size_t v1 = 42;
auto f = [v1]() mutable { return ++v1; };
v1 = 0;
auto j = f(); // j为 43
指定 lambda 返回类型(transform)
当 lambda 函数体中包含除 return 外其他语句时,lambda 函数判定为返回 void,此时需要指定返回类型:
transform(vi.begin(), vi.end(), vi.begin(), [](int i) -> int
{ if (i < 0) return -i; else return i; });
参数绑定
find_if 函数中的 lambda 函数只能传递一个参数,若想使用两个参数的函数,可用 bind 进行参数绑定:
auto wc = find_if(words.begin(), words.end(),
[sz](const string& s)
{ return s.size() >= sz; });
// 等价于以下
#include <functional>
using namespace std::placeholders;
bool check_size(const string& s, string::size_type sz){
return s.size() >= sz;
}
auto wc = find_if(words.begin(), words.end(), bind(check_size, _1, sz));
// 最后一句等价于以下
auto callable = bind(check_size, _1, sz);
auto wc = find_if(words.begin(), words.end(), callable);
bind 声明在 functional 头文件中。
占位符 _1 是 find_if 传递给 check_size 的第一个参数。_2 是第二个,以此类推。(调用 callable 时的参数)
名字 _n 都定义在 placeholders 的命名空间中,这个命名空间又定义在 std 命名空间中,想要使用它们,应先用 using 声明:
using std::placeholders::_1;
绑定引用参数
ostream& os = cout;
char c = ' ';
for_each(words.begin(), words.end(),
[&os, c](const string& s)
{ return os << s << c; });
// 等价于:
ostream& print(ostream& os, const string& s, char c){
return os << s << c;
}
for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' '));
除了 ref 函数,还定义了一个 cref 函数,生成一个保存 const 引用的类。他们都在头文件 functional 中。
迭代器
标准库 iterator
插入迭代器
接受一个容器,生成一个迭代器
- back_inserter
- front_inserter
- inserter
auto it = back_inserter(c);
// 在指定的当前位置插入值t
it = t;
// it 为 back_inserter时,等价于:
c.push_back(t);
// it 为 front_inserter时,等价于:
c.push_front(t);
// it 为 inserter时,等价于:(p 为传递给 inserter 的迭代器位置)
c.insert(t, p);
*it = val;
++it;
拷贝例:
list<int> lst = {1,2,3,4};
list<int> lst2, lst3;
// 拷贝完后 lst2: 4,3,2,1
copy(lst.cbegin(), lst.cend(), front_inserter(lst2));
// 拷贝完后 lst3: 1,2,3,4
copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin()));
流迭代器 (iostream 迭代器)
遍历关联的 IO 流
istream_iterator
istream_iterator<int> int_it(cin); // 从 cin 读取 int
istream_iterator<int> eof; // istream 尾后迭代器
// 从文件 a.txt 读取字符串
ifstream in("a.txt");
istream_iterator<string> str_it(in);
// 从标准输入读取整数,存入 vec 中:
vector<int> vec;
while(int_it != eof){
vec.push_back(*int_it++);
}
// 从迭代器范围构造 vec:
vector<int> vec(int_it, eof);
// 叠加算法:
cout << accumulate(int_it, eof, 0) << endl;
ostream_iterator
ostream_iterator<int> out1(cout);
// out2 将整数写入到 cout 中,每个值后面输出一个 d
// d 指向一个空字符结尾的字符数组
ostream_iterator<int> out2(cout, " ");
for (auto e: vec){
*out2++ = e; // 写入到 cout,每个元素后加一个空格
// 等价于
out2 = e;
}
cout << endl;
上例中写入到 cout 时的 * 和 ++ 操作对程序没有任何影响,这样写只是为了与其他迭代器的使用保持一致,方便移植和修改。
利用 copy 打印 vec 中的元素:
copy(vec.begin(), vec.end(), out_it);
cout << endl;
类类型
istream_iterator<Sales_item> item_it(cin), eof;
ostream_iterator<Sales_item> out_it(cout, "\n");
// 从 cin 中读第一条记录
Sales_item* sum = *item_it++;
while (item_it != eof){
// 相同isbn号的书合到一个对象中
if (item_it->isbn() == sum.isbn()){
sum += *item_it++;
} else {
// 打印到 cout
out_it = sum;
sum = *item_it++;
}
}
out_it = sum;
反向迭代器
除 forward_list 外的标准容器库都有反向迭代器
vector<int> vec = {0,1,2,3,4,5,6};
for (auto r_it = vec.crbegin(); r_it != vec.crend(); ++r_it){
cout << *r_it << endl;
}
++ 和 -- 的含义与正向迭代器含义完全相反,++ 是移动到前一个元素,-- 移动到后一个元素。
逆序排列:
sort(vec.rbegin(), vec.rend());
查找字符串中最後有一個单词:
string s = "first,second,third";
auto rcomma = find(s.crbegin(), s.crend(), ',');
string last = string(rcomma.base(), s.cend()); // third
上例的 find 的返回的是反向迭代器,使用时需要转换成普通迭代器,使用 base 进行转换。
rcomma 与 rcomma.base() 并不是相同位置,rcomma.base() 是 rcomma 的后一个位置。(crbegin() 和 cend() 也是一样)
forward_list 和流迭代器都不支持反向迭代器
移动迭代器
泛型算法结构
按操作分类:
| 迭代器类别 | 操作 |
|---|---|
| 输入迭代器 | 只读,不写;单遍扫描,只递增 |
| 输出迭代器 | 只写,不读;单遍扫描,只递增 |
| 前向迭代器 | 可读写;多遍扫描,只递增 |
| 双向迭代器 | 可读写;多遍扫描,可递增递减 |
| 随即访问迭代器 | 可读写;多遍扫描,支持全部迭代器运算 |
5类迭代器
输入迭代器:
- 比较相等和不相等(==,!=)
- 前置和后置递增(++)
- 解引用(
*) - 箭头运算符(
->),等价于解引用后取成员((*it).member)
输出迭代器:
- 前置和后置递增(++)
- 解引用(
*)- 对已经解引用的输出迭代器赋值,相当于将值写入它指向的元素
- 只能向一个输出迭代器赋值一次(单遍扫描特性)
随机访问迭代器
- 提供在常量时间内访问序列中任意元素的能力,支持双向迭代器所有功能
- 支持比较(<,<=,>,>=)
- 和一个整数的加减运算(+,-,+=,-=)
- 两个迭代器相减,得到两个迭代器的距离
- 下标运算符
iter[n]和*(iter[n])等价
sort 算法要求随即访问迭代器。 array,deque,string 和 vector 的迭代器都是随机访问迭代器,用于访问内置数组元素的指针也是。
算法形参模式
alg(beg, end, args);
alg(beg, end, dest, args);
alg(beg, end, beg2, args);
alg(beg, end, beg2, end2, args);
dest 需要一个插入迭代器或者输出迭代器
算法命名规范
使用重载传递一个谓词
unique(beg, end); // 等价于 ==
unique(beg, end, comp); // 用 comp 代替默认的 ==
comp 可以是函数或 lambda 表达式
_if 版本
find(beg, end, val);
find_if(beg, end, pred); // 查找第一个令 pred 为真(非0)的元素
pred 可以是函数或 lambda 表达式
区分拷贝和不拷贝的版本
重排算法是一种本地操作,想要写到指定的输出位置,在算法名后加 _copy
reverse(beg, end);
reverse_copy(beg, end, dest); // 逆序拷贝到 dest
有些算法同时提供 _if 和 _copy:
// 删除奇数元素
remove_if(v1.begin(), v1.end(),
[](int i){ return i%2; });
// v1不变,将偶数元素拷贝到 v2
remove_copy_if(v1.begin(), v1.end(), back_inserter(v2),
[](int i){ return i%2; });
特定容器算法
list 和 forward_list 与其他容器不同,他们分别提供双向迭代器和前向迭代器,因此不能使用通用版的 sort,通用版 sort 要求随机访问迭代器。所以 list 和 forward_list 定义了自己独有的 sort, merge, remove, reverse 和 unique.
其他算法的通用版可以用于链表,但代价太高。这些算法需要交换输入序列中的元素,而链表可以通过改变元素的链接而不必交换值来快速“交换”元素。
| 链表算法 | 说明 |
|---|---|
| lst.merge(lst2) lst.merge(lst2,comp) |
将 lst2 的元素合并入 lst.两个链表都必须是有序的。 元素将从 lst2 中删除。合并后 lst2 变为空。 第一个版本使用 <,第二个版本使用谓词 |
| lst.remove(val) lst.remove_if(pred) |
调用 erase 删除与给定值相等或令谓词返回值为真的每一个元素 |
| lst.reverse() | 反转 |
| lst.sort() lst.sort(comp) |
排序,默认使用 < |
| lst.unique() lst.unique(pred) |
调用 erase 删除同一個值的连续拷贝,默认用 == |
上述操作都返回 void
splice 成员
lst.splice(args);
flst.splice_after(args);
| 参数 | 说明 |
|---|---|
| (p, lst2) | p 是指向 lst 中元素的迭代器,或 flst 首前位置的迭代器。 将 lst2 的所有元素移动到 lst 中 p 之前的位置或 flst 中 p 之后的位置。 将元素从 lst2 中删除。lst2 的类型必须与 lst 或 flst 相同,且不能是同一個链表。 |
| (p, lst2, p2) | p2 是 lst2 的迭代器。将 p2 指向的元素移动到 lst 中,或将 p2 之后的一个元素移动到 flst 中。lst2 可以是与 lst 或 flst 相同的链表 |
| (p, lst2, b, e) | b 和 e 必须表示 lst2 中的合法范围。 将给定范围的元素从 lst2 移动到 lst 或 flst。 lst2 与 lst(或 flst)可以是相同的链表,但 p 不能指向给定范围中元素 |
链表特有的操作会改变容器
与通用版的区别是链表版会改变底层的容器。
比如 remove 的链表版本会删除指定的元素。
unique 的链表版会删除第二个和后继的重复元素。
merge 和 splice 会销毁其参数。

浙公网安备 33010602011771号