C++(20/23)标准模板库编程 - 11 算法 第二部分

本章是前一章的延续,主要介绍更多 STL 算法,包括
For_Each 遍历算法

变换算法

生成算法

查找算法

包含算法

搜索算法

累积与折叠算法

本章部分内容仅涵盖命名空间 std::ranges 中的 C++20/23 算法。但需注意,其中许多算法在命名空间 std 中也有对应的 C++20 之前版本。同时请牢记,大多数 STL 算法都定义了多重重载。使用时务必审查所有可选方案,为每个用例选择最合适的重载版本。

For_Each 算法


for_each 算法将函数对象应用于区间内每个解引用的迭代器。这些算法有时可作为显式编码的区间 for 循环的替代方案。清单 11-1-1 展示了示例 Ch11_01_ex1() 的源代码,该示例演示了 std::for_each() 和 std::ranges::for_each() 的用法。

//-------------------------------------------------------------------------
// Ch11_01_ex.cpp
//-------------------------------------------------------------------------
#include <algorithm>
#include <array>
#include <cmath>
#include <numbers>
#include <numeric>
#include "Ch11_01.h"
#include "MT.h"
void Ch11_01_ex1()
{
    using namespace std::numbers;
    const char* fmt = "{:9.2f} ";
    size_t epl_max {8};
    // initialize test array of radii
    std::array<double, 8> radii {};
    std::iota(radii.begin(), radii.end(), 1);
    MT::print_ctr("sphere radii:\n", radii, fmt, epl_max);
    // using std::for_each to print sphere areas
    auto sphere_area = [](double r)
        { std::print("{:9.2f} ", 4.0 * pi * r * r); };
    std::println("\nsphere surface areas:");
    std::for_each(radii.begin(), radii.end(), sphere_area);
    std::println("");
    // using std::ranges::for_each to print sphere volumes
    auto sphere_vol = [](double r)
        { std::print("{:9.2f} ", 4.0 * pi * r * r * r / 3.0); };
    std::println("\nsphere volumes:");
    std::ranges::for_each(radii, sphere_vol);
    std::println("");
}

 

Ch11_01_ex1() 中的第一个代码块使用 std::iota() 初始化 std::array<double, 8> radii。在随后的代码块中,Ch11_01_ex1() 定义了 sphere_area(),该函数使用提供的半径参数计算并打印球体表面积。同一代码块中,语句 std::for_each(radii.begin(), radii.end(), sphere_area) 将 sphere_area() 应用于[radii.begin(), radii.end())区间内每个解引用的迭代器。在当前示例中,由于半径 r 以值传递方式传给 sphere_area(),执行 std::for_each() 不会修改数组 radii 中的值。Ch11_01_ex1() 的最后一个代码块使用函数对象 sphere_vol() 结合 std::ranges::for_each() 来计算球体体积。 注意此处 std::ranges::for_each() 使用容器名 radii 作为范围参数。

示例 Ch11_01_ex2(),如代码清单 11-1-2 所示,重点展示了 std::for_each_n() 和 std::ranges::for_each_n() 的用法。这些 STL 算法用于计算并打印不同棱长下的八面体表面积和体积。请注意,std::for_each_n() 和 std::ranges_for_each_n() 都通过起始迭代器和元素数量来指定目标范围。

void Ch11_01_ex2()
{
    using namespace std::numbers;
    const char* fmt = "{:9.2f} ";
    size_t epl_max {8};
    // initialize test array of edge lengths
    std::array<double, 8> edge_lengths {};
    std::iota(edge_lengths.begin(), edge_lengths.end(), 1);
    MT::print_ctr("octahedron edge lengths:\n", edge_lengths, fmt, epl_max);
    // using std::for_each_n to print octahedron areas
    auto octahedron_area = [](double el)
        { std::print("{:9.2f} ", 2.0 * sqrt3 * el * el); };
    std::println("\noctahedron surface areas:");
    std::for_each_n(edge_lengths.begin(), edge_lengths.size(), octahedron_area);
    std::println("");
    // using std::ranges::for_each_n to print octahedron volumes
    auto octahedron_vol = [](double el)
        { std::print("{:9.2f} ", sqrt2 * el * el * el / 3.0); };
    std::println("\noctahedron volumes:");
    std::ranges::for_each_n(edge_lengths.begin(), edge_lengths.size(),
        octahedron_vol);
    std::println("");
}

