《C++Primer》第十章——泛型算法

第十章:泛型算法

泛型:这些算法是通用的,它们可以用于不同类型的容器和不同类型的元素
算法:因为其实现了一些经典算法的公共接口,如排序和搜索

10.1 概述

1.大多数算法都定义在头文件 algorithm 中;标准库还在头文件 numeric 中定义了一组数值泛型算法
2.一般情况下,算法不直接操作容器,而是遍历由两个指定的一个元素范围了进行操作,通常情况下,算法遍历范围,对其中每个元素进行一些处理

int val = 42;
//如果在vec中找到想要的元素,则返回结果指向它,否则返回结果为vec.cend()
auto result = find(vec.cbegin(), vec.cend(), val);

1)由于指针就像内置数组上的迭代器一样,我们可以用 find 在数组中找值

int ia[] = {27,210,12,47,109,83};
int val = 83;
int* result = find(begin(ia),end(ia),val);

2)还可以在序列的子范围中查找,只需将子范围首元素和尾元素之后位置的迭代器(指针)传递给 find

//在从ia[1]开始,直至(但不包含)ia[4]的范围内查找
auto result = find(ia + 1, ia + 4, val);

3.迭代器令算法不依赖于容器但算法依赖于元素类型的操作,大多数算法都使用了一个或多个元素类型上的操作,如 == 或 < 等等,大多数算法提供了一种方法,允许我们使用自定义的操作来代替默认的运算符
4.算法永远不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作

10.2 初始泛型算法

除少数例外,标准库算法都对一个范围内的元素进行操作,将这个元素范围称为“输入范围”,接收输入范围的算法总是使用两个参数来表示此范围,两个参数分别是指向要处理的第一个元素和尾元素之后位置的迭代器
大多数算法遍历输入范围的方式相似,但它们使用范围中元素的方式不同,理解算法最基本的方法是了解它们是否读取元素、改变元素或者是否重排元素熟顺序
1.只读算法:该算法只会读取其输入范围内的元素,而从不改变元素
1)对于只读取而不改变元素的算法,通常最好使用 cbegin() 和 cend(),但是如果计划使用算法返回的迭代器来改变元素的值,就需要使用 begin() 和 end() 的结果作为参数
2)对于只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长

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

2.写容器的算法
1)插入迭代器:插入迭代器是一种向容器中添加元素的迭代器,可以保证算法有足够元素空间来容纳输出数据。
通常情况,当我们通过一个迭代器向容器元素赋值时,值被赋予迭代器指向的元素;而当我们通过一个插入迭代器赋值时,一个与赋值号右侧值相等的元素被添加到容器中
2)算法不检查写操作:向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素

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

上面的调用是一场灾难,因为 vec 中并没有元素,这条语句是未定义的
3)介绍 back_inserter:定义在头文件 iterator 中的一个函数,接受一个指向容器的引用返回一个与该容器绑定的插入迭代器,当通过此迭代器赋值时,赋值运算符会调用 push_back 将一个具有给定值的元素添加到容器中

vector<int> vec;	//空向量
fill_n(back_inserter(vec), 10, 0);	//添加10个元素到vec

由于传递的参数是 back_inserter 返回的插入迭代器,因此每次赋值都会在 vec 上调用 push_back,最终这条语句向 vec 的末尾添加了10个元素
4)拷贝算法:拷贝算符是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法,接收三个迭代器,因此传递给 copy 的目的序列至少要包含与输入序列一样多的元素
**多个其他算法都提供相应的“拷贝”版本:**这些算法计算元素的值,但不会将它们放置在输入序列的末尾,而是创建一个新序列保存这些结果

//将所有值为0的元素改为42
replace(ilst.begin(), ilist.end(), 0, 42);
//使用back_inserter按需要增长目标序列
replace_copy(ilst.cbegin(), ilst.cend(), back_inserter(ivec), 0, 42);

对于 replace_copy,ilst 并未改变,ivec 包含 ilst 的一份拷贝,并在 ivec 中将原来的 0 改变为 42
3.重排容器元素的算法:这些算法会重排容器中元素的顺序(p 343)

10.3 定制操作

很多算法都会比较输入序列中的元素,默认情况下使用元素类型的 < 或 == 运算符完成比较,标准库还为这些算法定义了额外的版本,允许我们提供自己定义的操作来代替默认运算符
重载 sort 的默认行为的两种情况:
1)sort 算法默认使用元素类型的 < 运算符,但我们希望的排序与 < 所定义的顺序不同
2)我们的序列可能保存的是未定义 < 运算符的元素类型
1.向算法传递函数
1)谓词

  • 是一个可调用的表达式,其返回结果是一个能用做条件的值。包括一元谓词,只接受单一参数;还有二元谓词,意味有两个参数
  • 接受谓词参数的算法对输入序列中的元素调用谓词,因此,元素类型必须能转换为谓词的参数类型
