C++primer十万字笔记 第十章 泛型算法

泛型算法

 大多数算法都定义在algorithm中 ,标准库中还在 numeric中定义了一组数值泛型算法。一般情况下算法不直接操作容器而是由两个迭代器指定的范围来进行骚操作。
find(c.begin(),c.end(),val);//找到在两个之间的值

初识泛型算法

 标准库提供100多个算法。幸运的是这些算法有一致的结构。除了少数的情况下,标准库算法都对一个范围内的元素加进行操作。我们将此元素范围称之为输入范围。接收输入范围的算法总是使用前后两个参数来表示此范围——尾后指针和首元素指针。

只读算法

 顾名思义,不会修改容器中的元素。一个例子是accumulate函数。

string sum = accumulate(v.begin(),v.end(),string(""));//第三个参数是初始值,因为string定义了+操作。所以可以进行求和
equal(roster1.cbegin(),roster1.cend(),roster2.cbegin());//equal是一个只读算法,这个算法有一个假设就是第二个至少和第一个一样长
写容器元素的算法

 使用这类算法要确保序列原大小至少不小于我们需要写入的元素数目
fill(vec.begin().vec.end(),0);//填充

vector<int> vec;					//定义了一个空vector
fill_n(vec.begin(),vec.size(),0);	//全部填充0
fill_n(vec.begin(),10,0);			//错误,这时候vector没有元素,会有未定义的错误
back_inserter

 有时候会出现容器中的空间不够写的,就可以使用插入迭代器。插入迭代器是一种向容器中添加元素的迭代器。当我们通过一个迭代器进行赋值的时候,值被赋予迭代器指向的元素,当我们通过一个插入迭代器赋值时,一个与赋值号右侧相等的元素被添加到容器中。为了展示如何用算法向容器写入数据,使用back_iterator。定义在头文件iterator的函数

vector<int> vec;//空向量
auto it = back_inserter(vec);//通过它赋值会将元素添加到vec中
*it = 42;		//vec中现在有一个元素值为42	
// 可以使用back_iterator来创建迭代器作为算法的目的位置来使用,例如
fill_n(back_iterator(vec),10,0);	//很巧妙的添加了元素进去vec
拷贝算法

 拷贝算法(copy)是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法。此算法接受三个迭代器前两个表示输入范围第三个是目的序列的起始位置。

//使用copy来实现数组的内容拷贝
int a1[]={0,1,2,3,4,5};
int a2[sizeof(a1)/sizeof(*a1)];
auto ret = copy(begin(a1),end(a1),a2);	//把a1的元素都拷贝给a2  返回值是被写的元素的尾后指针。

 replace算法,读取一序列然后将其中所有等于 给定值的元素都改成另外的值。算法接受四个参数前两个使迭代器,第三个是要搜索的值最后一个是新值。