代码清单 11-1-3 展示了函数 Ch11_01_ex3() 的源代码。请注意 lambda 表达式 dodecahedron_area() 指定了 double& 类型的参数而非 double 类型。还需注意 dodecahedron_area() 计算了 x *= 15.0 * phi / std::sqrt(3.0 - phi)。此处使用引用意味着在执行 std::ranges::for_each(areas, dodecahedron_area) 后,std::array<double, 8> areas 包含的是计算后的十二面体面积而非边长。

void Ch11_01_ex3()
{
    using namespace std::numbers;
    const char* fmt = "{:9.2f} ";
    size_t epl_max {8};
    // using std::ranges::for_each to calculate dodecahedron areas
    std::array<double, 8> areas {};
    std::iota(areas.begin(), areas.end(), 1);
    MT::print_ctr("dodecahedron edge lengths:\n", areas, fmt, epl_max);
    auto dodecahedron_area = [](double& x)
        { x *= 15.0 * phi / std::sqrt(3.0 - phi); };
    std::ranges::for_each(areas, dodecahedron_area);
    MT::print_ctr("\ndodecahedron surface areas:\n", areas, fmt, epl_max);
    // using std::ranges::for_each to calculate dodecahedron volumes
    std::array<double, 8> volumes {};
    std::iota(volumes.begin(), volumes.end(), 1);
    auto dodecahedron_vol = [](double& x)
        { x *= 5.0 * phi * phi * phi / (6.0 - 2.0 * phi); };
    std::ranges::for_each(volumes, dodecahedron_vol);
    MT::print_ctr("\ndodecahedron volumes:\n", volumes, fmt, epl_max);
}

 Ch11_01_ex3() 中的第二个代码块利用 dodecahedron_vol() 和 std::ranges::for_each() 计算十二面体体积。与面积计算类似,执行 std::ranges::for_each() 后,std::array<double, 8> volumes 存储的是十二面体体积而非边长。以下是示例 Ch11_01 的运行结果:

----- Results for example Ch11_01 -----
----- Ch11_01_ex1() -----
sphere radii:
     1.00     2.00     3.00     4.00     5.00     6.00      7.00      8.00
sphere surface areas:
    12.57    50.27   113.10   201.06   314.16   452.39    615.75    804.25
sphere volumes:
     4.19    33.51   113.10   268.08   523.60   904.78   1436.76   2144.66
----- Ch11_01_ex2() -----
octahedron edge lengths:
     1.00     2.00     3.00     4.00     5.00      6.00     7.00      8.00
octahedron surface areas:
     3.46     13.86    31.18    55.43    86.60    124.71   169.74    221.70
octahedron volumes:
     0.47      3.77    12.73    30.17    58.93    101.82   161.69    241.36
----- Ch11_01_ex3() -----
dodecahedron edge lengths:
     1.00     2.00     3.00     4.00     5.00     6.00      7.00      8.00
dodecahedron surface areas:
    20.65    41.29    61.94    82.58   103.23   123.87    144.52    165.17
dodecahedron volumes:
     7.66    15.33    22.99    30.65    38.32    45.98     53.64     61.30

 

值得注意的是,示例 Ch11_01 中展示的所有 std::for_each() 算法都忽略了 lambda 表达式返回的任何值。对于需要利用返回值的场景,可以采用下一节将讨论的转换算法。

转换算法


