【10】C++之泛型算法


对于顺序容器的其他操作:查找元素、替换或者删除一个特定值、重排元素顺序等。标准库并未给每个容器都定义成员函数来实现这些操作,而是定义了一组泛型算法(generic algorithm):称它们为“算法”,是因为它们实现了一些经典算法的公共接口。如排序和搜索;称它们是“泛型的”,是因为它们可以用不同类型的元素和多种容器类型(不仅包括标准库类型,如vector或list,还包括内置的数组类型)。

标准库提供了超过100个算法。此篇只是一个入门,更多的STL操作参考《STL标准库》。


参考文章:

[1] 《C++ Primer》读书笔记—第十章 泛型算法

[2] 《C++泛型算法》

[3] 《c++中常用的泛型算法》


1、初识泛型算法

1.1 只读算法

一些算法只会读取其输入范围内的元素,而不改变元素。

① find

头文件:

#include <algorithm>

find接收三个参数,前两个是表示元素范围的迭代器,第三个参数是一个值。find将范围中每一个元素与给定值进行比较。它返回指向第一个等与给定值的元素的迭代器。如果范围中无匹配元素,则find返回第二个参数来表示搜索失败。因此,可以通过比较返回值和第二个参数来判断搜索是否成功。

对于string来说,由于其没有迭代器,所以find的用法有所不同:

语法:

size_type find( const basic_string &str, size_type index );
size_type find( const char *str, size_type index );
size_type find( const char *str, size_type index, size_type length );
size_type find( char ch, size_type index );

find()函数:

  • 返回str在字符串中第一次出现的位置(从index开始查找)。如果没找到则返回string::npos,
  • 返回str在字符串中第一次出现的位置(从index开始查找,长度为length)。如果没找到就返回string::npos,
  • 返回字符ch在字符串中第一次出现的位置(从index开始查找)。如果没找到就返回string::npos

例如:

string str = "hello are you ok";
string s_str = "hello";
if(str.find(s_str,0) != string::npos)
    cout << "find ok" << endl;

输出:

find ok

find_if:

find_if() find() 一样,为在输入迭代器所定义的范围内查找单个对象的算法,它可以在前两个参数指定的范围内查找可以使第三个参数指定的谓词(见本博客下面章节)返回 true 的第一个对象。谓词不能修改传给它的对象。
find_if() 返回一个指向被找到对象的迭代器,如果没有找到对象,会返回这个 序列的结束迭代器(也就是第二个参数)。

vector<int> num = {1,2,3,4,5,6,7,8,9};
int c = 10;
if(find_if(num.begin(),num.end(),[c](int n)->int{return c < n;}) != num.end())
    cout << "find it!" << endl;

输出:

find it!

② count

  • count(first,last,value):first是容器的首迭代器,last是容器的末迭代器,value是询问的元素,整个函数返回int型。count函数的功能是:统计容器中等于value元素的个数。
  • count_if(first,last,comp): (在comp为true的情况下计数)或者count_if(first,last,value,comp) (这个是在comp为true的情况下统计容器中等于value的元素):first为首迭代器,last为末迭代器,value为要查询的元素,comp为比较bool函数,为true则计数,函数返回型是int。

注:此两个函数复杂度是线性的,适用于小规模运算。

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main()
{
    vector<int > num{1,2,3,1,2,3,4,5,6,7,8,1};
    int numcount = count(num.cbegin(),num.cend(),1);
    cout << "1s = " << numcount << endl;
    return 0;
}

输出:

③ accumulate

头文件:

#include <numeric>

accumulate接收三个参数,前两个指出了需要求和的元素范围第三个参数是和的初值

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

#include <iostream>
#include <numeric>
#include <vector>

using namespace std;

int main()
{
    vector<int > num{1,2,3,4,5,6,7,8,9};
    vector<string> str{"123","456","789"};

    int num_sum = accumulate(num.cbegin(),num.cend(),0);
    string str_sum = accumulate(str.cbegin(),str.cend(),string(""));

    cout << "num_sum = " << num_sum << endl;
    cout << "str_sum = " << str_sum << endl;
    return 0;
}

Q:假设v是vector<double >,那么调用accumulate(v.cbegin(),v.cend(),0)是否正确?

错误

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main()
{
    vector<double > num{1.2,2.8,3.1};
    //使用(double)0将accumulate指明每个元素都是double类型的
    //否则就是1+2+3 = 6
    auto sum = accumulate(num.cbegin(),num.cend(),(double)0);
    cout << "sum = " << sum << endl;
    return 0;
}