replace(c.begin(),c.end(),0,99);//把所有的0换成99
`replace_copy(c.cbegin(),c.cend(),back_inserter(ivec),0,99);//这个不改变原来的而是把原来的赋值到新的容器中然后。

void elimDups(vector<sting> &words){
    sort(words.begin(),word.end());
    //unique重新排序会把重复的挪到后面去
    auto end_unique = unique(words.begin(),words.end());
    //使用向量操作erase删除重复单词
    words.erase(end_unique,words.end());	//为了真正的能够删除元素,必须容器操作显式的删除其
}
定制操作

 很多算法都会比较序列中的元素。默认情况下这类算法使用元素类型的<或者==运算符来完成。标准库允许我们自己定义的操作来代替默认运算符。例如sort算法中默认使用元素类型的<运算符中的比较方式不能满足我们的需要,或者元素就没有定义<运算符。这时候就需要重载sort的默认行为。

向算法传递函数

 谓词:谓词是一个可调用的表达式。其结果是一个能用做条件的值。标准库算法使用的谓词分为两类:一元谓词和二元谓词。接受谓词参数的算法使用这个谓词来代替<来比较元素。

bool isshorter(const string &s1,cosnt string &s2){
    return s1.size()<s2.size();
}
sort(words.begin(),words.end(),isShorter);//可以看到这里sort通过isShorter来进行排序判断  如果用stable_sort可以维护相等元素的原有顺序
lambda表达式

 根据算法接受一元还是二元谓词我们传递给算法 的谓词一定要严格接受一个或者两个参数。我们可以向一个算法传递任何类别的可调用对象。对于一个对象或者一个如果可以对其使用调用运算符就称之为可调用的。到目前为止只遇到两种可调用的分别为函数和函数指针下面介绍lambda。
 lambda表达式表示一个可调用的代码单元。可以将其理解为一个未命名的内联函数和任何函数类似其有一个返回类型一个参数列表和一个函数体。但与函数不同lambda可能定义在函数内部。一个lambda表达式如下形式:
[capture list](parameter list)->return type{function body}其中capture_list是一个lambda所在函数中定义的局部变量的列表(通常为空);return type,parameter和function body和一般的函数没什么不同只是使用了尾置返回而已。
 我们可以忽略参数列表和返回类型auto f=[] {return 42;}//课忽略参数列表和返回类型但是一定要包括捕获列表和函数体这个函数的完整版为auto f = []()->int{return 42;}

 lambda不能用默认参数。我们可以写一个另外版本的isShorter:[](const &a,const &b){return a.size()>b.size();}

 定义一个lambda时,编译器会生成一个与lambda对应的类类型。默认情况下labmda生成的类都包括捕获列表的数据成员,类似于任何普通类的数据成员,lambda数据成员在lambda对象创建时被初始化。
 变量捕获的方式是值捕获和引用捕获。和函数一样lambda也不能返回局部变量的引用。

void fcn1(){
    size_t v1=42;
    auto f = [v1]{return v1};
    auto f1= [&v1]{return v1;}
    v1 = 0;
    auto j= f();	//j为42,f创建的时候只是保存了v1的拷贝
    auto j1=f();	//j1为0,f1创建使用了v1的引用
}

 lambda可以使用自动捕获和混合捕获方式,对于自动捕获如果想捕获值使用[=],使用引用使用[&],也可在中括号内加一起其他的从而进行混合引用。

void biggers(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;});	//c为显式捕获,值捕获形式。 os为隐式捕获,引用捕获形式
    for_rach(words.begin(),word.end(),[=,&os](const string &s){os<<s<<c;}); //os为显式捕获,引用捕获形式。c为隐式捕获,为值捕获
}

 默认情况下对一个值拷贝的变量lambda不会改变其值,如果希望改变一个捕获变量的值,必须在参数列表首家伙是哪个关键字mutable。因此可变lambda能省略参数的值。

void fcn3(){
    size_t v1 =42;
    //f可以改变它捕获的变量的值
    auto f = [v1]()mutable{return ++v1;};		//multable意味着可以改变捕获变量的值了。
    v1 = 0;
    auto j =f();
}

 默认情况下如果一个lambda包含了除return之外的任何语句就会被推断为返回类型为void。因此有时候需要用尾置的方式说明返回值类型。

//transform接受三个迭代器和一个调用对象,前两个表示输入序列第三个表示目的位置。算法对输入序列中调用元素然后结果写入目的位置。
transform(vi.begin(),vi.end(),vi.begin(),[](int i){reutnr <0?-i:i;});	//这句话能推断出正确的return因为只有return语句
transform(vi.begin(),vi.end(),vi.begin(),[](int i){if(i<0) reutnr -i; else return i;});//就会推断为vodi型,因为有其他语句。 错误!
transform(vi.begin(),vi.end(),vi.begin(),[](int i)->int{if(i<0) reutnr -i; else return i;});//正确,修正了上面的 的错误。制定了返回值类型
参数绑定

 对于在一两个地方使用的简单操作,lambda是最有用的,如果需要在很多地方使用相同的操作应该使用函数。如果lambda的捕获列表为空通常可以使用函数代替之。但是对于捕获局部变量的lambda就有时候不是那么好代替了。例如在find_if中 调用lambda比较string和一个给定的大小。我们可以很容易编写一个完成相同功能的函数:

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

但是我们不能使用这个函数做为find_if的参数,因为find_if接受一个一元谓词。

bind函数

要解决这个问题应该使用标准库中的band函数,band函数是一种通用的函数适配器。它接受一个可调用对象生成一个可调用对象来适应原对象的参数列表。调用band的一般形式:

auto newCallable = bind(callable,arg_list);
//

其中newCallable本身是一个可调用对象,arg_list是一个逗号分割的参数列表,对应给定的callable参数,当我们调用newCallable时newCallable会调用callable并传给它arg_list中的参数。arg_list中的参数可能包含形如_n的名字其中n是一个整数,这些参数是占位符,表示newCallable的参数。它们占据了传递给newCallable的参数的位置。:_1为newCallable的第一个参数,_2为第二个参数,其他就是填充进去的参数。

auto check6 = bind(check_size,_1,6);//_1是一个占位符,表示传入的第一个参数是送入check_size函数的第一个参数。
string s = "hello";
bool b1 = check6(s);	//相当于调用check_size(s,6);

//我们也可以将下面的一个find_if语句替换
auto wc = find_if(words.begin(),words.end(),[sz](const string &a){});
auto wc = find_if(words.begin(),words.end(),bing(check_size,_1,sz);
使用bing重新排序参数

sort(word.begin(),word.end(),bind(isShorter(,_2,_1)));//这里bind返回的函数的变量位置就会被调换

绑定引用参数
for_each(words.begin(),words.end(),bind(print,os,_1,' ')); //因为os不能当做形参拷贝,而band只支持值传入
//可以用ref函数 
for_each(words.begin(),words.end(),bind(print,ref(os),_1,' ')); //ref返回一个可拷贝的对象 cref用来返回一个const &引用
for_each算法

 for_each听名字肯定是一个用来遍历的工具。
for_each(c.begin(),c.end(),[](const string &s){cout<<s<<"";}); cout<<endl;

再探迭代器

 一些额外的迭代器主要包括如下几种:

  • 插入迭代器:这些迭代器被绑定在一个容器上,可用来向容器插入元素
  • 流迭代器:用来绑定到输入输出流上用来遍历所由关联的IO
  • 反向迭代器:向后而不是向前移动除了forward_list都有反向迭代器
  • 移动迭代器:用来移动这些元素
插入迭代器

 迭代适配器接受一个容器生成一个迭代器。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KiC4PHEB-1641812144094)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210831155052352.png)]

*it = val;
//等价于如下
it = c.insert(it,val);	//这会让元素指向前一个位置
++it;					//再回到原来的位置

 front_inserter生成的迭代器总是插入到第一个元素之前。back_inserter总是把元素插入到最后面。

iostream迭代器

 虽然iostream不是容器,但是标准库定义了可以用于IO类型对象的迭代器。istream_iterator读取输入流,ostream_iterator读取向一个输出流写数据。通过使用流迭代器我们可以用泛型算法从流对象中读取数据以及向其中写入数据。

istream_iterator 操作

 当创建一个流迭代器中必须指定迭代器将要读写的对象类型。一个istream_iterator使用>>来读取流。因此要读取的类型必须定义了输入运算符。当创建一个istreawm_iterator时我们可以将它绑定到一个流。当然我们可以默认初始化迭代器。这样就创建了一个可以当做尾后值使用的迭代器。

istream_iterator<int> int_it(cin);	//从cin中读取int
istream_iterator<int> int_eof;		//尾后迭代器
ifstreawm in("file.txt");
istream_iterator<string> str_it(in);	//从file.txt中读取字符串。

istream_iterator<int> in_iter(cin);		//对一个流迭代器,一旦关联的流遇到文件尾或者IO错误,迭代器的值就和尾后迭代器相等。
istream_iterator<int> eof;
while (in_iter!=eof)	//比方说我输入了一个英文字符,就无法识别
    vec.push_back(*in_iter++);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F1ifXv3z-1641812144095)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210831171047260.png)]

 因为算法使用迭代器来处理数据,而流迭代器又至少支持某些迭代器操作。因此我们至少可以用某些算法来操作流迭代器。

istream_iterator<int> in(cin),eof;
cout<<acumulate(in,eof,0)<<endl;//此调用会计算出从标准输入读取的值的和。

 istream_iterator允许使用懒惰求值,当我们将一个istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流中读取数据。具体实现可以推迟从流中读取数据知道我们使用迭代器时才真正读取。标准库的实现保证在第一次解引用迭代器之前,从流中读取数据的操作已经完成了。

ostream_iterator操作

 我们可以给任何具有<<运算符的类型定义ostream_iterator。创建一个ostream_iterator时,我们可以提供(可选的)第二个参数,它是一个字符串在输出每个元素后打印此字符串。此字符串必须是一个c风格的字符串(即一个字符串常量或者是一个指向以空字符结尾的字符数组的指针)。必须将ostream_iterator绑定到一个指定的流,不允许空的或者表示尾后位置的ostream_iterator。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ouYveYU-1641812144095)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210831212653682.png)]

//我们使用ostream_iterator来输出值的序列如下:
ostream_iterator<int> out_iter(cout," ");
for(auto e:vec)
    *out_iter++=e;	//当然++是无效的因此可以out_iter = e;
cout<<endl;
//如果调用了copy更加简单
copy(vec.begin(),vec.end(),out_iter);
cout<<endl;
使用流迭代器类型处理类类型

 我们可以为任何定义了输入运算符>>的类型创造istream_iterator。类似的只要类型有输出运算符(<<)我们就可以为其定义ostream_iterator。

//由于Sales_date既有输出运算符也有输入运算符。因此你可以使用IO迭代器重写书店程序如下:
istream_iterator<Sales_data> item_iter(cin),eof;
ostream_iterator<Sales_data> out_iter(cout,"\n");
//将一笔交易记录存在sum中并读取下一条记录
Sales_item sum = *item_iter++;
while(iter_iter!=eof){
    //如果当前交易记录(存在iter_iter中)有相同的ISBN号
    if(item_iter->isbn()==sum.isbn())
        sum += *item_iter++;	//将其加到sum并读取下一条记录
    else{
        out_iter = sum;			//输出sum当前值
        sum = *item_iter++;		//读取下一条记录
    }
}
out_iter = sum;					//记得打印最后一组记录的和
反向迭代器

 反向迭代器就是在容器从尾元素到首元素反向移动的迭代器。对于反向迭代器,递增 和递减的含义会倒过来。递增一个反向迭代器(++it)会移动到前一个元素。
 除了forward_list外其他的容器都支持反向迭代器。可以通过调用rbegin,rend,crbegin和crend成员函数来获得反向迭代器。这些成员函数返回指向容器尾元素之前一个位置的迭代器。与普通迭代器一样,反向迭代器也有const和非cosnt版本。

//下面的程序打印一个以‘,’为分隔符的单词里列表打印第一个单词。
auto comma = find(line.cebegin(),line.cend(),',');
cout<<string(line.cbegin(),comma)<<endl;
//如果需要打印最后一个单词就可以使用反向迭代器
auto rcomma = find(line.crbegin()(),line.crend());
cout<<string(line,line.crbegin(),rcomma<<endl);	//错误,因为rcomma是一个反向的迭代器,会把单词反过来打印
cout<<string(line,rcomma.base(),line.crend());	//正确,通过使用反向迭代器的base成员函数会把它变成正向迭代器

泛型算法结构

 任何算法最基本的就是要求迭代器提供哪些操作。某些算法如find只需要迭代器访问元素、递增迭代器以及比较两个迭代器相等的能力。其他一些算法如sort还需要读写和随机访问元素的能力。算法所要求的迭代器可以分为下面5个类别。每个算法都会对它的每个迭代器参数指明需要提供哪类迭代器:

迭代器种类解释
输入迭代器只读,不写,单遍扫描,只能递增
输出迭代器只写,不读,单遍扫描,只能递增
前向迭代器可读写,多遍扫描,只能递增
双向迭代器可读写,多遍扫描,可递增递减
随机访问迭代器可读写,多变扫描,支持全部迭代器运算

 迭代器分类形成了一种层次。除了输出迭代器之外,一个高层类别的迭代器支持底层迭代器的所有操作。

5类迭代器

 迭代器也定义了一组公共操作。一些操作所有迭代器都支持,另外一些只有特定类别的才支持。例如ostream_iterator只支持递增、解引用和赋值。vector、string、deque的迭代器除了这些外还支持递减、关系和算术运算。
 cpp明确指明了泛型和数值算法对每个迭代器参数的最小类别。例如find在一个序列上进行扫描至少需要输入迭代器。replace需要一对迭代器,至少是前向迭代器。replace_copy前两个迭代器参数至少是前向迭代器,第三个表示目的位置,至少是输出迭代器。

迭代器类别

输入迭代器可以读取序列中的元素。一个输入迭代器必须支持:

  • 用于比较两个迭代器的相等和不相等运算符(==、!=)
  • 用于推进迭代器的前置和后置递增运算(++)
  • 用于读取元素的解引用运算符(*);解引用只会出现在赋值运算符左侧
  • 箭头运算符(->),等价于(*it).member,即解引用迭代器并提取对象的成员
输出迭代器

 写而不读元素。其必须支持

  • 用于推进迭代器的前置和后置递增运算(++)
  • 解引用运算符(*),只出现在赋值运算符的左侧(向一个已经解引用的输出迭代器赋值,就是将值写入它所指的元素)

 我们只能向一个输出迭代器复制一次。类似于输入迭代器,输出迭代器只能单遍扫描,用做目的为指导迭代器通常都是输出迭代器如copy函数的第三个输出迭代器和ostream_iteratro类型也是输出迭代器

前向迭代器

 可以读写元素。这类迭代器只能在序列中沿一个方向移动。前向迭代器支持所有输入和输出移的操作,而且可以多次读写同一个元素 。因此从可以保存前向迭代器的状态,使用前向迭代器的算法可以对序列进行多遍扫描,replace要求前向迭代器,forward_list上的迭代器是前向迭代器。

双向迭代器

 可以正向、反向读写序列中的元素。除了支持所欲前向迭代器的操作外,双向迭代器还支持前置和后置递减运算符(–)。算法reverse要求双向迭代器,除了forward_list外其他标准库中的容器都提供符合双向迭代器要求的迭代器。

随机访问迭代器

 提供在常量时间范围内访问序列中任何元素的能力。其支持双向迭代器所有的功能。此外还可以

  • 用于比较两个迭代器对应位置的关系运算符(<,>和<=,>=)
  • 迭代器和一个整数值的加减运算(+,+=,-,-=),计算结果是迭代器在序列前进(或后退)给定整数个元素后的位置
  • 用于两个迭代器的减法运算符(-),用来得到两个迭代器的距离。
  • 下标运算符(iter[n]),与*(iter[n])等价
算法形参形式

 在任何其他算法的分类上还有一组参数规范。理解这些参数规范对学习新算法很有帮助。大多数算符有如下的四种形式之一

  • alg(beg,end,other args)
  • alg(beg,end,dest,other args)
  • alg(beg,end,beg,other args)
  • alg(beg,end,beg2,end2,other args)

其中alg是算法的名字,beg和end表示算法所操作的输入范围。几乎所有的算法都接受一个输入范围,是否有其他参数依赖于要执行的操作。这里列出了常见的一种:dest,beg2和end2都是迭代器参数,它们分别承担指定目的位置和第二个范围的角色。除了这些迭代器参数,一些算法还接受一下额外的非迭代器的参数。

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

 dest参数表示算法可以写入的目的位置的迭代器。算法假定:按其需要写入数据不管写入多少个元素都是安全的。

 如果dest是一个直接指向容器的迭代器,那么算法将输出数据写到容器中已经存在的元素内。更常见的就是dest被绑定在一个插入迭代器或者一个ostream_iterator中。

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

 接收单独的beg和接受beg2和end2的算符用这些迭代器表示第二个输入范围,这些算法通常使用第二个范围内的元素与第一个输入范围结合来进行一些运算。
 只接受一个begin的表示将begin在的位置作为输出的首元素然后依次递增(当然只是运算符的递增,如果是front_inserter迭代器实际上还是反向输入的),如果还有end就是限制了第二个的范围。

算法命名规范

 除了参数外,算法还遵循一套命名和重载规范。这些规范处理诸如:如何提供一个操作代替默认的<或者==运算符以及是将输出数据写入输入序列还是一个分离的目的位置等问题。

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

 接受谓词单数来代替<或者<=运算符的算法,以及那些不接受额外参数的算法,通常都是重载的函数。函数的一个版本用元素类型的运算符来比较元素,另外一个版本接受一个额外的谓词参数来带起<或者==:

unique(beg,end);			//使用==运算符比较元素 
unique(beg,end,comp);		//使用comp比较元素		
if版本的算法

 接受一个元素值的算法通常有另一个不同名的版本,该版本接受一个谓词代替元素值。接受谓词参数的算法都有附加的_if前缀:

find(beg,end,val);				//find接受一个元素值来比较大小
find_if(beg,end,pred);			//pred也应该接受一个元素值返回一个bool
区分拷贝元素的版本和不拷贝的版本

 比如重排元素的算法的默认版本将重排元素的写会给定的输入序列中。这些算法还提供另外一个版本,将输出写到一个指定的位置中。如下例:

reverse(beg,end);
reverse_copy(beg,end,dest);

 一些算法同时提供_copy和_if版本,这些版本接受一个目的位置迭代器和一个谓词:

remove_if(v1.begin(),v1.end(),[](int i){return i%2;});
remove_copy_if(v1.begin(),v1.end(),back_inserter(v2),[](int i){reuturn i%2});
特定容器算法

 和其他容器不同,链表类型的list和forward_list定义了几个成员函数的形式的算法。它们拥有独立的sort,merge,remove ,reverse和unique算法。因为通用的sort版本需要提供随机访问迭代器,但是list提供双向迭代器和前向迭代器不能满足要求。链表拥有的成员函数形式的算法如下表所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J8wqQMRa-1641812144096)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210901105504348.png)]

 链表还提供了splice算法,其表述如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jbcqmjpe-1641812144096)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210901110010677.png)]

总结

 迭代器分为5类,输入、输出、前向、双向、和随机。算法不会直接改变操作序列的大小,只会把元素从一个位置拷贝到另外一个位置。但是算法可以接受一个插入迭代器。当我们将一个容器元素类型的值赋予一个插入迭代器时,迭代器就会将该值加入这个容器中。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值