《c++ primer笔记》第十章 泛型算法

前言

再次读本章居然一点印象都没有,可以看到第一次读的时候完全没有认真。本章的内容主要介绍标准库算法,以及如何配合迭代器使用等知识点。内容较多,大部分知识写几个案例都能理解。熟练的掌握常用算法和迭代器的使用对于那些用C++刷算法的同学很有帮助。

一、概述

​ 大多数算法都在头文件algorithm中定义,一般来说这些算法并不直接操作容器,而是通过迭代器去锁定访问元素的范围以及通过迭代器的解引用去获取元素值。迭代器的使用使算法不依赖于容器类型。但是大多数算法都使用了一个元素类型上的操作。算法永远不会改变容器的大小。

二、泛型算法

2.1只读算法

  1. accumulate

    定义在头文件numeric中,接收三个参数,前两个参数表示迭代器范围,第三个参数表示和的初值。在前面章节有了解过重载运算符,accumulate的第三个参数是否含有+成员函数将会决定执行的结果。下面第一行代码显示的初始化和的初值为一个string类型的空串,sum最终结果就是容器v中所有元素的字符串拼接。而第二行代码却无法执行,因为字符串字面值的类型是const char*,而它没有+运算符。

    string sum = accumulate(v.being(),v.end(),string(""));
    string sum = accumulate(v.being(),v.end(),""); // 错误
    

    对于只读取而不改变元素的算法,最好使用cbegin()和cend()

  2. equal

    用于比较两个序列中是否保存了相同的值。接收三个迭代器:前两个为第一个序列的范围,第三个表示第二个序列的起始位置。

  3. find

    在一个容器中查找目标值。接收三个参数,前两个为迭代器,表示查找容器范围,第三个表示目标值。如果在范围内找到目标值,返回第一个位置等于目标值的迭代器,否则返回第二个参数尾后迭代器表示搜索失败。

2.2写容器元素的算法

  1. fill

    接收一对迭代器表示范围,三个参数作为填充值。

    fill(vec.begin(), vec.end(), 0); // 将每个元素重置为0
    
  2. fill_n

    接收一个单迭代器表示起点,第二个参数表示填充的次数,第三个参数表示填充值。注意看下面第二行代码,我们向一个空容器填充10个0,但是vector中并没有元素,最后的结果是未定义的。向目标容器写入数据的算法假定目的位置足够大,能容纳要写入的元素

    vector<int> vec; 
    fill_n(vec.begin(),vec.size(),0); // 将所有元素重置为0
    fill_n(vec.begin(),10,0); // 错误
    
  3. back_insert

    插入迭代器可以保证算法有足够元素空间来容纳输出数据。定义在iterator。该方法接入一个指向容器的引用,返回一个与该容器绑定的插入迭代器,当使用这个返回的迭代器进行赋值操作时,会调用push_back将一个具有给定值的元素添加到容器中。

    vector<int> vec;
    auto it = back_inserter(vec); 
    *it = 42; // 将42添加到容器vec中
    
    fill_n(back_inserter(vec),10,0); // 添加10个元素到vec
    
  4. copy

    接收三个迭代器,前两个表示目标范围,第三个表示接收拷贝内容的起始点。接收拷贝内容的容器大小必须要大于等于目标范围的大小copy返回的是接收拷贝内容的尾后元素位置,下面例子ret就指向a2最后一个元素的后面。

    int a1[] = {1,2,3,4,5,6,7,87,8,1};
    int a2[sizeof(a1) / sizeof(*a1)];
    auto ret = copy(begin(a1), end(a1), a2); // 把a1的内容拷贝给a2
    

    凡是拷贝版本的算法都是创建了一个新序列保存新的值。

2.3重排容器元素的算法

  1. sort

    调用它默认会按照从小到大的顺序对元素进行排序。

  2. unique

    对容器进行重排,将相邻的重复项进行“消除”,算法不能执行容器的操作,所以unique只是对相邻的重复元素进行覆盖,使得不重复的元素出现在序列开始部分。该算法返回的迭代器指向最后一个不重复元素之后的位置。

    //输入一个vector 包含 |the|quick|red|fox|jumps|over|the|slow|red|turtle|
    
    sort(vec.begin(),vec.end()); // 进行排序
    auto end_unique = unique(vec.begin(), vec.end()); // 消除重复项
    //此时的vec 为 |fox|jumps|over|quick|red|slow|the|turtle|???|???| 可以看到容器的大小并没有变化
    // end_unique 指向第一个???
    vec.erase(end_unique,vec,end()); // 把多余的无值空间删除,也就是两个问号
    

