C++ primer学习笔记——第十章 泛型算法

标准库容器定义的操作集合惊人的小。标准库并未给每个容器添加大量功能,而是提供了一组算法,这些算法中的大多数都独立于任何特定的容器。这些算法是通用的(generic,或称泛型的):它们可以用于不同类型的容器和不同类型的元素

一、概述

大多数算法都定义在头文件algorithm中。标准库还在头文件中定义了一组数值泛型算法。

一般情况下,这些算法并不直接操作容器,而是遍历两个迭代器指定的一个元素范围来操作。通常情况下,算法遍历范围,对其中每个元素进行一些处理。

迭代器令算法不依赖于容器,但算法依赖于元素类型的操作

算法永远不会改变底层容器的大小。算法可能改变容器中保存的元素,也可能在容器中移动元素,但永远不会直接添加或删除元素。

二、初识泛型算法

1、只读算法

find、count、accumulate:

//对vec中的元素求和,和的初值是0
//只是读取而不改变元素,最好使用cbegin()/cend()
int sum=accumulate(vec.cbegin(),vec.cend(),0);

accumulate的第三个参数的类型决定了函数中使用哪个加法运算符以及返回值的类型。

算法和元素类型

//正确:string定义了+运算符
string sum=accumulate(v.cbegin(),v.cend(),string(""));
//错误:const char*上没有定义+运算符
string sum=accumulate(v.cbegin(),v.cend(),"");

操纵两个序列的算法

//roster2中的元素数目应该至少与roster1一样多
equal(roster1.cbegin(),roster1.cend(),roster2.cbegin());

由于equal通过迭代器完成操作,因此两个容器的类型不必相同,元素类型也不必相同,只要能够用==来比较两个元素类型即可。例如:vector<string>与list<const char*>.

那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。

2、写容器元素的算法

fill(vec.begin(),vec.end(),0); //将每个元素置0
fill(vec.begin(),vec.begin()+vec.size()/2,10); //容器前一半置为10

算法不检查写操作

一些算法接受一个迭代器来指出一个单独的目的位置,新值从目的迭代器的位置开始插入。

向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素:(举个反例)

vector<int> vec;  //空向量
fill_n(vec.begin(),10,0);   //灾难:修改vec中的10个(不存在的)元素

介绍back_inserter

插入迭代器是一种向容器中添加元素的迭代器,它是定义在头文件iterator中的一个函数

back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中:

vector<int> vec;
auto it=back_inserter(vec);
*it=42; //vec中现在有一个元素,42

拷贝算法

copy接受三个迭代器:前两个表示一个输入范围,第三个表示目的序列的起始位置。copy返回的是其目的位置迭代器(递增后)的值:

int a1[]={0,1,2,3,4,5,6,7,8,9};
int a2[sizeof(a1)/sizeof(*a1)]; //a2与a1一样大
//ret指向拷贝到a2的尾元素之后的位置
auto ret=copy(begin(a1),end(a1),a2);   

replace接受四个参数,将等于第一个值得元素替换为第二个值:

//将所有的0改为42
replace(ilst.begin(),ilist.end(),0,42);

如果希望保留原序列不变,可以调用replace_copy。此算法接受额外第三个迭代器参数,指出调整后序列的保存位置:

//使用back_inserter按需要增长目标序列
replace_copy(ilst.begin(),ilst.end(),back_inserter(vec),0,42);

此调用后,ilst并未改变,ivec包含ilst的一份拷贝,不过原来在ilst中值为0的元素在ivec中都变为42.

3、重排容器元素的算法

调用sort会重排容器中元素的顺序,使之有序,它是利用元素类型的<运算符来实现排序的。

排序完成后,我们可以使用unique重排输入序列,将相邻的重复项“消除”,并返回一个指向不重复值范围末尾的迭代器。此位置之后的元素仍然存在,但是我们并不知道它们的值是什么。

void elimDups(vector<string> &words)
{   //按照字典顺序重排words,以便于查找重复单词
    sort(words.begin(),words.end());
    //unique重排输入范围,使每个单词只出现一次,且排列在范围的前部
    auto end_unique=unique(words.begin(),words.end()));
    //删除重复单词
    words.erase(end_unique,word.end());
}

三、定制操作

1、向算法传递函数

我们可以按照长度重排vector,所以要重载sort。此时,sort接受第三个参数,称为谓词。

谓词

谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。根绝它们接受的参数数量,谓词可分为一元谓词和二元谓词。

接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换为谓词的参数类型。

//比较函数,用来按长度排序单词
bool isShorter(const string &s1,const string &s2)
{
    return s1.size()<s2.size();
}

sort(words.begin(),words.end(),isShorter);

排序算法

如果我们还希望相同长度的元素按照字典顺序排列,可以使用stable_sort算法:

elimDups(words);
//按照长度重新排序,长度相同的单词维持字典序
stable_sort(words.begin(),words.end(),isShorter);

2、lambda表达式

在上一个例子基础上继续提出要求,求大于等于一个给定长度的单词有多少。我们还会修改输出,使程序只打印大于等于给定长度的单词。

find_if算法接受一对迭代器表示一个范围,另外,第三个参数是一个谓词。它返回第一个使谓词返回为0的元素的迭代器,如果不存在这样的元素,则返回尾迭代器。

lambda表达式

对于一个对象或者一个表达式,如果可以对其使用调用表达式(()),则可以称它为可调用对象。

可调用对象有四种:函数、函数指针、重载了函数调用运算符的类、lambda表达式

可以将lambda表达式理解为一个未命名的内联函数。但与函数不同,lambda可能定义在函数内部。

一个lambda表达式具有以下形式:

[捕获列表](参数列表)->返回类型  {函数体}

捕获列表是一个lambda所在函数中定义的局部变量的列表(通常省略)。列表参数和返回类型可以忽略

与普通函数不同,lambda必须使用尾置返回来指定返回类型。

auto f=[] {return 42;};
cout<<f()<<endl;  //打印42

如果lambda的函数体包含任何单一return语句之外的内容,且未指定返回类型,则返回void

向lambda传递参数

与普通函数不同,lambda不能有默认参数,因此,一个lambda调用的实参数目必须永远与形参数目相等。

使用一个与isShorter函数功能相同的lambda来调用stable_sort:

//按长度排序,长度相同的单词维持字典序
stable_sort(words.begin(),words.end(),
            [](const string &s1,const string &s2)
             {return s1.size()<s2.size();});

使用捕获列表

一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。

[sz] (const string &a)
     {return a.size()>=sz;};  //因为是一条语句,结尾处需要有分号

//错误:sz未捕获
[] (const string &a)
     {return a.size()>=sz;}; 

捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和在它函数之外声明的名字。

for_each算法

此算法接受一个可调用对象,并对输入序列中每个元素调用此对象:

for_each(wc,words.end(),
             [](const string &s){cout<<s<<" ";});

完整的biggies

void biggies(vector<string> &words,
             vector<string>::size_type sz)
{
    elimDups(words);
    //按长度排序,长度相同的单词维持字典序
    stable_sort(words.begin(),words.end(),
                [](const string &s1,const string &s2)
                {return s1.size()<s2.size();});
    //获取一个迭代器,指向第一个满足size()>=sz的元素
    auto wc=find_if(words.begin(),words_end(),
                    [sz](const string &a)
                         {return a.size()>=sz;});
    auto count=words.end()-wc;
    cout<<count<<" "<<make_plural(count,"word","s")
        <<" of length "<<sz<<" or longer"<<endl;
    for_each(wc,words.end(),
             [](const string &s){cout<<s<<" ";});
    cout<<endl;
}

3、lambda捕获和返回

值捕获

与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝,因此在捕获之后对其修改不会影响到lambda内对应的值

引用捕获

如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的

void biggies(vector<string> &words,
             vector<string>::size_type sz,
             ostream &os=cout,char c=' ')
{
    //与之前例子一样重拍words
    //打印count的语句改为打印到os
    for_each(words.begin(),words.end(),
             [&os,c](const string &s){os<<s<<c;});
}

我们也可以从一个函数返回lambda,如果函数返回一个lambda,则与函数不能返回一个局部变量的引用类似,此lambda也不能包含引用捕获

隐式捕获

我们可以在捕获列表中写一个&或者=,指示编译器推断捕获列表。&告诉编译器采用捕获引用方式,=则表示采用值捕获方式

//sz为隐式捕获,值捕获方式    
auto wc=find_if(words.begin(),words_end(),
                    [sz](const string &s)
                         {return s.size()>=sz;});

如果我们希望对一部分变量采用值捕获,对其他变量采用引用捕获,可以混合使用隐式捕获和显式捕获。