转换算法会应用一元或二元函数对象到一个或两个输入范围。每个函数对象的计算结果随后被保存到输出范围。清单 11-2-1 展示了示例函数 Ch11_02_ex1() 的源代码,该函数演示了 std::transform() 和 std::ranges::transform() 的用法。

//-------------------------------------------------------------------------
// Ch11_02_ex.cpp
//-------------------------------------------------------------------------
#include <algorithm>
#include <deque>
#include <list>
#include <numbers>
#include <numeric>
#include <vector>
#include "Ch11_02.h"
#include "AminoAcid.h"
#include "MF.h"
#include "MT.h"
void Ch11_02_ex1()
{
    using namespace std::numbers;
    const char* fmt {"{:9.2f} "};
    constexpr size_t epl_max {8};
    // create test vector of radii
    std::vector<double> radii(8);
    std::iota(radii.begin(), radii.end(), 1);
    MT::print_ctr("sphere radii:\n", radii, fmt, epl_max);
    // using std::transform
    std::vector<double> areas(radii.size());
    auto sphere_area = [](double r) { return 4.0 * pi * r * r; };
    std::transform(radii.begin(), radii.end(), areas.begin(), sphere_area);
    MT::print_ctr("\nsphere surface areas:\n", areas, fmt, epl_max);
    // using std::ranges::transform
    std::vector<double> volumes(radii.size());
    auto sphere_vol = [](double r) { return 4.0 * pi * r * r * r / 3.0; };
    std::ranges::transform(radii, volumes.begin(), sphere_vol);
    MT::print_ctr("\nsphere volumes:\n", volumes, fmt, epl_max);
}

示例函数 Ch11_02_ex1() 以定义 std::vector<double> radii(8) 开始。随后的语句使用 std::iota() 来初始化 radii 的元素。接着定义了 lambda 表达式 sphere_area()。注意这个函数对象会计算并返回半径为 r 的球体表面积。在表达式 std::transform(radii.begin(), radii.end(), areas.begin(), sphere_area) 中,前两个参数——radii.begin() 和 radii.end()——指定了输入范围。参数 areas.begin() 则指定了输出范围的起始位置。注意 areas 的大小必须等于或大于 radii 的大小。 当前使用的 std::transform() 将 sphere_area() 应用于[radii.begin(), radii.end())中的每个半径元素,并将计算得到的每个面积保存到[areas.begin(), areas.end())的对应位置。

在随后的代码块中,表达式 std::ranges::transform(radii, volumes.begin(), sphere_vol) 计算球体体积并将这些值保存在 std::vector<double> volumes 中。与您已见过的其他 std::ranges 示例类似,容器名称 radii 指定了 std::ranges::transform() 的输入范围。

示例函数 Ch11_02_ex2(),如代码清单 11-2-2 所示,首先定义了包含多个氨基酸单字母小写代码的 std::vector<char> aa1。在随后的代码块中,执行 std::ranges::transform(aa1, aa1.begin(), tr_op1) 会对 aa1 中的每个元素应用 tr_op1()。注意该表达式中的输出迭代器是 aa1.begin(),这意味着当前使用的 std::ranges::transform() 会对 aa1 中的单字母代码执行原地的小写转大写转换。

void Ch11_02_ex2()
{
    const char* fmt1 {"'{:c}'   "};
    const char* fmt3 {"'{:3s}' "};
    constexpr size_t epl_max {12};
    // test sequence of amino acid codes (non-standard lower case)
    // (code 'x' is intentionally invalid)
    std::vector<char> aa1 {'a', 'g', 'l', 't', 'v', 'p', 'f', 'n', 'x', 'c', 'd'};
    MT::print_ctr("aa1 (initial values)\n", aa1, fmt1, epl_max);
    // using std::ranges::transform (inplace transformation)
    auto tr_op1 = [](char aa) { return MF::to_upper(aa); };
    std::ranges::transform(aa1, aa1.begin(), tr_op1);
    MT::print_ctr("\naa1 (after tansformation):\n", aa1, fmt1, epl_max);
    // using std::ranges::transform (non-inplace transformation)
    auto tr_op2 = [](char aa) { return AminoAcid::to_code3(aa); };
    std::deque<std::string> aa3 {};
    std::ranges::transform(aa1, std::back_inserter(aa3), tr_op2);
    MT::print_ctr("\naa3:\n", aa3, fmt3, epl_max);
}

