C++ Primer 第十章 泛型算法

泛型算法

  • “算法”:是因为它们实现了一些经典算法的公共接口,如排序和搜索
  • “泛型的”:是因为它们可以用于不同类型的元素和多种容器类型(不仅包括标准库类型,如 vector 或 list,还包括内置的数组类型),以及我们将看到的,还能用于其他类型的序列。

概述

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

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

例如:用 == 运算符完成每个元素与给定值的比较。其他方法可能要求元素类型支持 < 运算符
关键概念:算法永远不会执行容器的操作

初识泛型算法

  • 理解算法的最基本的方法就是了解它们是否读取元素、改变元素或是重排元素顺序

只读算法

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

另一个只读算法是 accumulate 它定义在头文件 numeric 中。接受三个参数,前两个指出了需要求和的元素的范围,第三个参数是和的初始值。

算法和元素类型

  • accumulate 将第三个参数作为求和起点,这蕴含着一个编程假定:将元素类型加到和的类型上的操作必须是可行的
    例:
string sum = accumulate(v.cbegin(), v.cend(), string(""));// 可行
string sum = accumulate(v.cbegin(), v.cend(), ""); // 错误:const char* 上没有定义 + 运算符
// 原因在于,如果我们传递了一个字符串字面值,用于保存和的对象的类型将是 const char*
  • 对于只读取而不改变元素的算法,通常最好使用 cbegin() 和 cend()。

操作两个序列的算法

  • 另一个只读算法是 equal,用于确定两个序列是否保存相同的值。
  • 它将第一个序列中的每个元素与第二个序列中对应元素进行比较。如果所有对应元素都相等 放回 true,否则返回 false
  • 此算法接受三个迭代器:前两个表示第一个序列中的元素范围,第三个表示第二个序列的首元素
// roster2 中的元素数目应该至少与 roster1 一样多
equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());
  • 由于利用迭代器完成操作,因此可以比较两个不同类型的容器中的元素。而且元素类型也不必一样,只要能用 == 来比较两个元素类型即可
    那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长

写容器元素的算法

  • 赋予序列中的元素新值的算法
  • 只要我们传递了一个有效的输入序列,写入操作就是安全的
fill(vec.begin(), vec.end(), 0); // 将每个元素重置为 0
// 将一个容器的子序列设置为 10
fill(vec.begin(), vec.begin() + vec.size() / 2, 10);

算法不检查写操作

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

函数 fill_n 假定写入指定个元素是安全的。即,如下形式的调用

fill_n(dest, n, val);
// fill_n 假定 dest 指向一个元素,而从 dest 开始的序列至少包含 n 个元素
vector<int> vec;
// 灾难:修改 vec 中的 10 个(不存在)元素
fill_n(vec.begin(), 10, 0);

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

介绍 back_inserter

  • 一种保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器。
  • 插入迭代器是一种向容器中添加元素的迭代器
  • back_inserter 定义在头文件 iterator 中,接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。
vector<int> vec;
auto it = back_inserter(vec); // 通过它赋值会将元素添加到 vec 中
*it = 42; // vec 中现在有一个元素,值为 42
// 正确:back_inserter 创建一个插入迭代器,可用来向 vec 添加元素
fill_n(back_inserter(vec), 10, 0); // 添加 10 个元素到 vec

拷贝算法

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

