S10泛型算法
一、概述
1、算法主要定义在algorithm头文件中,另外在numeric头文件中定义了数值泛型算法
2、一般情况下,算法不直接操作容器,而是遍历由两个迭代器指定的元素范围(左闭右开区间)
auto result = find(ia+1, ia+4, val); //在ia[1]、ia[2]和ia[3]中寻找val
3、泛型算法的核心:泛型算法只运行在迭代器上,通过迭代器间接访问容器,不会直接执行容器的操作,即算法对容器产生的影响取决于提供的迭代器能完成的操作,如普通迭代器不能添加删除元素而back_inserter
能调用容器操作添加新元素,泛型能使用于各种类型,只需要提供迭代器,算法根本不知道容器的存在
二、初识泛型算法
1、标准库提供了超过100个算法,参考《C++primer 5th》p.770,这里只简要列出书中提到的算法,衍生类型均在附录中
2、只读算法
find(beg, end, val) //在[beg,end)中寻找==val的对象,比较操作可以重载
accumulate(beg, end, val) //在[beg,end)内累+,val为累加初值,val的类型决定了累加过程的类型
equal(beg, end, beg2) //在[beg,end)内依次比较beg和beg2开始的元素是否相等,beg2开始的元素不少于[beg,end)
string sum = accumulate(beg, end, string("")); //string也定义了+,因此可以accumulate
注意:泛型算法对类型无关,只要具体类型定义了(或自定义)算法要求的操作,算法就可以使用
注意:对于那些只接受单一迭代器来表示第二个序列的算法,都假定第二个序列不短于第一个序列
3、写容器元素的算法
vector<int> vec;
vec.reserve(10); //将vec的容量capacity提升到10
fill_n(vec.begin(), 10, 0); //错误,reserve将vec的capacity=10但是size=0,要求size不能少于写入个数
fill_n(back_inserter(vec), 10, 0); //正确
注意:写容器元素要求容器原先元素个数不能少于算法要写入的个数
4、重排容器元素的算法
sort(words.begin(), words.end()); //按字典对词汇排序,使得相同的单词出现在邻近
auto end_unique = unique(words.begin(), words.end()); //unique去除邻近重复,所有不重复单词都出现在前段
//返回迭代器指向最后单词的后一个,此后元素是未知的
words.erase(end_unique, words.end(); //由于unique完成重排,并不真正删除多余元素,这里调用erase删除
三、定制操作
1、谓词:可调用的表达式,返回结果是一个能用作条件的值,标准库算法所使用的谓词有一元谓词和二元谓词两类,接受谓词参数的算法对输入序列中的元素调用谓词,因此元素类型必须能转换为谓词的参数类型
2、lambda表达式:表示一个可调用的代码单元,可以理解为一个未命名的内联函数;一个lambda表达式具有一个返回类型、一个参数列表和一个函数体
[capture list](parameter list) -> return type { function body }
1.capture list:捕获列表,即lambda表达式外的变量,通常为空
2.parameter list/return type/function body:与普通函数意义相同
3.->:lambda表达式必须用尾置返回来指定返回类型
(1)lambda中可以忽略参数列表和返回类型,但必须包括捕获列表和函数体
auto f = [] { return 42; };
cout << f() << endl; //output 42
(2)lambda中如果忽略参数列表,等于定义了空参数列表;如果忽略了返回类型,进而如果函数体只有单一return
则根据return
自动推断返回类型,如果函数体不只有单一return
还有任何其他语句则返回void
(3)lambda的形参数目必须与实参相等,lambda不允许存在默认实参
(4)lambda必须只有在其捕获列表中捕获一个lambda所在函数中的局部变量,才能在函数体中使用该变量,而对于局部static变量和lambda之外声明的名字如cout
等则可以直接在函数体中使用
int sz = 1;
auto f = [](const string &s){return s.size() >= sz;}; //错误,sz未捕获
auto f = [sz](const string &s){return s.size() >= sz;}; //正确,f中保存了sz的拷贝
3、lambda捕获与返回
(1)值捕获:采用值捕获要求变量可以被拷贝,当lambda对象创建时立刻将变量拷贝,而不是在调用时拷贝,因此在后续修改变量时不会影响到lambda内部对应的值
(2)引用捕获:在lambda内是对变量的引用,因此每次调用都是引用该变量,但是由于lambda调用时变量的值可能已被修改,为了确保功能正常,lambda应尽可能简化捕获的数据量同时避免捕获引用或指针
(3)隐式捕获:由编译器推断捕获列表,=
表示隐式值捕获,&
表示隐式引用捕获,显式/隐式捕获可以混合使用
[] //空捕获列表,lambda不能使用其所在函数的局部非static变量
[names] //names是逗号分隔的名字列表,默认时均值捕获,名字前有&则引用捕获
[&] //隐式引用捕获
[=] //隐式值捕获
[&, identifier_list] //identifier_list是逗号分隔的名字列表,采用值捕获且不允许有&,列表外的均隐式引用捕获
[=, identifier_list] //列表内名字前必须有&且列表中不能有this,采用引用捕获,列表外的均采用隐式值捕获
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;}); //os显式引用捕获,c隐式值捕获
(4)可变lambda:对于值捕获的变量,lambda不会改变拷贝过来变量的值,如果需要改变,则必须在参数列表后加上mutable
auto f = [v1] () mutable -> int { return ++v1; }; //有mutable时才可以改变内部v1,与外部原v1无关
(5)指定lambda返回类型:对于函数体只有一句return
的可以省略返回类型,具体类型由编译器推断得到,而不是只有一句return
的lambda表达式的返回类型默认都是void
,必须显式指出返回类型,返回类型在mutable
(如果有的话)后方
transform(..., [](int i){if(i<0) return -i; else return i;}); //错误,默认返回void
transform(..., [](int i) -> int {if(i<0) return -i; else return i;}); //正确,显式指出返回int
4、参数绑定
(1)标准库bind
函数:定义在头文件functional中,可以将bind
函数看作一个通用的函数适配器,接受一个可调用对象,生成一个新的可调用对象来适配原函数的参数列表,参数列表中_1/_2/.../_n
这些占位符都定义在placeholders
命名空间中,而这个命名空间定义在std
命名空间中,因此使用时需要加上using namespace std::placeholders;
auto newCallable = bind(callable, arg_list);
1.callable:原可调用对象
2.arg_list:传递给callable的参数列表,arg_list会有_n开头的占位参数,代表newCallable传进来的第n个参数
3.newCallable:新可调用对象,当调用newCallable时参数按次序填入_n,然后与arg_list一起调用callable
auto check6 = bind(check_size, _1, 6);
check6(s); //此时s会作为_1,与6一起调用check_size,即check6(s)就是check_size(s, 6)
(2)bind
参数:bind
参数中_n
是newCallable
的第n个参数,可以在调用callable
时进行重排,对于非_n
的参数默认是拷贝的,当要绑定非拷贝的引用参数时,必须使用标准库的ref
函数(对应的cref
函数生成const
引用)
auto g = bind(f, a, b, _2, c, _1);
g(X, Y); //等价调用f(a, b, Y, c, X);
auto x = bind(print, os, _1, ' '); //错误,输出流os不能拷贝
auto x = bind(print, ref(os), _1, ' '); //正确,生成了os的引用
bind1st(fun, arg); //将二元算子转换为一元算子,同时传进来的arg放在fun参数列表第一位(1st)
bind2nd(fun, arg); //将二元算子转换为一元算子,同时传进来的arg放在fun参数列表第二位(2nd)
四、再探迭代器
1、除普通迭代器外的特殊迭代器
- 插入迭代器:被绑定在容器上,可以用来向容器插入元素
- 流迭代器:被绑定在输入/输出流上,可以用来遍历关联的I/O流
- 反向迭代器:从尾向头移动,除
forward_list
外的标准库容器都支持反向迭代器 - 移动迭代器:这些专有的迭代器用于移动元素而不是拷贝元素
2、插入迭代器:插入器是一种迭代器适配器,接受一个容器生成一个迭代器能实现向给定的容器添加元素,通过插入器添加元素时,插入器调用容器操作来完成插入元素
it = t //it是插入迭代器,在it指定的当前位置插入t
//依赖于插入迭代器的不同种类,这个插入操作实际调用不同的容器操作来完成插入
*it/++it/it++ //*/++在这里都重载,结果是这些操作都返回it本身不做任何改变,因此*it=val等价于it=val
back_inserter(container) //创建使用push_back的插入迭代器
front_inserter(container) //创建使用push_front的插入迭代器
inserter(container, iterator) //创建使用insert的插入迭代器,iterator是指向container的非const迭代器
it = inserter(c, iter);
*it = val; //也可以用it = val
//效果等同于
it = c.insert(it ,val);
++it;
list<int> lst = {1,2,3,4};
list<int> lst1 = {5}, lst2 = {5}, lst3 = {5};
copy(lst.cbegin(), lst.cend(), front_inserter(lst1)); //lst1: 4 3 2 1 5
copy(lst.cbegin(), lst.cend(), back_inserter(lst2)); //lst2: 5 1 2 3 4
copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin())); //lst3: 1 2 3 4 5
3、iostream迭代器:可以为任何定义了输入运算>>
/输出运算<<
的类型创建istream_iterator/ostream_iterator
对象
(1)iostream的迭代器istream_iterator/ostream_iterator
将对应的输入/输出流当作一个特定类型的元素序列来处理,当使用默认初始化时创建的迭代器可以当作尾后迭代器
istream_iterator<int> int_it(cin); //从cin读取int的迭代器
istream_iterator<int> int_eof; //默认初始化创建尾后迭代器int_eof
ifstream in("file"); //创建文件输入流in
istream_iterator<string> str_it(in); //从文件file的输入流in读取字符串的迭代器
while(int_it != int_eof) //当有数据可供读取时
vec.push_back(*int_it++); //解引用*迭代器获得从流读取的前一个值,后置++运算读取流
//等价于
vector<int> vec(int_it, int_eof); //直接从流迭代器范围来创建vector对象vec
注意:对于一个绑定到流的迭代器,一旦其关联的流遇到文件尾或遇到IO错误,迭代器的值就与尾后迭代器相等
(2)istream_iterator
迭代器定义、初始化与操作
istream_iterator<T> in(is); //in从输入流is读取类型为T的值
istream_iterator<T> end; //读取类型为T的输入流尾后迭代器
in1 ==/!= in2 //读取类型相同下,绑定到相同的输入或都是尾后迭代器,则两者相等
*in //返回从输入流中读取的值
++in/in++ //使用元素类型定义的>>从输入流中读取下一个值
注意:istream_iterator允许使用懒惰求值,即不一定立即从流读取数据,可以推迟,但确保在解引用时已经完成读取数据
(3)ostream_iterator
迭代器定义、初始化与操作
ostream_iterator<T> out(os); //out将类型为T的值写到输出流os中
ostream_iterator<T> out(os, d); //写到输出的时候,每个值后都输出一个d,d必须是C风格字符串(字面值或字符串指针)
out = val //val的类型必须与T类型兼容,将val写到out所绑定的输出流os中
*out/++out/out++ //不做任何操作,都返回out自身
注意:out不能默认初始化,不存在输出流的尾后迭代器
(4)使用流迭代器处理类类型
4、反向迭代器:反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器,可以通过调用reverse_iterator
中的base
将反向迭代器转换为普通迭代器
vector<int> a{ 1,2,3,4,5,6 };
auto rit = a.rbegin(); //rit指向6
rit++; //rit指向5
cout << "*rit = " << *rit << endl; //*rit = 5
cout << "*rit.base() = " << *rit.base() << endl; //*rit.base() = 6
注意:由于普通迭代器和反向迭代器都符合右闭合区间,因此反向迭代器转换后的普通迭代器和原反向迭代器所指的元素相邻而并不是同一个
五、泛型算法结构
1、5类迭代器:由于泛型算法是运行在迭代器上的,因此任何算法最基本的特性就是它要求其迭代器提供哪些操作,根据算法的要求可以将迭代器分为五类
- 输入迭代器input:只读,不写,单遍扫描,只能递增
- 输出迭代器output:只写,不读,单遍扫描,只能递增
- 前向迭代器forward:可读写,多遍扫描,只能递增
- 双向迭代器bidirectional:可读写,多遍扫描,可递增递减
- 随机访问迭代器random_access:可读写,多遍扫描,支持全部迭代器运算
注意:向算法传递错误类别的迭代器不会被编译器警告
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
的算法都假定目标空间足够容纳写入的数据
注意:接受第二个输入序列beg2
的算法都假定从beg2
开始的序列与[beg,end)所表示的范围至少一样大
3、算法命名规范
(1)使用重载形式传递谓词
(2)后缀_if
版本的算法:接受一个元素值val
的算法往往有后缀_if
的版本接受一个谓词表判断
(3)区分拷贝元素和不拷贝元素的版本:拷贝元素则原序列不变,新序列写至另一个地方,后缀为_copy
六、特定容器算法
1、链表类型list
和forward_list
特别定义了一些算法作为成员函数,由于链表类型迭代器功能的限制,因此通用的一些算法无法应用到链表上,链表则特别定义了一组算法
注意:一般情况下,链表优先选用成员函数算法,通用算法会带来性能的下降
2、通用算法对应的链表成员函数算法
lst.merge(lst2) //lst/lst2都必须是有序的,将lst2的元素并入lst,操作完成后lst2为空,默认采用<,返回void
lst.merge(lst2, comp) //同上,采用comp比较
lst.remove(val) //调用erase删除与给定val相等的元素,默认采用==
lst.remove_if(pred)
lst.reverse() //翻转lst中的元素
lst.sort() //排序lst,默认采用<
lst.sort(comp)
lst.unique() //调用erase删除同一个值的连续拷贝,默认采用==
lst.unique(pred)
3、链表独有的成员函数算法
lst.splice(args)/flst.splice_after(args)
(p, lst2) //p指向链表中的元素;splice将lst2所有元素移动到lst中的p之前或flst中的p之后
//移动的元素将从lst2中删除;lst2必须与lst/flst链表类型元素类型都相同,且不能是同一个链表
(p, lst2, p2) //p2指向链表中的元素;如果lst2是list,splice将p2所指元素移动到lst中p所指元素之前;
//如果lst2都是forward_list,splice将p2之后的一个元素移动到lst中p所指元素之后;
//lst2必须与lst/flst链表类型元素类型都相同,可以是同一个链表
(p, lst2, b, e) //b和e必须表示lst2中的合法范围,将范围内的元素从lst2移动到lst中的p之前或flst中的p之后;
//lst2必须与lst/flst链表类型元素类型都相同,可以是同一个链表(此时p不能指向b和e范围内)
list<int> lst{ 1,2,3 }, lst2{ 7,8,9 };
forward_list<int> flst{ 1,2,3 }, flst2{ 7,8,9 };
-----test1-----
lst.splice(lst.begin(), lst2); //lst: 7 8 9 1 2 3 lst2: empty
flst.splice_after(flst.begin(), flst2); //flst: 1 7 8 9 2 3 flst2: empty
-----test2-----
lst.splice(lst.begin(), lst2, lst2.begin()); //lst: 7 1 2 3 lst2: 8 9
flst.splice_after(flst.begin(), flst2, flst2.begin()); //flst: 1 8 2 3 flst2: 7 9
-----test3----
lst.splice(lst.begin(), lst2, lst2.begin(), lst2.end()); //lst: 7 8 9 1 2 3 lst2: empty
flst.splice_after(flst.begin(), flst2, flst2.begin(), flst2.end()); //flst: 1 8 9 2 3 flst2: 7
注意:多数链表特有的算法都与通用版本相似但不相同,一个重要区别在于链表版本会改变底层的容器,例如merge
等算法通用版本不会销毁参数而链表版本会销毁参数