Ch11_02_ex2() 中的后续代码块以 lambda 表达式 tr_op2() 的定义开始。该函数对象利用 AminoAcid::to_code3(aa) 将单字母氨基酸代码转换为三字母的 std::string。接着定义了 std::deque<std::string> aa3 {}。随后调用了 std::ranges::transform(aa1, std::back_inserter(aa3), tr_op2)。1 执行该语句会将[aa1.begin(), aa1.end())区间内的每个单字母代码转换为对应的三字母代码,并将结果保存在 aa3 中。这里需要注意输入和输出范围使用了不同的容器类型。

代码清单 11-2-3 展示了最终转换示例的代码,该示例使用两个输入范围执行转换操作。函数 Ch11_02_ex3() 的执行首先初始化两个名为 vec1 和 vec2 的 std::vector<std::string> 容器 。接着定义了二元运算符 tr_op(),该运算符使用参数 s1 和 s2 返回普通 std::string 连接的结果。

void Ch11_02_ex3()
{
    const char* fmt {"{:12s}  "};
    constexpr size_t epl_max {5};
    // initialize test vectors
    std::vector<std::string> vec1 {"one", "two",  "three", "four",   "five"};
    std::vector<std::string> vec2 {"un",  "deux", "trois", "quatre", "cinq"};
    MT::print_ctr("vec1:  ", vec1, fmt, epl_max);
    MT::print_ctr("\nvec2:  ", vec2, fmt, epl_max);
    // using std::ranges::transform (binary operator)
    auto tr_op = [](const std::string& s1, const std::string& s2)
        { return s1 + '-' + s2; };
    std::list<std::string> list1 {};
    std::ranges::transform(vec1, vec2, std::back_inserter(list1), tr_op);
    MT::print_ctr("\nlist1: ", list1, fmt, epl_max);
}

接下来在清单 11-2-3 中定义了 std::list<std::string> list1 {}。随后的语句 std::ranges::transform(vec1, vec2, std::back_inserter(list1), tr_op) 会使用[vec1.begin(), vec1.end())和[vec2.begin(), vec2.end())范围内的对应元素来应用 tr_op()。每个 tr_op() 的结果都会被保存到 list1 中。STL 算法 std::transform() 和 std::ranges::transform() 在多种使用场景中特别高效。在后续章节中您还会看到这些函数的更多示例。以下是源代码示例 Ch11_02 的运行结果:

----- Results for example Ch11_02 -----
----- Ch11_02_ex1() -----
sphere radii:
     1.00     2.00     3.00     4.00     5.00      6.00      7.00      8.00
sphere surface areas:
    12.57    50.27   113.10   201.06   314.16    452.39    615.75    804.25
sphere volumes:
     4.19    33.51   113.10   268.08   523.60    904.78   1436.76   2144.66
----- Ch11_02_ex2() -----
aa1 (initial values)
'a'   'g'   'l'   't'   'v'   'p'   'f'   'n'   'x'   'c'   'd'
aa1 (after tansformation):
'A'   'G'   'L'   'T'   'V'   'P'   'F'   'N'   'X'   'C'   'D'
aa3:
'Ala' 'Gly' 'Leu' 'Thr' 'Val' 'Pro' 'Phe' 'Asn' '???' 'Cys' 'Asp'
----- Ch11_02_ex3() -----
vec1:  one           two           three         four          five
vec2:  un            deux          trois         quatre        cinq
list1: one-un        two-deux      three-trois   four-quatre   five-cinq

生成算法