④ 额外的操作

参考:C++(STL) all_of、any_of及none_of算法详解

1.2 写容器元素的算法、

一些算法将新值赋予序列中的元素。当我们使用这类算法时,必须注意确保序列原大小至少不小于我们要求写入的元素数目(原.size() >= 写入元素数目

① fill:将一个值赋给一个范围内的元素

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    vector<int > num{1,1,2,2,3,3};
    fill(num.begin(),prev(num.end()),0);
    for(auto i : num)
        cout << i << endl;
    return 0;
}

注意:此处由于是对容器执行写操作,故不能使用num.cbegin()

输出:

② fill_n:接受一个单迭代器、一个计数值和一个值。它将给定的值赋予迭代器指向的元素开始的指定个元素。

vector<int > num = {1,1,2,2,3,3};
fill_n(num.begin()+1,1,0);
for(auto i : num)
    cout << i << endl;

输出:

向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素。不能向空容器进行写入操作。

③ back_insert

头文件:

#include <iterator>

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

vector<int > num = {1};
fill_n(back_inserter(num),2,2);
for(auto i : num)
    cout << i << endl;

输出:

④ copy

拷贝算法是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法。此算法接受三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置此算法将输入范围中的元素拷贝到目的序列中。传递给copy的目的序列至少要包含于输入序列一样多的元素。(目的序列.size() >= 输入序列元素个数

copy返回的是其目标位置迭代器递增以后的值。如下列中,ret指向的是numToCopy尾后迭代器,所以*prev(ret) = 3

vector<int> num = {1,1,2,2,3,3};
vector<int> numToCopy{0,0,0,0,0,0};
copy(num.begin(),num.end(),numToCopy.begin());
for(auto i : numToCopy)
    cout << i << endl;

1.3 重排容器元素的算法

① sort(按照ASCII升序进行排列)

sort接受两个迭代器,表示要排序的元素范围。

更多sort的操作参见之前的博客:

按照ASCII降序进行排列:

sort(num.begin(),num.end(),greater<int>());
vector<int > num = {1,3,3,5,2,4,6,6};
sort(num.begin(),num.end(),greater<int>());
for(auto i : num)
    cout << i << endl;

② unique

unique接受两个迭代器,表示要排序的元素范围。

unique算法重排输入序列,将相邻的重复元素消除掉,并返回一个指向不重复值范围末尾的迭代器。

vector<int > num = {1,3,3,5,2,4,6,6};
sort(num.begin(),num.end(),greater<int>());
auto ret = unique(num.begin(),num.end());
for(auto i : num)
    cout << i << endl;

cout << "*prev(ret) =" << *prev(ret) << endl;

可以看出:

unique并未真正改变容器大小,它只是覆盖相邻的重复元素,使得不重复地元素出现在序列开始的地方。

unique_copy:它接受第三个迭代器,表示拷贝不重复元素的目的位置

2、定制操作

2.1 向算法传递函数

谓词:

一元谓词:它只接收单一参数

二元谓词:他们有两个参数

例如:如下,使用sort版本用这个谓词来代替 < 来比较元素。

bool isShorter(const string &s1,const string &s2){
    return s1.size() < s2.size();
}

int main()
{
    vector<string> str{"are","youu","ok","hello"};
    sort(str.begin(),str.end(),isShorter);
    for(auto i : str)
        cout << i << endl;
}

stable_sort:保持等长元素间的字典序。

2.2 lambda表达式

lambda表达式:(使用尾置返回类型 -> 对于返回类型比较复杂的函数最为有效)

[capture list](parameter list) -> return type { function body};

 

capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为空);return type(返回类型)、parameter list(参数列表)和function body(函数体)与任何普通函数是一样的。

可以忽略参数列表返回类型,但必须永远包含捕获列表和函数体。如果忽略返回类型,lambda根据函数体的代码腿短出返回类型。如果函数体只是一个return语句,则返回类型从返回的表达式的类型推断而来。否则,返回类型为void。

auto f = [](int a,int b)->int{return a+b;};
    cout << "f() = " << f(1,1) << endl;

输出是:f() = 2

注意:

  1. lambda不能有默认参数,所以,lambda调用的实参数目永远与形参数目相等。
  2. lamda虽然是出现在一个函数中,但是它只能使用出现在捕获列表中的局部变量
