泛型算法
- 因为它们实现共同的操作,所以称之为“算法”;而“泛型”、指的是它们可以操作在多种容器类型上。
- 泛型算法本身不执行容器操作,只是单独依赖迭代器和迭代器操作实现。
- 头文件: #include 或者 #include (算数相关)
- 大多数算法是通过遍历两个迭代器标记的一段元素来实现其功能。
- 必要的编程假定:算法永远不会改变底层容器的大小。算法可能改变容器中保存的元素的值,也可能在容器内移动元素,但不能直接添加或者删除元素。
find
auto result = find(vec.cbegin(),vec.end(),val);
//输入:两个标记范围的迭代器和目标查找值。返回:如果找到,返回对应的迭代器,否则返回第二个参数,即标记结尾的迭代器。
- 迭代器算法不依赖于容器,但算法依赖于元素类型的操作。如find在比较此元素与我们要查找的值时。
初识泛型算法
- 了解算法是否读取元素,改变元素,重排元素顺序
只读算法 find count accumulate
int sum = accumulate(vec.cbegin(),vec.cend(),0); //第三个参数是和的初值
string sum = accumulate(v.cbegin(),v.cend(),""); //const char*没有+运算符,须string("")
equal(roster1.cbegin(),roster1.cend(),roster2.cbegin());
写容器元素的算法
fill(vec.begin(),vec.end(),0);
vector<int> vec;
fill_n(vec.begin(),vec.size(),0);
//易错
vector<int> vec;
fill_n(vec.begin(),10,0); //写入10个元素,但vec中并没有元素,是空的。
back_inserter
- 一种保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器。是一种向容器中添加元素的迭代器。在头文件iterator中。
vector<int> vec;
fill_n(back_inserter(vec),10,0); //添加10个元素到vec
//fill_n向给定序列的一个元素赋值,由于传递的参数是back_inserter返回的迭代器,每次赋值都会在vec上调用push_back。
拷贝算法
- 传递给copy的目的序列至少要包含与输入序列一样多的元素。
int a1[] = {};
int a2[sizeof(a1)/sizeof(*a1)];
auto ret = copy(begin(a1),end(a1),a2); //copy返回其目的的位置迭代器递增后的值。
//replace
replace(list.cbegin(),list.cend(),0,42);//0换成42
replace_copy(list.cbegin(),list.cend(),back_inserter(ivec),0,42);
//此调用后,list并未改变,ivec包含list的一份拷贝,不过原来在list中值为0的元素在ivec中都变为42。
重排容器元素的算法
//消除重复单词
void elimDups(vector<string>& words){
sort(words.begin(),words.end());
auto end_unique = unique(words.begin(),words.end());
words.erase(end_unique,words.end()); //end_unique指向最后一个不重复元素之后的位置
}
定制操作
- sort算法默认使用元素类型的<运算符,也可重载sort的默认行为,自己定义操作来替代默认运算符。
向算法传递函数
- 谓词:是一个可调用的表达式,返回结果是一个能用作条件的值。一元谓词:接受一个参数,二元谓词:接受两个参数。
bool isShorter(const string& s1,const string& s2){
return s1.size() < s2.size();
}
sort(words.begin(),words.end(),isShorter);
//排序算法
elimDups(words);
stable_sort(words.begin(),words.end(),isShorter); //相同长度的word会保持字典序
for(const auto &s : words){ //无需拷贝字符串
cout<<s<<" ";
}
cout<<endl;
lambda表达式
- 有时可能希望操作可以接受更多的参数。
- lambda表达式表示一个可调用的代码单元,可以理解成是一个未命名的内联函数。
- 可向算法传递任何类别的可调用对象。函数和函数指针,重载了函数调用运算符的类,都是可调用对象,lambda表达式也是。
- 必须使用尾置返回
auto f = []{return 42;}; //可忽略参数列表和返回类型
cout<<f()<<endl;//42
//向lambda传递参数
[](const string& a,const string& b){return a.size() < b.size();}
stable_sort(words.begin(),words.end(),[](const string& a,const string& b){return a.size() < b.size();});
//当stable_sort需要比较两个元素时,就会调用这个lambda表达式。
//使用捕获列表
[sz](const string& a){return a.size >= sz;};
//使用find_if
auto wc = find_if(words.begin(),words.end(),[sz](const string& a){return a.size() >= sz;});
//find_if返回一个迭代器,指向第一个长度不小于给定参数sz的元素,如果不存在则返回words.end()的一个拷贝。
//计算满足size>=sz的元素的数目
auto count = words.end() - wc;
cout<<count<<make_plural(count,"word","s")<<endl;
//for_each算法
//打印长度大于等于给定值的单词,每个单词后面接一个空格。
for_each(wc,words.end(),[](const string& s){cout<<s<<" ";});
cout<<endl;
//完整的程序
void biggies(vector<string>& words,vector<string>::size_type sz){
elimDups(words);
stable_sort(words.begin(),words.end(),[](const string& a,const string& b){return a.size() < b.size();});
auto wc = find_if(words.begin(),words.end(),[sz](const string& a){return a.size()>=sz;});
auto count = words.end() - wc;
for_each(wc,words.end(),[](const string& s){cout<<s<<" ";});
cout<<endl;
}
lambda捕获和返回
- 定义一个lambda时,编译器生成一个与lambda对应的新的未命名的类类型,当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象。传递的参数是此编译器生成的类类型的未命名对象。类似的,当使用auto定义一个用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;};
v1 = 0;
auto j = f2(); //j为0,f2保存v1的引用,而非拷贝
}
//引用捕获的必要性
void biggies(vector<string>& words,vector<string>::size_type sz,ostream& os = cout,char c = ' '){
for_each(words.begin(),words.end(),[&os,c](const string& s){os<<s<<c;});
}
- 可以从一个函数返回lambda。
隐式捕获
- 让编译器根据lambda体中的代码来推断我们要使用那些变量,=,&
wc = find_if(words.begin(),words.end(),[=](const string& s){return s.size() >= sz;});
//sz为隐式捕获,=表示值捕获方式,&表示捕获引用方式。
//部分变量值捕获,其他变量引用捕获,可以混合使用隐式捕获和显式捕获
void biggies(vector<string>& words,vector<string>::size_type sz,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;});
//os显式引用捕获,c隐式值捕获
}
//当混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个&或=。
//隐式捕获用了&,则显式捕获命名变量必须采用值方式。如果隐式捕获采用的是值方式使用了=,则显式捕获命名变量必须采用引用方式。
[] //空捕获列表。lambda不能使用所在函数中的变量。一个lambda只有在捕获变量后才能使用它们。
[names] //names是一个逗号分隔的名字列表,这些名字都是在lambda所在函数的局部变量,捕获列表中的变量都被拷贝,名字前如果使用了&,则采用引用捕获方式。
[&] //隐式捕获列表,采用引用捕获方式。lambda体中所使用的来自所在函数的实体都采用引用方式使用。
[=] //隐式捕获列表,采用值捕获方式。
[&, identifier_list] //identifier_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量。这些变量采用值捕获方式,而任何隐式捕获的变量都采用引用方式捕获。identifier_list中的名字前面不能使用&
[=, identifier_list] //identifier_list中的变量采用引用方式捕获,而任何隐式捕获的变量都采用值方式捕获。identifier_list中的名字不能包括this,且前面必须使用&
可变lambda
//对于一个值被拷贝的变量,lambda不会改变其值
void fcn3(){
size_t v1 = 42;
auto f = [v1] () mutable {return ++v1;};
v1 = 0;
auto j = f(); //j为43
}
void fcn4(){
size_t v1 = 42;
auto f2 = [&v1]{return ++v1;};
v1 = 0;
auto j = f2(); //j为1
}
指定lambda返回类型
- 默认情况,如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void。被推断返回void的lambda不能返回值。
transform(vi.begin(),vi.end(),vi.begin(),[](int i){return i<0 ? -i : i;});
transform(vi.begin(),vi.end(),vi.begin(),[](int i){if(i<0) return -i else return i;});
//错误:不能推断lambda的返回类型。推断返回void,但实际上返回了int。
//当需要为一个lambda定义返回类型时,必须使用尾置返回类型
transform(vi.begin(),vi.end(),vi.begin(),[](int i)->int{if(i<0) return -i else return i;});
参数绑定
- 那种只在一两个地方使用的简单操作,lambda表达式是最有用的。如需要在很多地方使用相同的操作,通常应定义一个函数,而不是多次编写相同的lambda表达式。
- lambda捕获列表为空,通常可以用函数来替代它,如可用lambda或者isShorter函数实现将vector中单词按长度排序。但对于捕获局部变量的lambda,用函数替代它就不那么容易了。
bool check_size(const string& s,string::size_type sz){
return s.size() <= sz;
}
- 前面find_if接受一个一元谓词,因此传递给find_if的可调用对象必须接受单一参数。biggies传递给find_if的lambda使用捕获列表来保存sz。
//标准库bind函数 #include<functional>
auto check6 = bind(check_size,_1,6); //_1占位符
string s = "hello";
bool b1 = check6(s);//check6(s)会调用check_size(s,6)
auto wc = find_if(words.begin(),words.end(),[sz](const string& a){});
auto wc = find_if(words.begin(),words.end(),bind(check_size,_1,sz));
使用placeholders名字
- 名字_n都定义在名为placeholders的命名空间中,这个命名空间本身定义在std命名空间中。
//_1对应的using声明
using std::placeholders::_1;//对每个_n单独声明麻烦
using namespace std::placeholders;
bind的参数
auto g = bind(f,a,b,_2,c,_1);
即将g(_1,_2)映射成f(a,b,_2,c,_1)
//即对g的调用会调用f,用g的参数替代占位符,再加上绑定的参数a,b,c。
用bind重排参数顺序
sort(words.begin(),words.end(),isShorter);//单词长度由短至长排序 isShorter(A,B)
sort(words.begin(),words.end(),bind(isShorter,_2,_1));//由长至短 相当于调用isShorter(B,A)
绑定引用参数
for_each(words.begin(),words.end(),[&os,c](const string& s){os<<s<<c;});
ostream& print(ostream& os,const string& s,char c){
return os<<s<<c;
}
//错误:不能拷贝os
for_each(words.begin(),words.end(),bind(print,os,_1,' '));
//原因是bind拷贝其参数,我们不能拷贝一个ostream,我们希望传递给bind一个对象而又不拷贝它,必须使用标准库ref函数。
for_each(words.begin(),words.end(),bind(print,ref(os),_1,' '));
//函数ref返回一个对象,包含给定的引用,此对象是可以拷贝的。
//标准库中还有函数cref,生成一个保存const引用的类。ref,cref定义在头文件functional中。
再探迭代器
- 标准库在头文件iterator中还定义了额外几种迭代器。
- 插入迭代器:insert iterator:这些迭代器被绑定到一个容器上,可用来向容器插入元素。
- 流迭代器 stream iterator:绑定到输入或输出流上,可用来遍历所关联的IO流。
- 反向迭代器 reverseiterator:这些迭代器向后而不是向前移动。除forward_list之外的标准库容器都有反向迭代器。
- 移动迭代器 move iterator:不拷贝其中的元素,而是移动它们。
插入迭代器
- 是一种迭代器适配器,接受一个容器,生成一个迭代器,能向给定容器添加元素。
it=t //在it指定的当前位置插入值t。假定c是it绑定的容器,依赖于插入迭代器的不同种类,此赋值会分别调用c.push_back(t)、c.push_front(t)、c.insert(t, p),其中p是传递给inserter的迭代器位置
*it, ++it, it++ //这些操作虽然存在,但不会对it做任何事情,每个操作都返回it
- 插入迭代器有三种类型,差异在于元素插入的位置:
- back_inserter:创建一个使用push_back的迭代器。
- front_inserter:创建一个使用push_front的迭代器。
- inserter:创建一个使用inserter的迭代器。函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前。
//it是inserter生成的迭代器
*it = val;//效果与下面代码一样
it = c.insert(it,val);
++it;//指向原来的元素
list<int> lst = {1,2,3,4};
list<int> lst2,lst3;
copy(lst.cbegin(),lst.cend(),front_inserter(lst2)); //包含4,3,2,1
copy(lst.cbegin(),lst.cend(),inserter(lst3,lst3.begin());//1,2,3,4
iostream迭代器
- istream_iterator和ostream_iterator
istream_iterator<int> int_it(cin); //从cin读取int
istream_iterator<int> int_eof; //尾后迭代器
ifstream in("afile");
istream_iterator<string> str_it(in); //从 afile 读取字符串
istream_iterator<int> in_iter(cin);
istream_iterator<int> eof;
while(in_iter != eof)
vec.push_back(*in_iter++);
istream_iterator<int> in_iter(cin),eof;
vector<int> vec(in_iter,eof); //从迭代器范围构造vec
//istream_iterator操作
istream_iterator<T> in(is); //in从输入流is读取类型为T的值
istream_iterator<T> end; //读取类型是T的值的istream_iterator迭代器,表示尾后位置
in1 == in2 //in1和in2必须读取相同类型。如果他们都是尾后迭代器,或绑定到相同的输入,则两者相等。
in1 != in2 //类似上条
*in //返回从流中读取的值
in->mem //与*(in).mem含义相同
++in, in++ //使用元素类型所定义的>>运算符从流中读取下一个值。前置版本返回一个指向递增后迭代器的引用,后置版本返回旧值。
使用算法操作流迭代器
istream_iterator<int> in(cin),eof;
cout<<accumulate(in,eof,0)<<endl;
//此调用会计算出从标准输入读取的值的和。
- istream_iterator允许使用懒惰求值。绑定到一个流时,标准库并不保证迭代器立即从流读取数据。可推迟直到我们使用迭代器时才真正读取。
ostream_iterator操作
ostream_iterator<T> out(os); //out将类型为T的值写到输出流os中
ostream_iterator<T> out(os, d); //out将类型为T的值写到输出流os中,每个值后面都输出一个d。d指向一个空字符结尾的字符数组。
out = val //用<<运算符将val写入到out所绑定的ostream中。val的类型必须和out可写的类型兼容。
*out, ++out, out++ //这些运算符是存在的,但不对out做任何事情。每个运算符都返回out。
ostream_iterator<int> out_iter(cout," ");
for(auto e : vec){
*out_iter++ = e; //将元素写到cout
}
cout<<endl;
for(auto e : vec)
out_iter = e;//与上面等价,可忽略解引用和递增运算。
cout<<endl;
//可通过调用copy来打印vec中的元素,比循环更为简单
copy(vec.begin(),vec.end(),out_iter);
cout<<endl;
使用流迭代器处理类类型
istream_iterator<Sales_item> item_iter(cin),eof;
ostream_iterator<Sales_item> out_iter(cout,"\n");
Sales_item sum = *item_iter++;
while(item_iter != eof){
if(item_iter->isbn() == sum.isbn()){
sum += *item_iter++;
}else{
out_iter = sum;
sum = *item_iter++;
}
}
out_iter = sum;
反向迭代器
- 反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。
- 对于反向迭代器,递增和递减的操作含义会颠倒。
- 通过rbegin,rend,crbegin,crend获得反向迭代器。返回指向容器尾元素和首元素之前一个位置的迭代器。
vector<int> vec = {0,1,2,3,4,5,6,7,8,9};
for(auto r_iter = vec.crbegin();r_iter != vec.crend();++r_iter){
cout<<*r_iter<<endl;
}
sort(vec.begin(),vec.end());//从小到大
sort(vec.rbegin(),vec.rend());//最小元素放置在vec的末尾
auto comma = find(line.cbegin(),line.cend(),',');
cout<<string(line.cbegin(),comma)<<endl;//打印第一个单词
auto rcomma = find(line.crbegin(),line.crend(),',');//打印最后一个单词
cout<<string(line.crbegin(),rcomma)<<endl; //错误,rcomma是反向迭代器,会输出逆序
cout<<string(rcomma.base(),line.cend())<<endl;// base成员函数返回其对应的普通迭代器
//rcomma和rcomma.base()指向不同的元素,rcomma指向逗号,而rcomma.base()转换成普通迭代器后,指向逗号后面的元素。
泛型算法结构
- 迭代器类别
输入迭代器 //只读,不写;单遍扫描,只能递增 ==,!=,++,*,->
输出迭代器 //只写,不读;单遍扫描,只能递增 ++,*
前向迭代器 //可读写;多遍扫描,只能递增 ==,!=,++,*,->
双向迭代器 //可读写;多遍扫描,可递增递减 ==,!=,++,--,*,->
随机访问迭代器 //可读写,多遍扫描,支持全部迭代器运算 ==,!=,<,<=,>,>=,++,--,+,+=,-,-=,*,->,iter[n]==*(iter[n])
- 算法形参模式
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都是迭代器参数,是否使用要依赖于执行的操作。
- 算法命名规范
unique(beg,end); //使用==运算符比较
unique(beg,end,comp); //使用comp比较元素
//_if版本的算法
find(beg,end,val);
find_if(beg,end,pred);//查找一个令pred为真的元素
//区分拷贝元素的版本和不拷贝的版本 _copy版本
reverse(beg,end);//反转输入范围中元素的顺序
reverse_copy(beg,end,dest);//将元素按逆序拷贝到dest
//从v1中删除奇数元素
remove_if(v1.begin(),v1.end(),[](int i){return i % 2;});
remove_copy_if(v1.begin(),v1.end(),back_inserter(v2),[](int i){return i % 2;});
特定容器算法
- list和forward_list成员函数版本的算法
lst.merge(lst2) //将来自lst2的元素合并入lst,二者都必须是有序的,元素将从lst2中删除。
lst.merge(lst2, comp) //同上,给定比较操作。
lst.remove(val) //调用erase删除掉与给定值相等(==)的每个元素
lst.remove_if(pred) //调用erase删除掉令一元谓词为真的每个元素
lst.reverse() //反转lst中元素的顺序
lst.sort() //使用<排序元素
lst.sort(comp) //使用给定比较操作排序元素
lst.unique() //调用erase删除同一个值的连续拷贝。使用==。
lst.unique(pred) //调用erase删除同一个值的连续拷贝。使用给定的二元谓词。
- splice成员,链表类型定义了splice算法。
//lst.splice(args)或flst.splice_after(args)
(p, lst2) //p是一个指向lst中元素的迭代器,或者一个指向flst首前位置的迭代器。函数将lst2中的所有元素移动到lst中p之前的位置或是flst中p之后的位置。将元素从lst2中删除。lst2的类型必须和lst相同,而且不能是同一个链表。
(p, lst2, p2) 同上,p2是一个指向lst2中位置的有效的迭代器,将p2指向的元素移动到lst中,或将p2之后的元素移动到flst中。lst2可以是于lst或flst相同的链表。
(p, lst2, b, e) b和e表示lst2中的合法范围。将给定范围中的元素从lst2移动到lst或first中。lst2与lst可以使相同的链表,但p不能指向给定范围中的元素。
- 链表特有的操作会改变容器。