int a1[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int a2[sizeof(a1) / sizeof(*a1)];
auto ret = std::copy(begin(a1), end(a1), a2); // copy 返回的是目的位置迭代器(递增后)的值。
// 即 ret 恰好指向拷贝到 a2 的尾元素之后的位置
  • 多个算法都提供所谓的“拷贝”版本。这些算法计算新元素的值,但不会将它们放置在输入序列的末尾,而是创建一个新序列保存这些结果
replace(ilst.begin(), ilst.end(), 42, 0); // 将所有 0 改为 42
replace_copy(ilst.begin(), ilst.end(), std::back_inserter(ivec), 42, 0);
// ilst 并未改变,ivec 包含 ilst 的一份拷贝,不过在原来 ilst 中值为 0 的元素在 ivec 中都变为 42

重排容器元素的算法

消除重复单词、使用 unique、使用容器操作删除元素

void elimDups(vector<string> &words) {
sort(words.begin(), words.end());
auto end_unique = unique(words.begin(), words.end());
words.erase(end_unique, words.end());
}

定制操作

  • 标准库还为这些算法定义了额外的版本,允许我们提供自己定义的操作来代替默认运算符

向算法传递函数

  • 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);
for (const auto &s : words) cout << s << " ";
cout << endl;

lambda 表达式

我们的新问题是在 vector 中寻找第一个大于等于给定长度的元素。
我们可以用 find_if 来查找第一个具有特定大小的元素,它接受一对迭代器和一个谓词(一元谓词)。find_if 算法对输入序列中的每个元素调用给定的这个谓词。他返回第一个使谓词返回非 0 值的元素,如果不存在这样的元素,返回尾迭代器。

但是 find_if 的任何函数都必须严格接受一个谓词,没有任何办法能传递给它第二个参数表示长度。为了解决此问题,需要引用另外一些语言特性。

介绍 lambda

  • 我们可以向一个算法传递任何类别的可调用对象。

可调用对象包括:函数、函数指针、重载了函数调用运算符的类和 lambda 表达式
一个 lambda 表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。但与函数不同,lambda 可能定义在函数内部。

[capture list](parameter list) -> return type { function body}
  • [capture list] 是一个 lambda 所在函数中定义的局部变量的列表
  • return type、parameter list 和 function body 与任何普通函数一样。分别表示返回类型、参数列表和函数体
  • lambda 必须使用尾置返回
  • 我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体
auto f = [] { return 42; };
  • lambda 的调用方式与普通函数的调用方式相同,都是使用调用运算符
cout << f() << endl; // 打印 42
  • 在 lambda 中忽略括号和参数列表等价于指定一个空参数列表
  • 如果忽略返回类型,lambda 根据函数体中的代码推断出返回类型。如果函数体只是一个 return 语句,则返回类型从返回的表达式的类型推断而来。否则,返回类型为 void
  • 如果lambda 的函数体包含任何单一 return 语句之外的内容,且未指定返回类型,则返回 void

向 lambda 传递参数

  • 调用一个 lambda 时给定的实参被用来初始化 lambda 的形参。通常,实参和形参的类型必须匹配
  • lambda 不能有默认参数
    例:isShorter 函数
[](const string &a, const string &b) {
return a.size() < b.size();
}

可以使用此 lambda 来调用 stable_sort;

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

使用捕获列表

  • 一个 lambda 通过将局部变量包含在其捕获列表中来指出将会使用这些变量。捕获列表指引 lambda 在其内部包含访问局部变量所需的信息
[sz](const string &a) {
return a.size() >= sz;
};

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

调用 find_if

string make_plural(size_t ctr, const string &word, const string &ending) {
return (ctr > 1) ? word + ending : word;
}
int main(int argc, char *argv[]) {
vector<string> words = {"a", "as", "asd"};
int sz;
cin >> 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;
return 0;
}

for_each 算法

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

捕获列表为空,但是其函数体还是使用了两个名字:s 和 cout,前者是他自己的参数
捕获列表为空,是因为我们只对 lambda 所在函数中定义的(非 static)变量使用捕获列表。一个 lambda 可以直接使用定义在当前函数之外的名字。
**捕获列表只用于局部非 static 变量,lambda 可以直接使用局部 static 变量和在它所在函数之外声明的名字

完整的 biggies

void biggies(vector<string> &words, vector<string>::size_type sz) {
elimDups(words);
stable_sort(words.begin(), words.end(), [](const string &a, const string &b) { return a.size() < b.size(); });
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;
}