bool isShorter(const string &s1, const string &s2)
{
	return s1.size() < s2.size()
}
//按长度由短到长排序
sort(words.begin(),words.end(),isShorter);

2.lambda表达式
1)介绍 lambda

  • 我们可以向一个算法传递任何类别的可调用对象,对于一个对象或一个表达式,如果可以对其使用调用运算符,则称是可调用的
    注:目前为止,我们使用的两种可调用对象是函数函数指针,还有两种可调用对象,一个是重载了函数调用运算符的类,另一种即为 lambda 表达式
  • 一个 lambda 表达式表示一个可调用的代码单元,也可理解为是一个未命名的内联函数,与任何函数类似,一个 lambda 具有一个返回类型、一个参数列表和一个函数体,与函数不同,lambda 可能定义在函数内部
    [capture list] (parameter list) -> return type { function body }
    capture list 是一个 lambda 所在函数中定义的局部变量的列表(通常为空)
    其他部分与任何不同函数一样,但 lambda 必须使用尾置返回来指定返回类型
  • 我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体
auto f = [] {return 42;};
cout << f() << endl;//打印 42

在 lambda 中忽略括号和参数列表等价于指定一个空参数列表;如果忽略返回类型,lambda 根据函数体中的代码推断返回类型
注:若 lambda 的函数体包含任何单一 return 语句之外的内容,且未指定返回类型,则返回 void
2)向 lambda 传递参数
与普通函数调用类似,调用一个 lambda 时给定的实参被用来初始化 lambda 的形参,但不同的是,lambda 不能有默认参数,因此,一个lambda 调用的实参数目永远与形参数目相等

//使用二元谓词的方式
bool isShorter(const string &s1, const string &s2)
{
	return s1.size() < s2.size()
}
stable_sort(words.begin(),words.end(),isShorter);

//使用 lambda 的方式
//空捕获列表表明此 lambda 不使用它所在函数中的任何局部变量
stable_sort(words.begin(), words.end(), 
			[](const string &a, const string &b){return a.size() < b.size();});

3)使用捕获列表

  • 一个 lambda 可以出现在一个函数中,使用该函数的局部变量,但它必须明确指定使用哪些变量才能在函数体中使用此变量,一个 lambda 通过将局部变量包含在其捕获列表中来指出将会使用哪些变量,捕获列表指引 lambda 在其内部包含访问局部变量所需的信息
  • 捕获列表只用于局部非 static 变量,lambda 可以直接使用局部 static 变量和它所在函数之外声明的名字
    3.lambda捕获和返回
    1)当定义一个 lambda 时,编译器生成一个与 lambda 对应的新的(未命名的)类类型,也可以理解为,当向一个函数传递一个 lambda 时,同时定义了一个新类型和该类型的一个对象,默认情况下,从 lambda 生成的类都包含一个对应该 lambda 所捕获的变量的数据成员,类似任何普通类的数据成员,lambda 的数据成员也在 lambda 对象创建是被初始化
    2)值捕获:与函数传值参数类似,采用值捕获的前提是变量可拷贝,但不同的是,被捕获的变量的值是在 lambda 创建时拷贝而非调用时拷贝
void fcn1()
{
	size_t v1 = 42;
	auto f = [v1] { return v1; };
	v1 = 0;
	auto j = f();	//j为42,f保存了我们创建它时v1的拷贝
}

由于被捕获变量的值实在 lambda 创建时拷贝,因此随后的修改并不影响
3)引用捕获:定义 lambda 时可以采用引用方式捕获变量

void fcn2()
{
	size_t v1 = 42;
	//对象 f2 包含 v1 的引用
	auto f2 = [&v1] { return v1; };
	v1 = 0;
	auto j = f2();	//j 为0,f2 保存 v1 的引用,而非拷贝
}

注:当以应用方式捕获一个变量时,必须保证在 lambda 执行时变量时存在的,lambda 不过的都是局部变量,这些变量在函数结束后就不复存在了,若果 lambda 可能在函数结束后执行,捕获的引用指向的局部变量已经消失则会导致错误
注:一般来说,我们应尽量减少捕获的数据量,而且可能的话,应避免捕获指针或引用
4)隐式捕获:除了显式列出我们希望使用来自所在函数的变量之外,还可以让编译器根据 lambda 体中的代码来推断我们要使用哪些变量,为了指示编译器推断捕获列表,应在捕获列表中写一个&或=,其中&告诉编译器采用捕获引用方式,=则表示采用值捕获方式

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

我们也可以混合使用隐式捕获和显式捕获,捕获列表中的第一个元素必须是一个&或=,此符号指定了默认捕获方式为引用或值,而显示捕获的变量必须使用与隐式捕获不同的方式,比如如果隐式捕获是引用方式,则显示捕获命名变量必须采用值方式,反之亦是

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; });

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