当混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个&或=。

当混合使用隐式捕获和显式捕获时,显式捕获的变量必须使用与隐式捕获不同的方式。

void biggies(vector<string> &words,
             vector<string>::size_type sz,
             ostream &os=cout,char c=' ')
{
    //与之前例子一样重拍words
    //打印count的语句改为打印到os
    //os隐式捕获,引用捕获方式,c显式捕获,值捕获方式
    for_each(words.begin(),words.end(),
             [&,c](const string &s){os<<s<<c;});
    for_each(words.begin(),words.end(),
             [=,&os](const string &s){os<<s<<c;});
}

可变lambda

默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。如果我们希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字mutable。

void fcn3()
{
    size_t v1=42;
    //f可以改变它所捕获的变量的值
    auto f=[v1] () mutable{return ++v1;};
    v1=0;
    auto j=f();  //j为43
}

指定lambda返回类型

默认情况下,如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void。与其他返回void的函数类似,被推断返回void的lambda不能返回值

//错误:不能推断lambda的返回类型
transform(vi.begin(),vi.end(),vi.begin(),
           [](int i) {if(i<0) return -i; else return i;});

当需要为一个lambda定义返回类型时,必须使用尾置返回类型:

transform(vi.begin(),vi.end(),vi.begin(),
           [](int i)->int
           {if(i<0) return -i; else return i;});

4、参数绑定

如果我们要在很多地方使用相同的操作,编写相同的lambda表达式会过于麻烦,此时应该定义一个函数。

如果lambda的捕获列表为空,通常可以用函数来代替它。但是,对于捕获局部变量的lambda,用函数来代替它就不是那么容易的了

标准库bind函数

bind定义在头文件functional中。它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表

bind的一般形式:

auto newCallable=bind (callable, arg_list);

绑定check_size的sz参数

bool check_size(const string &s,string::size_type sz)
{
    return s.size()>=sz;
}

//check6是一个可调用对象,接受一个string类型的参数
//并用此string和值6来调用check_size
auto check6=bind(check_size,_1,6);

此bind只有一个占位符_1,表示check6只接受单一参数const string&。

find_if的check_size版本:

auto wc=find_if(words_begin(),words.end(),

                        bind(check_size,_1,sz));

使用placeholders名字

名字_n都定义在一个名为placeholders的命名空间中,而这个命名空间本身定义在std命名空间中,为了使用这些名字,两个命名空间都要写上:

using namespace std::placeholders;

bind的参数

可以用bind绑定给定课调用对象中的参数或重新安排其顺序:

auto g=bind(f,a,b,_2,c,_1);

实际上,调用g(X,Y)会调用f(a,b,Y,c,X)

用bind重排参数顺序

//按单词长度由短至长排序
sort(words.begin(),words.end(),isShorter);
//按单词长度由长至短排序
sort(words.begin(),words.end(),bind(isShorter,_2,_1));

绑定引用参数

默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。

因为bind拷贝其参数,而我们不能拷贝一个ostream。如果我们希望传递给bind一个对象而又不拷贝它,就必须使用标准库ref函数:

ostream &print(ostream &os,const string &s,char c)
{
    return os<<s<<c;
}

for_each(words.begin(),words.end(),
         bind(print,ref(os),_1,' '));

四、再探迭代器

在iterator头文件中还定义了几种迭代器:

插入迭代器

流迭代器

反向迭代器(forward_list除外)

移动迭代器

1、插入迭代器

插入迭代器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。

插入迭代器有三种类型:

back_inserter

front_inserter

inserter

只有在容器支持push_front的情况下,我们才可以使用front_inserter。类似的,只有在容器支持push_back的情况下,我们才能使用back_inserter.

当调用insert(c,iter)时,我们得到一个迭代器,使用它时,会将元素插入到iter原来指向的元素之前的位置(iter一直指向固定的一个元素)

list<int> lst={1,2,3,4};
list<int> lst2,lst3; //空list
//拷贝完成后,lst2包含4,3,2,1
copy(lst.begin(),lst.end(),front_inserter(lst2));
//拷贝完成后,lst3包含1,2,3,4
copy(lst.begin(),lst.end(),inserter(lst3,lst3.begin()));

2、iostream迭代器

istream_iterator读取输入流,ostream_iterator向一个输出流写数据