lambda 捕获和返回

  • 当定义一个 lambda 时,编译器生成一个与 lambda 对应的新的类类型。
  • 目前可以这样理解:当向一个函数传递一个 lambda 时,同时定义了一个新类型和该类型的一个对象:传递的参数是此编译器生成的类类型的未命名对象。类似的,当使用 auto 定义一个用 lambda 初始化的变量时,定义了一个从 lambda 生成的类型的对象。
  • 默认情况下,从 lambda 生成的类都包含一个对应该 lambda 所捕获的变量的数据成员。类似任何普通类的数据成员,lambda 的数据成员也在 lambda 对象创建时被初始化。

值捕获

  • 与参数不同,被捕获的变量的值是在 lambda 创建时拷贝,而不是调用是拷贝
void fcn1() {
size_t v1 = 42;
// 将 v1 拷贝到名为 f 的可调用对象
auto f = [v1] { return v1; };
v1 = 0;
auto j = f(); // j 为 42;f 保存了我们创建它时 v1 的拷贝
}

引用捕获

void fcn2() {
size_t v1 = 42;
// 将 v1 拷贝到名为 f 的可调用对象
auto f2 = [&v1] { return v1; };
v1 = 0;
auto j = f2(); // j 为 0;f 保存了 v1 的引用,而非拷贝
}

当以引用方式捕获一个变量是,必须保证在 lambda 执行时变量是存在的

void biggies(vector<string> &words, vector<string>::size_type sz, ostream &os = cout, char c = ' ') {
for_each(words.begin(), words.end(), [&os, c](const string &s) { os << s << c; });
}

也可以从一个函数返回 lambda。函数可以直接返回一个可调用对象,或者返回一个类对象,该类含有可调用对象的数据成员。
如果函数返回一个 lambda,则与函数不能返回一个局部变量的引用类型,此 lambda 也不能包含引用类型。

隐式捕获

在捕获列表中写一个 & 或 = 可以让编译器根据 lambda 体中的代码来推断我们要使用哪些变量。

  • & 告诉编译器采用捕获引用方式
  • = 则表示采用值捕获方式
auto wc = find_if(words.begin(), words.end(), [=](const string &a) { return a.size() >= sz; });

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

void biggies(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; });
for_each(words.begin(), words.end(), [=, &os](const string &s) { os << s << c; });
}

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

可变 lambda

  • 默认情况下,对于一个值被拷贝的变量,lambda 不会改变其值。如果我们希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字 mutable。
void fcn3() {
size_t v1 = 42;
// 将 v1 拷贝到名为 f 的可调用对象
auto f = [v1]() mutable { return ++v1; };
cout << v1 << endl; // 42
v1 = 0;
auto j = f(); // j 为 43
cout << j << endl; // 43
}
  • 一个引用捕获的变量是否可以修改依赖于此引用指向的是一个 const 类型还是一个非 const 类型
void fcn4() {
size_t v1 = 42;
// v1 是一个非 const 变量的引用
// 可以通过 f2 中的引用来改变它
auto f2 = [&v1] { return ++ v1; };
v1 = 0;
auto j = f2(); // j 为 1
}

指定 lambda 返回类型

  • transform 函数接受三个迭代器和一个可调用对象。前两个迭代器表示输入序列,第三个迭代器表示目的位置。算法对输入序列中每个元素调用可调用对象,并将结果写到目的位置。
  • 默认情况下,如果一个 lambda 体包含 return 之外的任何语句,则编译器假定此 lambda 返回 void
// 正确
transform(vi.begin(), vi.end(), vi.begin(), [](int i) { return i < 0 ? -i : i; });
// 错误:不能推断 lambda 的返回类型,会推断为 void,但是它返回 int(但是VS2022运行成功了,应该是编译器优化了)
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;
});

参数绑定

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

为了用 check_size 来代替此 lambda,必须解决如何向 sz 形参传递一个参数的问题