三、定制操作

3.1向算法传递函数

​ 前面提到,我们在使用排序算法sort时默认使用的是<运算符符,如果我们想按照自己的方式来进行排序操作,可以使用sort第二个重载版本,接收第三个参数,该参数是一个谓词。

谓词

​ 谓词就是一个表达式,返回结果是一个能用作条件的值。标准库算法使用两类谓词:一元谓词(只接收单一参数)和二元谓词(接收两个参数)。

// 自定义比较规则
bool isShorter(const string &s1, const string &s2) {
	return s1.size() < s2.size();
}
sort(vec.begin(), vec.end(), isShorter);

对于上面的代码,我们能保证一段字符串中,每个子串按照从小到大的顺序排序,但是对于等长的子串,如果我们让它们根据字典顺序排序,可以使用stable_sort

stable_sort(vec.begin(),vec.end(),isShorter);

3.2lambda表达式

​ 我们可以向一个算法传递任何类别的可调用对象,对于一个对象或者一个表达式,如果可以对其使用调用运算符,则称它为可调用的。

​ 一个lambda表达式表示一个可调用的代码单元,可以将其理解为一个未命名的内联函数lambda与函数类似,与它不同的在于lambda可以定义在函数内部。下面是lambda的格式:

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

capture list是一个lambda所在函数中定义的局部变量的列表一般为空。与普通函数不同,lambda必须使用尾置返回来指定返回类型

​ 可以忽略参数列表和返回类型,但是必须永远包含捕获列表和函数体

auto f = [] { return 42; };

向lambda中传递参数

lambda不能含有默认参数。上面我们写了一个自定义比较大小用于sort的谓词,也可以用lambda达到同样的效果。

[] (const string &a, const string &b) {
	return a.size() < b.size();
}

stable_sort(vec.begin(), vec.end(), [] (const string &a, const string &b) {
	return a.size() < b.size();
})

使用捕获列表

​ 一个lambda通过将局部变量包含在其捕获列表中来指出将会使用这些变量。考虑这样一个场景,我们需要在一堆数字中,找到第一个大于等于目标值sz的元素,通过find_if(第三个参数为判断条件,返回一个迭代器)和lambda表达式完成。捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和它所在函数之外声明的名字

auto wc = find_if(vec.begin(), vec.end(), [sz] (const string &a) { return a.size() >= sz; }); 

// wc是一个迭代器,指向第一个大于sz的元素,加入原容器按照从小到大的顺序排序,我们可以使用for_each输出所有大于sz的值

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

3.3lambda捕获和返回

​ 当定义一个lambda时,编译器生成一个与lambda对应的新的类类型。当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象。这一块书上的概念没咋看懂

值捕获

​ 采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝

void fcn1() {
	size_t v1 = 42; // 局部变量
	auto f = [v1] {return v1;};
	v1 = 0;
	auto j = f(); // j = 42,这里展示了被捕获的值在创建lambda时就进行了拷贝
}

引用捕获

​ 在使用引用捕获时一定要确保引用的对象在lambda执行时还存在。一般在不能对某些对象进行拷贝时(比如ostream)进行引用捕获。

void fcn2() {
	size_t v1 = 42; // 局部变量
	auto f = [&v1] {return v1;};
	v1 = 0;
	auto j = f(); // j = 0,
}

隐式捕获

​ 如果需要让编译器推断捕获列表,应在捕获列表中写一个&=&告诉编译器采用捕获引用方式,=表示采用值捕获方式。

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

也可以对一部分变量采用值捕获,对其他变量采用引用捕获,可以混合使用隐式捕获和显示捕获。

for_each(vec.begin(), vec.end(),
	[&, c] (const string &s) { os << s << c;}); // os采用隐式引用捕获,s采用显示值捕获
for_each(vec.begin(), vec.end(),
        [=, &os] (const string &s) { os << s << c; }); // os采用显示引用捕获,c采用隐式值捕获

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

可变lambda

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

size_t v1 = 42; 
auto f = [v1] () mutable { return ++v1 };
v1 = 0;
auto j = f();

当然,我们也可以通过引用捕获的方式去修改一个对象,前提是这个对象是一个非常量对象。

指定lambda返回类型

​ 默认情况下,如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void。考虑一个例子,使用transform算法将一个序列中的元素中负数替换为其绝对值。

transform(v.begin(), v.end(), v.begin(),
	[] (int i) { return i < 0 ? -i : i}; ); // transform的前两个参数表示需要输入序列,第三个参数表示替换开始的位置

上面代码中lambda可以自行推断出返回类型,但是如果我们将其改变为if判断形式。