//使用捕获列表
auto f = [c](int a,int b)->int{
    if(a+b > c)
        return a+b;
    else
        return c;
};
cout << "f() = " << f(1,1) << endl;

输出:f() = 10

for_each算法:

template<class InputIterator, class Function>
  Function for_each(InputIterator first, InputIterator last, Function fn)
{
  while (first!=last) {
    fn (*first);
    ++first;
  }
  return fn;      // or, since C++11: return move(fn);
}

例子:

vector<int> num = {1,2,3,4,5,6,7,8,9};
for_each(num.begin(),num.end(),[](int i)->void{cout << i << endl;});

2.3 lambda捕获和返回

① lambda捕获

lambda捕获列表
[]

空捕获列表。lambda不能使用所在函数中的变量。一个lambda只有捕获变量后才能使用他们

[names]names是一个逗号分割的名字列表,这些名字都是lambda所在函数的局部变量。默认情况下,捕获列表中的变量都被拷贝。名字前如果使用了&,则采用引用捕获方式
[&]隐式捕获列表。采用引用捕获方式。lambda体中所使用的来自所在函数的实体都采用引用方式使用
[=]隐式捕获列表,采用值捕获方式。lambda体将拷贝所使用的来自所在函数的实体的值
[&, identifer_list] 
[=, identifer_list] 

可变lambda:

case1:值捕获时,要想改变所捕获的值,就必须要在参数列表首加上关键字mutable。

case2:引用捕获时,变量是否可以修改依赖于次饮用指向的是一个const类型还是一个非const类型

② 指定lambda返回类型

  • 默认情况下,如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void
  • 当我们需要为一个lambda定义返回类型时,必须使用尾置返回类型
#include <vector>
#include <iostream>
#include <algorithm>

using namespace std;

int main()
{
    vector<int> num = {-1,2,-3,4,-5};
    //转化为正数
    transform(num.begin(),num.end(),num.begin(),[](int i){return i>0?i:-i;});
    //转为负数
    transform(num.begin(),num.end(),num.begin(),[](int i)->int{if(i >= 0){return -i;}else{return i;}});
    for(auto i : num)
        cout << i;

    return 0;
}

2.4 参数绑定

 

3、再探迭代器

标准库头头文件iterator中还定义了额外几种迭代器。分别是插入迭代器、流迭代器、反向迭代器、移动迭代器

插入迭代器:这些迭代器被绑定到一个容器上,可用来向容器插入元素

流迭代器:这些迭代器被绑定到输入或输出流上,可用来遍历所关联的IO流。

反向迭代器:这些迭代器向后而不是向前移动,除了forward_list之外的标准库容器都有反向迭代器

移动迭代器:这些专用的迭代器不是拷贝其中的元素,而是移动他们。

3.1 插入迭代器

插入迭代器操作
it = t在it指定的当前位置插入值t。假定c是it绑定的容器,依赖于插入迭代器的不同种类,此值分别调用c.push_back(t)、c.push_front(t)或c.insert(t,p),其中p为传递给insert的迭代器位置
*it,++it,it++这些操作虽然是存在的,但不会对it做任何事情。每个操作都返回it

插入迭代器有三种类型,差异在于元素插入的位置:

  • back_inserter创建一个使用push_back的迭代器
  • front_inserter创建一个使用push_front的迭代器
  • inserter 创建一个使用insert的迭代器。此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前。

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

vector<int> num = {1};
auto it = inserter(num,num.begin());
*it = 3;
for(auto i : num)
    cout << i;

输出:32

而对于front_inserter,元素总是插入到第一个元素之前

list<int> num = {1};
for(int i = 0;i < 5;i++)
    *(front_inserter(num)) = i;
auto it = inserter(num,num.begin());
*it = 3;
for(auto i : num)
    cout << i;

输出:432101

对于back_inserter,相当于使用了push_back

list<int> num = {1};
for(int i = 0;i < 5;i++)
    *(back_inserter(num)) = i;    //使用back_inserter
auto it = inserter(num,num.begin());
*it = 3;
for(auto i : num)
    cout << i;

输出:

3.2 iostream迭代器

 

3.3 反向迭代器

反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增(以及递减)操作的含义会颠倒过来。递增一个反向迭代器(++it)会移动到迁移费元素;递减一个迭代器(--it)会移动到下一个元素。可以通过rbegin、rend、crbegin和crend成员函数来获得反向迭代器。

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

4、泛型算法结构

4.1 类迭代器

 

4.2 算法形参模式

 

4.3算法命名规范

 

5、特定容器算法

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值