void fcn3()
{
	size_t v1 = 42;		//局部变量
	auto f = [v1] () mutable { return v1; };
	v1 = 0;
	auto j = f();	//j为43
}

对于一个引用捕获的变量是否可以修改依赖于此引用指向的是一个 const 类型还是一个非 const 类型

void fcn4()
{
	size_t v1 = 42;
	//v1是一个非const变量的引用
	//可以通过f2中的引用来改变它
	auto f2 = [&v1] { return ++v1; };
	v1 = 0;
	auto j = f2();	//j为1
}

4.参数绑定
1)标准库 bind 函数:bind 标准库函数,定义在头文件 functional 中,可以将 bind 函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表
调用 bind 的一般形式为:auto newCallable = bind(callable, arg_list);
其中,newCallable 本身是一个可调用对象,arg_list 是一个逗号分隔的参数列表,对应给定的 callable 的参数
即,当我们调用 newCallable 时,newCallable 会调用 callable,并传递给它 arg_list 中的参数
bind 的参数列表中,形如 _n 的参数称为“占位符”,表示 newCallable 的参数,它们占据了传递给 newCallable 的参数的“位置”,_1 则表示是 newCallable 的第一个参数…

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

因为 bind 调用中只有一个占位符,表示 check6 只接受单一参数,而占位符出现在 arg_list 的第一个位置,表明 check6 的此参数对应 check_size 的第一个参数

//基于lambda的find_if调用
auto wc = find_if(words.begin(), words.end(), [sz](const string &a) {return a.size() >= sz; });

bool check_size(const string &s, string::size_type sz)
{
	return s.size() >= sz;
}
//使用check_size的版本
auto wc = find_if(words.begin(), words.end(), bind(check_size, _1, sz));

此 bind 调用生成一个可调用对象,将 check_size 的第二个参数绑定到 sz 的值,当 find_if 对 words 中的 string 调用此对象时,这些对象会调用 check_size,将给定的 string 和 sz 传递给他
2)使用 placeholders 名字
名字 _n 都定义在名为 placeholders 的命名空间中,而这个命名空间本身定义在 std 命名空间中
与 bind 函数一样,placeholders 命名空间也定义在 functional 头文件中

//_1对应的using声明
//但这样很麻烦,对于每个占位符名字,都需要提供一个单独的 using 声明
using std::placeholders::_1

//另外一种不同形式的 using 语句
//using namespace namespace_name,表明来自 namespace_name 的名字都可以在我们的程序中直接使用
using namespace std::placeholders	//表明 placeholders 定义的所有名字都可用

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

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

g 是一个有两个参数的可调用对象,分别用占位符 _2 和 _1 表示
这个新的可调用对象将自己的参数作为第三个和第五个参数传递给 f,f 的第一个、第二个、第四个参数被绑定在给定的值 a、b、c上
传递给 g 的参数按位置绑定到占位符,即第一个参数绑定到 _1,而第二个参数绑定到 _2
所以,g(_1, _2) ----> f(a, b, _2, c, _1)
4)绑定引用参数
默认情况下,bind 的那些不是占位符的参数被拷贝到 bind 返回的可调用对象中,但有时希望以引用方式传递

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 一个对象而又不拷贝它,则必须使用标准库 ref 函数
for_each(words.begin(), words.end(), bind(print, ref(os), ' '));

函数 ref 返回一个对象,包含给定的引用,此对象是可拷贝的
标准库中还有一个 cref 函数,生成一个保存 const 引用的类
与 bind 一样,函数 ref 和 cref 都定义在 functional 中

10.4 再探迭代器

除了为每个容器定义的迭代器之外,标准库在头文件 iterator 中额外定义了几种迭代器
1.插入迭代器:这些迭代器被绑定在一个容器上,可以用来向容器插入元素(p 358)
2.流迭代器:这些迭代器被绑定在输入流或输出流上,可用来遍历所关联的 IO 流,尽管 iostream 类型不是容器,但通过使用流迭代器,我们可以用泛型算法从流对象读取数据以及向流写入数据
1)istream_iterator:读取输入流

  • 当创建一个流迭代器时,必须指定迭代器将要读写的对象类型
  • 还可以默认初始化迭代器,这样就创建了一个当作尾后值使用的迭代器
  • 对于一个绑定到流的迭代器,一旦其关联的流遇到文件尾或遇到 IO 错误,迭代器的值就与尾后迭代器相等
istream_iterator<int> in_iter(cin), eof;
vector<int> vec(in_iter, eof);