标准库 bind 函数

  • 使用 bind 来解决
  • 可以将 bind 函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表
    一般形式
auto newCallable = bind(callable, arg_list);
  • newCallable 本身是一个可调用对象
  • arg_list 是一个逗号分隔的参数列表对应给定的 callable 参数
    即当我们调用 newCallable 时,newCallable 会调用 callable,并传递给它 arg_list 中的参数
  • arg 中形如 _n 的名字是占位符,例:std::placeholders::_1

绑定 check_size 的 sz 参数

// check6 是一个可调用对象,接受一个 string 类型的参数
// 并用此 string 和值 6 来调用 check_size
auto check6 = bind(check_size, std::placeholders::_1, 6);
  • 此 bind 调用只有一个占位符,表示 check6 只接受单一参数
string s = "hello";
bool b1 = check6(s); // check6(s) 会调用 check_size(s, 6)
  • 使用 bind,我们可以将原来基于 lambda 的 find_if 调用:
auto wc = std::find_if(words.begin(), words.end(), [sz] (const string &a) { return a.size() > sz; });
  • 替换为
auto wc = std::find_if(words.begin(), words.end(), bind(check_size, std::placeholders::_1, sz));

使用 placeholders 名字

名字 _n 都定义在一个名为 placeholders 的命名空间中,而这个命名空间本身定义在 std 命名空间中。
因此,声明应该为:using std::placeholders::_1;

为了避免出错,可以使用另外一种不同形式的 using 语句

using namespace namespace_name;

例:

using namespace std::placeholders;

使得由 placeholders 定义的所有名字都可用。与 bind 函数一样,placeholders 命名空间也定义在 functional 头文件中。

bind 的参数

  • 我们可以用 bind 修正参数的值。更一般的,可以用 bind 绑定给定可调用对象中的参数或重新安排其顺序。
    例:
// g 是一个有两个参数的可调用对象
auto g = bind(f, a, b, _2, c, _1);

这个 bind 调用会将

g(_1, _2);

映射为

f(a, b, _2, c, _1);

即,对 g 的调用会调用 f,用 g 的参数代替占位符,再加上绑定的参数 a、b 和 c。例如,调用 g(X, Y) 会调用 f(a, b, Y, c, X)

用 bind 重排参数顺序

下面是用 bind 重排参数顺序的一个具体例子:

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

绑定引用参数

例如:替换一个引用方式捕获 ostream 的 lambda

// os 是一个局部变量,引用一个输出流
// c 是一个局部变量,类型为 char
for_each(words.begin(), words.end(), [&os, c](const string &s) { os << s << c; });

可以很容易地编写一个函数,完成相同的工作

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

但是不能直接用 bind 来代替对 os 的捕获,原因在于 bind 拷贝其参数,而我们不能拷贝一个 ostream。如果我们希望传递给 bind 一个对象而又不拷贝它,就必须使用标准库 ref 函数

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

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

再探迭代器

除了为每个容器定义的迭代器之外,标准库在头文件 iterator 中还定义了额外几种迭代器。这些迭代器包括以下几种。

  • 插入迭代器:这些迭代器被绑定到一个容器上,可用来向容器插入元素
  • 流迭代器:这些迭代器被绑定到输入或输出流上,可用来遍历所关联的 IO 流
  • 反向迭代器:这些迭代器向后而不是向前移动。除了 forward_list 之外的标准库容器都有反向迭代器。
  • 移动迭代器:这些专用的迭代器不是拷贝其中的元素,而是移动它们。

插入迭代器

插入器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。当我们通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。

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

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

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

理解插入器的工作过程是很重要的:当调用 inserter(c, iter) 时,我们得到一个迭代器,接下来使用它时,会将元素插入到 iter 原来所指向的元素之前的位置。即,如果 it 是由 inserter 生成的迭代器,则下面的语句

*it = val;