transform(v.begin(), v.end(), v.begin(),
	[] (int i) { if (i < 0) return -i; else return i );  // 错误。默认情况下如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void

所以当我们要指定返回类型时,必须使用尾置返回类型

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

3.4参数绑定

​ 一般的lambda表达式都可以用函数来代替,但是对于捕获局部变量的lambda,就不是很容易用函数进行代替。比如对于上面提到的find_if使用例子,里面捕获了一个sz的变量,当转换成函数形式:

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

如果把这个函数传递给find_if就不行,因为它的第三个参数只接收一个一元谓词,因此可以使用bind解决该问题。

标准库bind函数

bind定义在头文件functional中,可以将bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。下面是bind的一般形式:

auto newCallable = bind(callable, arg_list)

arg_list中的参数有_n之类的名字,这是占位符,表示newCallable的参数,数值n表示生成的可调用对象中参数的位置。

绑定check_size的sz参数

​ 此bind调用只有一个占位符,表示check6只接受单一参数、占位符出现在arg_list的第一个位置,表示check6的此参数对应check_size的第一个参数(const string &)。

auto check6 = bind(check_size, _1, 6);

调用check6必须传递给它一个string类型的参数,check6会将此参数传递给check_size

string s = "hello";
bool b1 = check6(s); // 相当于调用check_size(s, 6)

通过bind,将原来基于lambdafind_if调用替换为check_size版本

auto wc = find_if(vec.begin(), vec.end(),
	[sz] (const string &a));
auto wc = find_if(vec.begin(), vec.end(), bind(check_size, _1, sz));

placeholder

​ 名字_n定义在一个名为placeholders的命名空间中,而这个命名空间本身定义在std命令空间,_1对应的using声明为using std::placeholders::_1。对于每个占位符,都要进行这样的声明非常麻烦,可以使用using namespace namespace_name表明所有来自namespace_name的名字都可以在我们的程序中直接使用,比如using namespace std::placeholders

用bind重排参数顺序

sort(v.begin(),v.end(),isShorter); // 调用cmp(A,B)
sort(v.begin(),v.end(),bind(isShorter,_2,_1));// 调用cmp(B,A)

绑定引用参数

​ 默认情况,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中,如果有些绑定的参数无法进行拷贝时,就需要进行引用。

// 替换一个引用方式捕获ostream的lambda
for_each(v.begin(),v.end(),
	[&os,c] (const string &s) { os << s << c;});

// 用于替换的函数
ostream &print(ostream &os, const string &s, char c) {
	return os << s << c;
}

对于上面替换的函数,我们无法用bind进行绑定,因为ostream无法被拷贝,如果希望传递给bind一个对象而不拷贝它,就可以使用ref函数

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

函数ref返回一个对象,包含给定的引用,此对象可以拷贝,

四、迭代器

​ 头文件iterator定义了额外的几种迭代器

  1. 插入迭代器:绑定于容器上,用于向容器中掺入元素
  2. 流迭代器:绑定于输入或输出流上,用于遍历所有关联的IO流
  3. 反向迭代器:该迭代器向后移动(从右到左),forward_list之外的标准库容器都有反向迭代器
  4. 移动迭代器:用于移动元素,不对它们进行拷贝

4.1插入迭代器

​ 接受一个容器,生成一个迭代器。

image-20230315181625711

一共有三种类型的插入迭代器:

  • back_inserter:创建一个使用push_back的迭代器
  • front_inserter:创建一个使用push_front的迭代器
  • inserter:创建一个使用insert的迭代器。

4.2iostream迭代器

isstream_iterator

​ 当创建一个流迭代器时,必须指定迭代器将要读写的对象类型。它使用>>读取流,所以istream_iterator要读取的类型必须定义了输入运算符。默认初始化迭代器可以当作一个尾后迭代器,如下面的int_eof

istream_iterator<int> int_it(cin); // 从cin读取int
istream_iterator<int> int_eof; // 尾后迭代器
ifstream in("afile");
istream_iterator<string> str_in(in); // 从“afile”读取字符串

再来一个书上从标准输入读取数据的例子:

istream_iterator<int> in_ter(cin); // 从cin读取int
istream_iterator<int> eof; // istream尾后迭代器
while( in_ter != eof ) {
	vec.push_back(*in_iter++);
}

image-20230315183608027

懒惰求值

istream_iterator使用的就是懒惰求值,当我们帮了一个流时,标准库并不保证迭代器立即从流中读取数据,会等到从流中读取数据,知道我们使用迭代器才真正读取。标准库保保证了在第一次解引用迭代器之间,从流中读取数据的操作已经完成了

ostream_iterator

​ 任何具有输出运算符<<的类型都可以定义ostream_iterator。一个ostream_iterator对象接收第二个可选参数,它是一个C风格的字符串(字符串字面值或者以空字符结尾的字符数组指针)。必须将ostream_iterator绑定到一个指定的流,不允许空的或表示尾后位置的ostream_iterator

image-20230315184700278

看一个例子:

ostream_iterator<int> out_iter(cout, " ");
for(auto e : vec) {
	*out_iter++ = e; // 将元素写到cout
    out_iter = e; // 可以忽略解引用和递增运算
}
cout << endl;

上面的代码以此输出vec中的元素,并在每一个输出后面加上一个空格

4.3反向迭代器

​ 反向迭代器的所有操作与正常的迭代器都是相反的,递增操作会从右往左移动。可以通过rbegin、rend、crbegin和crend成员函数来获得反向迭代器。

image-20230315185422913

书上有一个例子,需要从头或者尾部开始输出第一个元素,在从尾部开始时有一点,当我们使用反向迭代器找到了尾部的第一个逗号,下图rcomma,如果我们进行打印会得到TSAL,这时我们需要把反向迭代器rcomma转换成正常的迭代器,可以使用base函数,如下图rcomma.base()注意rcomma转换后没有指向相同的位置,这是因为反向得带器的目的是表示元素范围,而这些范围使不对称的

image-20230315190016961

五、泛型算法结构

算法所要求的迭代器操作可以分成5个迭代器类型:

image-20230315200512285

C++标准指明了泛型和数值算法的每个迭代器参数的最小类别。比如find在一个序列上遍历找目标值,至少需要输入迭代器;replace至少需要一对迭代器,至少是前向迭代器。

5.1迭代器类别

输入迭代器

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

输入迭代器只用于顺序访问,递增它可能导致所有其他指向流的迭代器失效,即不能保证输入迭代器的状态可以保存下来并用来访问元素。所以输入迭代器只能用于单遍扫描算法。find、accumulate要求输入迭代器,istream_iterator是一种输入迭代器。

输出迭代器

  • 用于推进迭代器的前置和后置递增运算(++)
  • 解引用运算符(*),只出现在赋值运算符的左侧

输出迭代器也只能用于单遍扫描算法,用作目的位置的迭代器通常都是输出迭代器。ostream_iterator类型也是输出迭代器。

前向迭代器

​ 可以读写元素,这类迭代器只能在序列中沿一个方向移动,使用前向迭代器可以多次读写同一个元素,因此可以保存前向迭代器的状态,使用它的算法可以对序列进行多遍扫描。算法replace要求前向迭代器,forward_list上的迭代器是前向迭代器。

双向迭代器

​ 可以正向/反向读写序列中的元素,reverse要求双向迭代器,除了forward_list之外,其他标准库都提供符合双向迭代器要求的迭代器。

随机访问迭代器

​ 提供在常量时间内访问序列中任意元素的能力,支持双向迭代器的所有功能。

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

算法sort要求随机访问迭代器,array、deque、string和vector的迭代器都是随机访问迭代器,用于访问内置数组元素的指针也是

5.2算法命名规范

​ 接受谓词参数来代替<==运算符的算法,以及那些不接受额外参数的算法,通常是重载函数。

unique(beg,end); // 使用 == 运算符比较元素
unique(beg,end,comp); // 使用自定义规则比较元素

_if版本的算法

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

find(beg, end, val);
find_if(beg, end, pred); // 查找第一个令pred为真的元素

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

​ 默认情况,重排元素的算法将重排后的元素写回给定的输入序列中。写到额外目的空间的算法(拷贝版本)都在名字后面附加一个_copy

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

六、特定容器算法

​ 链表类型listforward_list定义了几个成员函数形式的算法:

image-20230315204302417

链表版本的算法比对应的通用版本效率更高、代价更小。

splice成员

image-20230315204433207

链表特有的操作会改变容器

链表特有版本与通用版本间的一个至关重要的区别是链表版本会改变底层的容器。例如通用版本的merge将合并的序列写到一个给定的目的迭代器,两个输入序列是不变的。而链表版本的merge函数会销毁给定的链表,元素从参数指定的链表中删除,被合并到调用merge的链表对象中。

​ 默认情况,重排元素的算法将重排后的元素写回给定的输入序列中。写到额外目的空间的算法(拷贝版本)都在名字后面附加一个_copy

reverse(beg, end);
reverse_copy(beg, end, dest);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

madkeyboard

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值