2)ostream_iterator:向一个输出流写数据

  • 可以对任何具有输出运算符(<< 运算符)的类型定义 ostream_iterator
  • 当创建一个 ostream_iterator 时,可以提供可选的第二参数,它是一个字符串,在输出每个元素后都会打印此字符串,但是该字符串必须是一个 C 风格字符串,即一个字符串字面值常量或者一个指向空字符结尾的字符数组的指针
ostream_iterator<int> out_iter(cout, " ");
for(auto e : vec)
	*out_iter++ = e;	//赋值语句实际上就是将元素写到 cout
cout << endl;

3)使用流迭代器处理类类型
我们可以为任何定义了输入运算符(>>)的类型创建 istream_iterator 对象
类似的,只要类型有输出运算符(>>),我们就可以为其定义 ostream_iterator
3.反向迭代器

  • 这些迭代器向后而不是向前移动,除了 forward_list 之外的标准容器库都有反向迭代器
  • 反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器,对于反向迭代器,递增和递减操作的含义会颠倒,递增一个反向迭代器会移动到前一个元素;递减一个迭代器会移动到下一个元素
  • 可以通过调用 rbegin、rend、crbegin、crend 成员函数来获得反向迭代器,这些成员函数分别指向容器尾元素和首元素之前一个位置的迭代器
  • 只能从既支持++也支持–的迭代器来定义反向迭代器,因此除了 forward_list 之外,标准容器上的迭代器都既支持递增运算又支持递减运算,但是流迭代器不支持递减运算,因此不可能从一个 forward_list 或一个流迭代器创建反向迭代器
    1)反向迭代器和其他迭代器之间的关系(p 363)
    补充:通过调用 reverse_iterator 的 base 成员函数可以将反向迭代器转换成普通迭代器,但要注意转换前后的迭代器变化,指向的并不是同一位置
    4.移动迭代器:这些专用的迭代器不是拷贝其中的元素,而是移动它们(p 480)

10.5 泛型算法结构

算法分类的方式除了10.2中按照是否读、写或者重排序列中的元素来分类,还可以按照算法所要求的迭代器操作进行分类
任何算法的最基本的特性是它要求迭代器提供哪些操作,因此可以将算法所要求的迭代器操作分为5个迭代器类别,每个算法都会对它的每个迭代器参数指明须提供哪类迭代器
1. 5类迭代器
1)输入迭代器:只读,不写;单遍扫描;只能递增
eg: istream_iterator
2)输出迭代器:只写,不读;单遍扫描;只能递增
eg: ostream_iterator
3)前向迭代器:可读写;多遍扫描;只能递增
eg: forward_list 上的迭代器是前向迭代器
4)双向迭代器:可读写;多遍扫描;可递增递减
eg: 除了 forward_list 之外,其他标准库都提供符合双向迭代器要求的迭代器
5)随机访问迭代器:可读写;多遍扫描;支持全部迭代器计算,即提供在常量时间内访问序列中任意元素的能力
eg: array、vector、deque 和 string 的得带器都是随机访问迭代器,用于访问内置数组元素的指针也是
2.算法形参模式(p 367)
3.算法命名规范
1)一些算法使用重载形式传递一个谓词
接受谓词参数来代替 < 或 == 运算符的算法,以及那些不接受额外参数的算法,通常都是重载的函数
函数的一个版本用元素类型的运算符比较元素;另一个版本接受一个额外谓词参数来代替 < 或 =
由于两个版本的函数在参数上不相等,因此具体调用哪个版本不会产生歧义

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

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

find(beg, end, val);	//查找输入范围中 val 第一次出现的位置
find_if(beg, end, pred);	//查找第一个令 pred 为真的元素

3)区分拷贝元素的版本和不拷贝的版本
默认情况下,重排元素的算法将重排后的元素写回给定的输入序列中,算法还提供了另一个版本,将元素写到一个指定的输出目的位置
写到额外目的空间的算法都在名字后面附加一个 _copy

reverse(beg, end);				//反转输入范围中元素的顺序
reverse_copy(beg, end, dest);	//将元素按逆序拷贝到 dest

补充:某些算法同时 _copy 和 _if 版本

10.6 特定容器算法

  • 与其他容器不同,链表类型 list 和 forward_list 定义了几个成员函数形式的算法
  • list 和 forward_list 有独有的 sort、merge、remove、reverse 和 unique
  • 通用版本的 sort 要求随机访问迭代器,而 list 和 forward_list 只能分别提供双向迭代器和前向迭代器,因此只能使用成员函数版本的 sort
  • 链表定义的其他算法的通用版本可以用于链表,但通用版本的性能一般比成员函数版本的性能低得多,因此对于 list 和 forward_list,应该优先使用成员函数笨笨的算法而非通用算法
    1)链表特有的操作会改变容器
    多数链表特有的算法那都与其通用版本相似,但不完全相同,链表特有的版本与通用版本间的一个至关重要的区别就是链表版本会改变底层的容器,即会修改给定的链表
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值