其效果与下面代码一样

it = c.insert(it, val); // it 指向新加入的元素
++ it; // 递增 it 使它指向原来的元素

front_inserter 生成的迭代器的行为与 inserter 生成的迭代器完全不一样。当我们使用 front_inserter 时,元素总是插入到容器第一个元素之前。即使我们传递给 inserter 的位置原来指向第一个元素,只要我们在此元素之前插入一个新元素,此元素就不再是容器的首元素了:

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

iostream 迭代器

虽然 iostream 类型不是容器,但标准库定义了可以用于这些 IO 类型对象的迭代器。

  • istream_iterator 读取输入流
  • ostream_iterator 向一个输出流写数据
    这些迭代器将它们对应的流当作一个特定类型的元素序列来处理。通过使用流迭代器,我们可以用泛型算法从流对象读取数据以及向其写入数据。

istream_iterator 操作

  • 当创建一个流迭代器时,必须指定迭代器将要读写的对象类型。
  • istream_iterator 使用 >> 来读取流。因此,istream_iterator 要读取的类型必须定义了输入运算符。
  • 当创建一个 istream_iterator 时,我们可以将它绑定到一个流。我们还可以默认初始化迭代器,这样就创建了一个可以当作尾后值使用的迭代器
std::istream_iterator<int> int_it(cin); // 从 cin 读取 int
std::istream_iterator<int> int_eof; // 尾后迭代器
std::ifstream in("afile");
std::istream_iterator<string> str_it(in); // 从 "afile" 读取字符串

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

std::istream_iterator<int> in_iter(cin); // 从 cin 读取 int
std::istream_iterator<int> eof; // istream 尾后迭代器
while (in_iter != eof) { // 一旦其关联的流遇到文件尾或遇到 IO 错误,迭代器的值就与尾后迭代器相等
// 后置递增运算读取流,返回迭代器的旧值
// 解引用迭代器,获得从流读取的前一个值
vec.push_back(*in_iter ++);
}

还可以将程序重写为如下形式

std::istream_iterator<int> in_iter(cin), eof; // 从 cin 读取 int
vector<int> vec(in_iter, eof); // 从迭代器范围构造 vec
// 这个构造函数从 cin 中读取数据,直至遇到文件尾或者遇到一个不是 int 的数据为止、
// 从流中读取的数据被用来构造 vec
istream_iterator<T> in(is); in 从输入流 is 读取类型为 T 的值
istream_iterator<T> end; 读取类型为 T 的值的 istream_iterator 迭代器,表示尾后位置
in1 == in2 in1 和 in2 必须读取相同类型。如果它们都是尾后迭代器,或绑定到相同
in1 != in2 的输入,则两者相等
*in 返回从流中读取的值
in -> mem 与 (*in).mem 的含义相同
++ in, in ++ 使用元素类型所定义的 >> 运算符从输入流中读取下一个值。与以往一样,
前置版本返回一个指向递增后迭代器的引用,后置版本返回旧值

使用算法操作流迭代器

  • 由于算法使用迭代器操作来处理数据,而流迭代器又至少支持某些迭代器操作,因此我们至少可以用某些算法来操作流迭代器。
std::istream_iterator<int> in(cin), eof;
cout << std::accumulate(in, eof, 0) << endl; // 会计算出从标准输入读取的值的和。

istream_iterator 允许使用懒惰求值

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

ostream_iterator 操作

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

ostream_iterator<T> out(os); out 将类型为 T 的值写到输出流 os 中
ostream_iterator<T> out(os, d); out 将类型为 T 的值写到输出流 os 中,每个值
后面都输出一个 d。d 指向一个空字符结尾的字符数组
out = val 用 << 运算符将 val 写入到 out 所绑定的 ostream 中,val 的类型
必须与 out 可写的类型兼容
*out, ++ out, out ++ 这些运算符是存在的,但不对 out 做任何事情。每个运算符都返回 out

