10. 泛型算法
标准库并未给每个容器都定义成员函数来实现一些公共操作,标准库因此定义了一组泛型算法,可以用于不同类型的元素和多种容器。
10.1 标准库
-
大多数算法都定义在头文件algorithm中,标准库还在头文件numeric中定义了一组数值泛型算法。
-
find函数的两种用法(迭代器和数组)
类似的,还有count算法(计数)auto result = find(vec.cbegin(), vec.cend(),val); auto result = find(ia + 2, ia + 5, ia);
-
迭代器令算法不依赖于容器,但算法依赖于元素类型的操作。
举例:find操作使用==
对元素进行比较,而用户可以重载==
的功能。 -
算法不执行容器的操作,它可能改变容器中保存的元素的值,也可能移动元素,但不会添加或删除元素。
10.2 初识泛型算法
10.2.1 只读算法
-
举例:find、count。
-
accumulate算法,定义在头文件numeric中。
前两个参数指定的序列必须与第三个参数匹配。int sum = accumulate(vec.begin(), vec.cend(), 0); string sum = accumulate(v.cbegin(), v.cend(), ""); string sum = accumulate(v.cbegin(), v.cend(), string(""));
-
equal算法:用于确定两个序列是否保存相同的值。
所对比的容器类型允许不一样,只要对应元素可以==运算即可。
三个参数中,后者容器长度必须大于或等于前者,否则函数将试图访问后者容器中不存在的元素,引发错误。equal(vec.cnegin(), vec.cend(), list.cbegin());
-
三个参数的算法,后者容器长度必须大于或等于前者,否则函数将试图访问后者容器中不存在的元素,引发错误。
10.2.2 写容器的算法
-
要求我们算法写入的元素数目大于或等于容器原容量。
-
fill算法、fill_n算法:对容器原有的元素进行修改。
fill(vec.begin(), vec.begin() + vec.size(), 0); fill_n(vec.begin(), 5, 0);
-
back_inserter插入迭代器:定义在头文件iterator中。
back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。
当使用此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中。vector<int> vec; auto it = back_inserter(vec); *it = 42; // 先执行back_inserter增加空间,再执行fill_n改值 fill_n(back_inserter(vec), 10, 0);
-
copy算法:前两个参数接收输入,第三个参数为目的序列的起始位置。
copy算法可用于内置数组。
copy算法返回的是其目的位置的尾后迭代器(或指针)。int a1[] = {0,1,2,3,4,5,6,7,8,9}; int a2[sizeof(a1) / sizeof(*a1)]; // 此时返回值是end(a2),即a2尾元素之后的位置 auto ret = copy(begin(a1), end(a1), a2);
-
replace算法:把第三个参数的值替换为第四个参数。
replace_copy算法:中间加一个参数,用于指出调整后序列的保存位置。
即,原来的ilist并未改变,replace(ilist.begin(), ilist.end(), 0, 42); replace_copy(ilist.begin(), ilist.end(), back_inerter(ivec), 0, 42)
10.2.3 重排容器的算法
-
sort算法:利用<运算符来实现排序,形参为两个迭代器
-
unique算法:重排输入序列,将相邻的重复项“消除”,并返回一个指向不重复值范围末尾的迭代器。
-
标准库算法对迭代器而不是容器进行操作。因此,算法不能直接添加或删除迭代器。真正的删除元素,必须使用容器操作。
10.3 定制操作
- sort算法默认使用元素类型的<运算符,标准库还为这些算法定义了额外版本,允许用户提供自己定义的操作来代替默认运算符。
10.3.1 向算法传递参数
-
谓词:一个可调用的表达式,返回结果是一个能用做条件的值(即表达式运算结果是0和非0)。
一元谓词:只接受单一参数;二元谓词:有两个参数。 -
举例:使用isShorter替代sort函数原有操作(<)。
bool isShorter(const string &s1, const string &s2); sort(words.begin(), words.end(), isShorter);
-
stable_sort算法:稳定排序算法维持相等元素的原有顺序,
以上面的例子继续,调用stable_sort,可以保持等长元素间的字典序。elimDumps(words); // 字典序重排元素 stable_sort(words.begin(), words.end(), isShorter);
-
partition算法:接受一个谓词,对容器内容进行划分,使得谓词为true的值排在前半部分。返回一个迭代器,指向最后一个true元素之后的位置。
-
stable_partition:划分后的序列中维持原有的顺序。
10.3.2 lambda表达式
-
find_if算法:接受一对迭代器表示范围,接受谓词,返回第一个谓词运算非0的元素的迭代器。
-
可调用对象:函数、函数指针、lambda表达式
-
lambda表达式:根据函数体内唯一的return语句返回类型。
如果函数体内不只有return语句,且不指定返回类型,默认返回类型为void。
如果指定函数类型,必须采用至尾返回方式。
定义:[捕获列表](参数列表) -> 类型 {函数体}
-
向lambda传递参数:不能有默认参数,即调用时,实参与形参一一匹配。
-
lambda只有在其捕获列表里捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。
-
for_each算法:接受一对范围用来遍历,执行谓词的操作。
for_each(iter, words.end(), [](const string &str) { cout << str << endl; } );
-
上一个例子中的cout为什么可以直接用?答:cout的定义在biggies函数外。
lambda所在函数的局部变量,需要捕获列表来进行“捕获”;
lambda所在函数的局部static变量、所在函数之外的名字,不需要“捕获”。
10.3.3 lambda捕获和返回
-
当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。因此,lambda的数据成员也在lambda对象创建时被初始化。
-
值捕获:创建时拷贝值(类似于值传递)
-
引用捕获:创建时绑定变量(类似于引用传递)
使用引用捕获时应避免,所引用的变量在调用lambda之前被销毁。
引用捕获一般应用于iostream这种不能拷贝的对象。 -
显式捕获、隐式捕获:
[&, a, b]
:除了a、b是值捕获,其余全是引用捕获。
[=, &a, &b]
:除了a、b是引用捕获,其余全是值捕获。 -
可变lambda:在表达式中加上关键字mutable,使之可以改变捕获来的值。
注:如果是引用捕获,则还要看原变量本身是否是const。 -
count_if算法:接受一对迭代器,接受一个谓词并执行,返回true的计数值。
10.3.4 参数绑定
-
如果lambda表达式的捕获列表为空,通常可以用比较小的函数替代。
-
如果lambda表达式的捕获列表非空,可以使用bind函数来替代,bind函数定义在头文件functional中。
-
bind函数接受一个可调用对象,生成一个新的可调用对象。
新生成的可调用对象的参数个数<=bind里面的调用对象的参数个数。auto check = bind(check_size, _1, 6);
-
新生成的可调用对象的参数 在 bind里面的调用对象的参数列表 里使用占位符。
占位符:_1
表示 新生成的可调用对象 的第一个参数。
占位符定义在命名空间placeholders中,placeholders定义在std中:using namespace std::placeholders;
-
bind函数作用:修正参数的值,参数列表重新排序等。
-
绑定引用参数:ref、cref,定义在头文件functional中
默认情况下,bind的那些不是占位符的参数是被拷贝到bind返回的可调用对象中的。
对于像cout这种无法拷贝的对象,可以使用标准库ref函数。
标准库还有一个cref函数,生成一个保存const引用的类。for_each(words.begin(), words.end(), bind(print, ref(cout), _1, ' ') );
-
向后兼容:参数绑定
旧版本使用bind1st、bind2nd函数实现bind类似功能,新版不再支持。
10.4 再探迭代器
10.4.1 插入迭代器
-
迭代器支持的操作:
it = t
:在it指定的位置插入值t。*it
、++it
、it++
:不会对it做任何事情,返回it。
-
插入器有三种类型:
- back_inserter:创建一个使用push_back的迭代器
- front_inserter:创建一个使用push_front的迭代器
- inserter:创建一个使用insert的迭代器,此函数接受第二个参数,这个参数必须指向给定容器的迭代器。元素插入到指定元素之前。
-
只有当迭代器支持push_back时,才能使用back_inserter。
只有当迭代器支持front_back时,才能使用front_inserter。 -
迭代器工作:
-
inserter:在it之前插入元素,it再向后移动,使其仍然指向原来的元素。
*it = cal; // 与下面的写法等价 it = c.insert(it, val); ++it;
-
front_inserter:插入到第一个元素之前,首元素会更新。
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()));
-
unique_copy函数:接受第三个迭代器,表示拷贝不重复元素的目的位置。
-
10.4.2 iostream迭代器
-
istream_iterator读取输入流
一个istream_iterator使用>>来读取流。
当创建一个istream_iterator时,我们可以将它绑定到一个流。istream_iterator<int> int_t(cin); // 从cin读取int istream_iterator<int> int_eof; // 尾后迭代器 ifstream in("afile"); istream_iterator<string> str_in(in);// 从afile中读取字符串
-
下面是一个例子:
istream_iterator<int> in_iter(cin); istream_iterator<int> eof; while ( in_iter != eof ) vec.push_bck(*in_iter++); // 以上代码可以简化为: istream_iterator<int> in_iter(cin), eof; vector<int> vec(in_iter, eof);
-
输入迭代器的运算:
操作 说明 istream_iterator<int> in (is)
in从输入流is读取int值。 istream_iterator<int> end
读取int值的istream_iterator迭代器,表示尾后位置。 in1 == in2
in1和in2必须读取相同的类型才能进行 ==
或!=
的比较。若均为尾后迭代器或绑定到相同的输入,则两者相等。*in
返回从流中读取的值。 in->mem
与 (*in).mem
含义相同。++in
、in++
使用元素类型所定义的>>运算符从输入流中读取下一个值。 -
下面是一个例子:
istream_iterator<int> in(cin), eof; cout << accumulate(in, eof, 0) << endl; // 输入为1 2 3 4 5 6 7 8 9 // 输出为45
-
istream_iterator允许使用懒惰求值:
istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据。具体实现可以推迟从流中读取数据,直到我们使用迭代器时才真正读取。
另外,当两个不同对象同步读取一个流时,一定要注意度的顺序。 -
ostream_iterator向一个输出流写数据。
可以对任何具有输出运算符(<<)的类型定义ostream_iterator。操作 说明 ostream_iterator<int> out(os);
out将int值写到输出流os中 ostream_iterator<int> out(os, d);
out将int值写到输出流os中,每个值后面都输出一个d,d指向一个空字符结尾的字符数组 out = val;
用 <<
运算符将val写入到out所绑定的ostream中(但不推荐使用)*out
、++out
、out++
运算符是存在的,但不对out做任何事情。每个运算符都返回out -
下面是一个例子:
ostream_iterator<int> out_iter(cout, " "); for (auto e : vec) // 等价于out_iter = e; 但不推荐 *out_iter++ = e; cout << endl; // 代码中的for语句可以简化为: copy(vec.cbegin(), vec.cend(), out_iter); cout << endl;
-
istream_iterator支持定义了
>>
运算的类的对象;
同样,ostream_iterator支持定义了<<
运算的类的对象。
10.4.3 反向迭代器
-
反向迭代器就是容器中向首元素反向移动的迭代器。
除了forward_list外,其他容器都支持反向迭代器。
递增一个反向迭代器(++it)会移动到前一个元素;递减会移动到下一个元素。
特别注意:crbegin为尾元素迭代器,crend为首前迭代器。 -
使用反向迭代器的一个例子:
// 此例将打印9 8 7 6 5 4 3 2 1 0 vector<int> vec = {0,1,2,3,4,5,6,7,8,9}; for (auto r_iter = vec.crbegin(); // 将r_iter绑定到尾元素 r_iter != vec.crend(); ++r_iter); // crend指向首元素之前的位置 cout << *r_iter << " " << endl;
-
除了forward_list外,标准容器上的其他迭代器都既支持递增运算,又支持递减运算。
但流迭代器不支持递减运算,因为不可能从一个流中反向移动。 -
使用反向迭代器,会反向处理string中的字符。
如需正向移动,应使用reverse_iterator的base成员函数。// 此时rcomma类型是reverse_iterator,而不是iterator auto rcomma = find(line.crbegin(), line.crend(), ','); // 此语句会将最后一个单词,倒序输出 cout << string(line.crbegin(), rcomma) << endl; // 实际上,base成员是rcomma的后一个位置的“正向迭代器” // reverse_iterator无法和iterator进行比较和参与运算 // 即,rcomma + 1 != rcomma.base(); // 即,rcomma - 1 != rcomma.base(); cout << string(rcomma.base(), line.cend()) << endl;
10.5 泛型算法结构
- 迭代器按操作进行分类,形成了一种层次。除了输出迭代器之外,一个高层次类别的迭代器支持低层次类别迭代器的所有操作。
- C++指明了泛型和数值算法的每个迭代器参数的最小类别。例如:
- find算法在一个序列上进行一边扫描,对元素进行只读操作,因此至少需要输入迭代器。
- replace函数需要一对迭代器,至少是前向迭代器。
- replace_copy的前两个迭代器参数也要求至少是前向迭代器,第三个迭代器表示目的位置,必须至少是输出迭代器。
- 对每个迭代器参数来说,其能力必须与规定的最小类别至少相当。向算法传递一个能力更差的迭代器会产生错误。
10.5.1 五类迭代器
-
输入迭代器(input iterator):只读,不写,单遍扫描,只能递增
- 相等或不等:==、!=
- 前置或后置的递增或递减:++、–
- 解引用:*
- 箭头运算符:->
对于一个输入迭代器,*it++
保证是有效的,但递增它可能导致所有其他指向流的元素失效。
其结果就是,输入迭代器只能用于单遍扫描算法。
算法find和accumulate要求输入迭代器。
istream_iterator是一种输入迭代器。
-
输出迭代器(output iterator):只写,不读,单遍扫描,只能递增
- 前置或后置的递增或递减:++、–
- 解引用:*
我们只能向一个输出迭代器赋值一次。
输出迭代器只能用于单遍扫描算法。
用作目的位置的迭代器通常都是输出迭代器。
copy函数的第三个参数就是输出迭代器。
ostream_iterator类型也是输出迭代器。
-
前向迭代器(forward iterator):可读写,多遍扫描,只能递增
只能在序列中沿一个方向移动,支持所有输入和输出操作。
可以多次读写同一个元素,保存状态并多次扫描。
replace要求前向迭代器。
forward_list上的迭代器是前向迭代器。 -
双向迭代器(bidirectional iterator):可读写,多遍扫描,可递增递减
可以正向/反向读写序列中的元素,支持所有输入和输出操作。
同时,还支持前置和后置递减运算符。
算法reverse要求双向迭代器。
除了forward_list之外,其他标准库都提供符合双向迭代器要求的迭代器。 -
随机访问迭代器(random-access iterator):可读写,多遍扫描,支持全部迭代器运算
- 提供在常量时间内访问序列中任意元素的能力。
- 此类迭代器支持双向迭代器的所有功能。
- 比较操作:<、<=、>、>=
- 加减、复合运算:+、+=、-、-=
- 两个迭代器相减运算
- 下标运算。
10.5.2 算法形参形式
-
在算法分类基础上,还有参数规范,可以帮助学习新算法。
- alg(beg, end, other args);
- alg(beg, end, dest, other args);
- alg(beg, end, beg2, other args);
- alg(beg, end, beg2, end2, other args);
-
接受单个目标迭代器的算法
dest经常是插入迭代器,或ostream_iterator。
dest需保证空间是足够的。 -
接受第二个序列的算法
beg2或beg2, ned2,表示第二个输入范围。。
需保证 beg2或beg2, ned2 的范围比 beg, end 的范围一样或更大。
10.5.3 算法命名规范
-
使用重载形式传递一个谓词
接受谓词参数来代替原有算法,如:unique(beg, end); unique(beg, end, comp);
-
_if版本算法
接受谓词参数来代替原比较值。find(beg, end, val); find_if(beg, end, pred);
-
_copy版本算法
比如重排算法,普通版本会把元素重排后写回原容器。
而_copy版本会写到另一个位置,原容器元素位置不变。reverse(beg, end); reverse_copy(beg, end, dest);
-
_if和_copy同时作用:
比如remove算法:// 第一个,在输入序列中将奇数元素删除 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;});
10.6 特定容器算法
-
与其他容器不同,链表类型list和forward_list定义了几个成员函数形式的算法。
原因:list采用双向迭代器,forward_list采用前向迭代器。操作 说明 lst.merge(lst2) 将来自lst2的元素并入lst,在合并之后,lst2变为空。此版本默认使用<运算符。 lst.merge(lst2, comp) 此版本使用谓词comp lst.remove(val) 调用erase,删除与给定值相等(==)的元素 lst.remove_if(val, pred) 调用erase,删除使给定谓词为真的元素 lst.reverse() 反转lst中的元素顺序 lst.sort() 使用<比较排序元素 lst.sort(comp) 使用一元谓词comp排序元素 lst.unique() 调用erase删除同一个值的连续调用,此版本使用== lst.unique(pred) 调用erase删除同一个值的连续调用,此版本使用二元谓词pred -
splice成员:
lst.splice(args);
或flst.splice_after(args);
args类型 说明 (p, lst2) p是一个指向lst中元素的迭代器,或一个指向flst首前位置的迭代器。函数将lst2的所有元素移动到lst中p之前的位置或flst中p之后的位置。将元素从lst2中删除。lst2的类型必须与lst或flst相同,且不能是同一个链表 (p, lst2, p2) p2是一个指向lst2中位置的有效的迭代器。将p2指向的元素移动到lst中,或将p2之后的元素移动到flat中。lst2可以是lst或flst相同的链表 (p, lst2, b, e) b和e必须表示lst2中合法的范围。将给定范围中的元素从lst2移动到lst或flst。lst与lst(或flst)可以是相同的链表,但p不能指向给定范围中的元素 -
链表特有的操作会真的改变容器:
链表特有的操作remove、uniuqe都会删除元素。通用的remove、unique算法,实际上仅对迭代器进行操作。
如果想真的删除元素,需要使用容器操作,而非泛型算法。 -
类似的,链表特有的操作merge和splice会销毁其参数,元素仍然存在,但已在同一个链表中。
通用的merge和splice算法,将合并的序列写到一个给定的目的迭代器,两个输入序列是不变的。