生成算法通过将连续用户自定义函数的返回值依次赋给区间内的每个元素。这些函数与 std::iota() 有些相似,但正如稍后所见,它们提供了更强的算法灵活性。

代码清单 11-3-1 展示了 std::generate() 的简单示例。其中 Ch11_03_ex1() 函数首先声明了 std::vector<double> vec1(20),随后定义了 lambda 表达式 gen_op = [next_val = 0.0]() mutable { double x = next_val; next_val += 0.25; return x; }。连续调用 gen_op() 将依次返回 0.00、0.25、0.50 等值。

//-------------------------------------------------------------------------
// Ch11_03_ex.cpp
//-------------------------------------------------------------------------
#include <algorithm>
#include <fstream>
#include <vector>
#include "Ch11_03.h"
#include "MF.h"
#include "MT.h"
#include "MTH.h"
void Ch11_03_ex1()
{
    const char* fmt = "{:8.2f}";
    constexpr size_t epl_max {10};
    // using std:generate
    // note: no arguments allowed for gen_op()
    std::vector<double> vec1(20);
    auto gen_op = [next_val = 0.0]() mutable
        { double x = next_val; next_val += 0.25; return x; };
    std::generate(vec1.begin(), vec1.end(), gen_op);
    MT::print_ctr("vec1:\n", vec1, fmt, epl_max);
}

在 gen_op() 定义之后是语句 std::generate(vec1.begin(), vec1.end(), gen_op)。执行该语句会为区间[vec1.begin(), vec1.end())中的每个元素调用 gen_op()。传递给 std::generate() 的生成函数必须不带任何参数,这就是为什么 next_val 被定义为 gen_op() 中的局部变量。

在示例 Ch11_03_ex2() 中(如代码清单 11-3-2 所示),lambda 表达式 gen_op() 包含了一些任意逻辑来模拟更复杂的生成函数。再次注意,lambda 表达式 gen_op() 利用局部变量在连续调用之间保持数据状态。

void Ch11_03_ex2()
{
    const char* fmt = "{:2c}";
    constexpr size_t epl_max {30};
    // using std::ranges::generate
    std::vector<char> vec1(26);
    auto gen_op = [next_char = 'A']() mutable
        { char c = next_char++; return (c & 0x01) ? c : MF::to_lower(c); };
    std::ranges::generate(vec1, gen_op);
    MT::print_ctr("vec1: ", vec1, fmt, epl_max);
}

清单 11-3-3-1 展示了示例 Ch11_03_ex3() 的源代码。该函数以一系列参数开头,这些参数由 MTH::generate_sine_wave() 用于生成离散正弦波的数据点。稍后将详细介绍这一点。由 MTH::generate_sine_wave() 生成的结果数据点(利用了 std::ranges::generate_n())被保存在 std::vector<double> vec1 中。Ch11_03_ex3() 中的剩余代码将 vec1 中的数据点保存到 CSV 文件中。

void Ch11_03_ex3()
{
    constexpr double amplitude {1.0};
    constexpr double frequency {5.0};
    constexpr double phase {0.0};
    constexpr double t_start {0.0};
    constexpr double t_end {1.0};
    constexpr double t_step {0.001};
    // create sine wave vector (uses std::generate_n)
    std::vector<double> vec1 = MTH::generate_sine_wave(amplitude, frequency,
        phase, t_start, t_end, t_step);
    std::println("vec.size(): {:d}", vec1.size());
    // write sine wave vector to CSV file
    std::string fn = MF::mk_test_filename("ch11_03_ex2.csv");
    std::ofstream ofs {fn, std::ios_base::out | std::ios_base::trunc};
    if (!ofs.good())
    {
        std::println("file open failed: {:s}", fn);
        return;
    }
    double t {t_start};
    for (auto v : vec1)
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

akluse

失业老程序员求打赏,求买包子钱

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

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

打赏作者

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

抵扣说明:

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

余额充值