我们可以用 ostream_iterator 来输出值的序列

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

可以忽略引用和递增运算。

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

运算符 * 和 ++ 实际上对 ostream_iterator 对象不做任何事情,因此忽略它们对我们的程序没有任何影响。
但是第一种写法,流迭代器的使用与其他迭代器的使用保持一致。如果想将此循环改为操作其他迭代器类型,修改起来非常容易。而且对于读者来说,此循环的行为也更为清晰。

可以通过调用 copy 来打印 vec 中的元素,这比编写循环更为简单

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

使用流迭代器处理类类型

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

std::istream_iterator<Sales_item> item_iter(cin), eof;
std::ostream_iterator<Sales_item> out_iter(cout, "\n");
// 将第一笔交易记录存在 sum 中,并读取下一条记录
Sales_item sum = *item_iter ++;
while (item_iter != eof) {
if (item_iter->isbn() == sum.isbn()) {
sum += *item_iter ++;
} else {
out_iter = sum;
sum = *item_iter ++;
}
}
out_iter = sum;

反向迭代器

  • 反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器
  • (++ it)会移动到前一个元素
  • (-- it)会移动到下一个元素
  • 除了 forw_list 其他容器都支持反向迭代器
  • 我们可以通过 rbegin、rend、crbegin 和 crend 成员函数来获得反向迭代器

image

vector<int> vec = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
// 9 8 7 6 5 4 3 2 1 0
for (auto r_iter = vec.crbegin(); r_iter != vec.crend(); ++ r_iter) {
cout << *r_iter << ' ';
}
cout << endl;

还可以按用到算法中

sort(vec.begin(), vec.end()); // 从小到大
sort(vec.rbegin(), vec.rend()); // 从大到小

反向迭代器需要递减运算符

  • 我们只能从既支持 ++ 也支持 -- 的迭代器来定义反向迭代器。因此,不可能从一个 forw_list 或一个 流迭代器创建反向迭代器

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

假定有一个名为 line 的 string,保存着一个逗号分隔的单词列表,我们希望打印 line 中的第一个单词。

string line = "FIRST,MIDDLE,LAST";
auto comma = find(line.cbegin(), line.cend(), ',');
cout << string(line.cbegin(), comma) << endl;

如果希望打印最后一个单词,可以改用反向迭代器

auto rcomma = find(line.crbegin(), line.crend(), ',');
// 指向反向搜索中找到的第一个逗号

但是

cout << string(line.crbegin(), rcomma) << endl; // 会输出 TSAL
// 因为我们使用的是反向迭代器,会反向处理 string。因此会从 crbegin 反向打印 line 中的内容
// 所以需要调用 reverse_iterator 的 base 成员将 rcomma 转换回一个普通迭代器
cout << string(rcomma.base(), line.cend()) << endl;
// 正确:得到一个正向迭代器,从逗号开始读取字符直到 line 末尾

image

泛型算法结构

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

5 类迭代器

  • 迭代器是按它们所提供的操作来分类的,而这种分类形成了一种层次。除了输出迭代器之外,一个高层类别的迭代器支持低层类别迭代器的所有操作
  • 对每个迭代器参数来说,其能力必须与规定的最小类别至少相当。向算法传递一个能力更差的迭代器会产生错误

迭代器类别

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

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

输出迭代器:可以看作输入迭代器功能上的补集——只写而不读元素。输出迭代器必须支持

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

前向迭代器:可以读写元素。

  • 这类迭代器只能在序列中沿一个方向移动。
  • 前向迭代器支持所有输入和输出迭代器的操作,而且可以多次读写同一个元素。

双向迭代器:可以正向/反向读写序列中的元素。

  • 除了支持所有前向迭代器的操作之外,双向迭代器还支持前置和后置递减运算符(--)

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

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

算法形参模式

大多数算法具有如下 4 种形式之一

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