istream_iterator操作
istream_iterator<T> in (is); in从输入流is读取类型为T的值
istream_iterator<T> end; 默认初始化迭代器,相当于创建了一个尾后迭代器
in1=in2 如果两者都是尾后迭代器,或者绑定到相同的输入,则两者相等
in1!=in2
*in 返回从流中读取的值
in->mem 与(*in).mem的含义相同
++in,in++ 使用元素类型所定义的>>运算符从输入流中读取下一个值

用一个istream_iterator从标准输入读取数据,存入一个vector的例子:

istream_iterator<int> in_iter(cin);   //从cin读取int
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绑定到一个流时,标准库并不保证迭代器立即从流读取数据,可以直到我们使用迭代器时才真正读取。

ostream_iterator操作

ostream_iterator操作
ostream_iterator<T> out(os); out将类型为T的值写到输出流OS中
ostream_iterator<T> out(os,d); out将类型为T的值写到输出流os中,每个值后面都输出一个d。d必须是一个C风格字符串(即,一个字符串字面常量或者一个指向以空字符结尾的字符数组的指针)
out=val 用<<运算符将val写入到out所绑定的ostream中。
*out,++out,out++ 每个运算符都返回out

必须将ostream_iterator绑定到一个指定的流,不允许空的或表示尾后位置的ostream_iterator

使用ostream_iterator来输出值的序列:

//一下三种形式等价
ostream_iterator<int> out_iter(cout," ");
for(auto e:vec)
    *out_iter++=e;
cout<<endl;

ostream_iterator<int> out_iter(cout," ");
for(auto e:vec)
    out_iter=e;
cout<<endl;

copy(vec.begin(),vec.end(),out_iter);
cout<<endl;

运算符*和++实际上对ostream_iterator对象不做任何事情,因此忽略它们对我们的程序没有任何影响

3、反向迭代器

除了forward_list之外,其他容器都支持反向迭代器

rbegin、crbegin返回指向容器尾元素的迭代器

rend、crend返回指向首元素之前一个位置的迭代器。

sort(vec.begin(),vec.end());//按“正常序”排序vec
sort(vec.rbegin(),vec.rend()); //按逆序排序:将最小元素放在vec的末尾

反向迭代器和其他迭代器之间的关系

我们可以通过调用reverse_iterator的base成员函数可以将其转换成普通迭代器

//在一个逗号分隔的列表中查找第一个元素
auto comma=find(line.cbegin(),line.cend(),',');
cout<<string(line.cbegin(),comma)<<endl;

//反向迭代器版本
auto rcomma=find(line.crbegin(),line.crend(),',');
cout<<string(rcomma.base(),line.cend())<<endl;
//错误:将逆序输出单词的字符
cout<<string(line.crbegin(),romma)<<endl;

关键点在于[line.crbegin(),rcomma)和[rcomma.base(),line.cend())指向line中相同的元素范围。为了实现这一点,rcomma和rcomma.base()必须生成相邻位置而不是相同位置,crbegin()和cend()也是如此。

五、泛型算法结构

1、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);

接受单个目标迭代器的算法

向输出迭代器写入数据的算法都假定目标空间足够容纳写入的数据

接受第二个输入序列的算法

接受单独beg2的算法假定从beg2开始的序列与beg与end所表示的范围至少一样大

3、算法命名规范

一些算法使用重载形式传递一个谓词

unique(beg,end);       //使用==运算符比较元素
unique(beg,end,comp);  //使用comp比较元素

_if版本的算法

find(beg,end,val);
find_if(beg,end,pred);

区分拷贝元素的版本和不拷贝的版本

reverse(beg,end);    //反转
reverse_copy(beg,end,dest);  //将元素按逆序拷贝到dest

remove_if(v1.begin(),v1.end(),
          [](int i) {return i%2;});   //从v1中删除奇数元素
remove_copy_if(v1.begin(),v1.end(),back_inserter(v2),
          [](int i) {return i%2;});   //将偶数元素从v1拷贝到v2,v1不变

六、特定容器算法

list和forward_list成员函数版本的算法
这些操作都返回void
lst.merge(lst2)  
lst.merge(lst2,comp)  
lst.remove(val)  
lst.remove_if(pred)  
lst.reverse()  
lst.sort()  
lst.sort(comp)  
lst.unique()  
lst.unique(pred)  

猜你喜欢

转载自blog.csdn.net/oil_you/article/details/82887094