alg是算法名字,begend表示算法所操作的输入范围。destbegend都是迭代器参数。
如果用到这些迭代器参数,它们分别承担指定目的位置和第二个范围的角色。除了这些迭代器参数,一些算法还接受额外的、非迭代器的特定参数。

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

  • dest 参数是一个表示算法可以写入的目的位置的迭代器。算法假定:按其需要写入数据,不管写入多少个元素都是安全的。
    向输出迭代器写入数据的算法都假定目标空间足够容纳写入的数据

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

  • 接受单独的 beg2 或是接受 beg2 和 end2 的算法用这些迭代器表示第二个输入范围。这些算法通常使用第二个范围中的元素与第一个输入范围结合来进行一些运算
  • 如果一个算法接受 beg2 和 end2,这两个迭代器表示第二个范围。
  • 只接受单独的 beg2(不接受 end2)的算法将 beg2 作为第二个输入范围中的首元素。
    接受单独 beg2 的算法假定从 beg2 开始的序列与 beg 和 end 所表示的范围至少一样大

算法命名规范

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

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

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

_if 版本的算法

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

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

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

默认情况下,重排元素的算法将重拍后的元素写回给定的序列中。这些算法还提供另一个版本,将元素写到一个指定的输出目的位置。

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

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

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

特定容器算法

与其他容器不同,链表类型 list 和 forward_list 定义了几个成员函数形式的算法。特别是,它们定义了独有的 sort、merge、remove、reverse 和 unique。通用版本的 sort 要求随机访问迭代器,因此不能用于 list 和 forw_list 因为这两个类型分别提供双向迭代器和前向迭代器。

链表类型定义的其他算法的通用版本可以用于链表,但代价太高。这些算法需要交换输入序列中的元素。一个链表可以 通过改变元素间的链接而不是真的交换它们的值来快速“交换”元素。
对于 list 和 forward_list ,应该优先使用成员函数版本的算法而不是通用算法

这些操作都返回 void
lst.merge(lst2) 将来自 lst2 的元素合并入 lst。lst 和 lst2 都必须是有序的。
lst.merge(lst2, comp) 元素将从 lst2 中删除。在合并之后,lst2 变为空。第一个版本使用 < 运算符
第二个版本使用给定的比较操作
lst.remove(val) 调用 erase 删除掉与给定值相等(==) 或令一元谓词为真的每个元素
lst.remove_if(pred)
lst.reverse() 反转 lst 中元素的顺序
lst.sort() 使用 < 或给定比较操作排序元素
lst.sort(comp)
lst.unique() 调用 erase 删除同一个值得连续拷贝。第一个版本使用 ==;第二个版本
lst.unique(pred) 使用给定得二元谓词

splice 成员

链表类型还定义了 splice 算法,此算法是链表数据结构所特有得,因此不需要通用版本

lst.splice(args) 或 flst.splice_after(args)
(p, lst2) p 是一个指向 lst 中元素的迭代器,或一个指向 flst 首前位
置的迭代器。函数将 lst2 的所有元素移动到 lst 中 p 之前的
位置或是 flst 中 p 之后的位置。将元素从 lst2 中删除。lst2
的类型必须与 lst 或 flst 相同,且不能是同一个链表
(p, lst2, p2) p2 是一个指向 lst2 中位置的有效的迭代器。将 p2 指向的元
素移动到 lst 中,或将 p2 之后的元素移动到 flst 中。lst2
可以是与 lst 或 flst 相同的链表
(p, lst2, b, e) b 和 e 必须表示 lst2 中的合法范围。将给定范围中的元素从
lst2 移动到 lst 或 flst。lst2 与 lst(或flst)可以是相
同的链表,但 p 不能指向给定范围中的元素

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

多数链表特有的算法都与其通用版本很相似,但不完全相同。链表特有版本与通用版本间的一个至关重要的区别是,链表版本会改变底层的容器。

posted @   HuiPuKui  阅读(9)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示