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

在本书至此,您已学习了如何使用多种STL容器类,包括序列容器、关联容器和无序关联容器。现代C++编程还要求对STL算法有全面理解。本章将开启您对STL算法库的学习之旅。本章中,您将掌握如何运用一系列基础且实用的STL算法,包括:

计数算法
最小值与最大值算法
复制算法
移动算法
反转算法
替换算法
移除算法
填充算法

本章精选的这些算法也将为您后续章节中将要学习的高级算法奠定基础。

算法入门

STL算法库是一个庞大的模板函数集合,用于对容器或范围执行操作。STL算法主要分为两大类:第一类是由C++11标准规范化的算法。这类算法通常通过一对迭代器指定的元素范围来执行操作。例如,执行std::sort(ctr.begin(), ctr.end())会对容器ctr中[ctr.begin(), ctr.end())区间内的元素(即ctr中的所有元素)进行排序。使用迭代器参数的优势在于可以直观地指定算法操作的容器子范围。但缺点是程序往往需要算法对整个容器的元素集合进行操作,而反复为每个算法指定ctr.begin()和ctr.end()不仅需要大量额外输入,还存在潜在的错误风险。

STL算法的第二类别包含C++20及后续版本中的范围(或约束)变体,这些算法定义在std::ranges命名空间中。这类算法支持直接使用容器名称作为参数。例如,std::ranges::sort(ctr)可对容器ctr中的元素进行排序。此处的参数ctr是C++中对[ctr.begin(), ctr.end())范围的简写表示。std::ranges中定义的大部分算法还支持投影等附加功能,这些内容将在第14章详述。

在阅读本书讲解STL算法的章节时,请特别注意以下几点:
某些容器会定义与全局算法功能相同的成员函数。例如反转std::list元素时,既可使用成员函数std::list::reverse(),也可调用全局算法std::reverse()。在条件允许时,应优先选用容器专属算法,因为这些函数通常针对容器内部数据结构进行了优化。

大多数C++ STL算法都包含多重重载版本。这些重载通常通过参数来修改算法的默认行为。本书的源代码示例展示了多种STL算法用法,但建议始终参考权威的C++参考资料以了解每个STL算法的其他重载选项。

STL算法并非适用于所有容器类,这主要取决于算法所需的迭代器类型。例如std::sort()要求随机访问迭代器,因此该算法不能用于std::list或std::forward_list。会修改容器元素的STL算法通常也不适用于关联容器或无序关联容器,因为这类容器依靠元素值来确定其在树形结构或哈希表中的位置。

定义在命名空间std和std::ranges中的全局算法不能用于添加或删除容器元素。必须使用容器成员函数来执行这些操作。

切勿对STL算法与自定义代码的性能做出任何假设。始终通过基准测试来验证任何预期的性能提升。第16章将对此进行更详细的阐述。

计数算法

源代码示例Ch10_01重点展示了几种计数类算法的应用。第一组算法包括std::count()、std::ranges::count()、std::count_if()和std::ranges::count_if()。前两者统计容器中元素出现的次数,后两者则对用户定义的谓词函数返回true的元素进行计数。第二组涵盖std::any_of()、std::all_of()和std::none_of()算法,以及std::ranges命名空间中的对应算法。这些算法对容器每个元素应用一元谓词,并根据谓词对部分、全部或零个元素返回true的情况来判定结果。

清单10-1-1展示了示例函数Ch10_01_ex1()的源代码,该函数演示了std::count()的用法。函数起始代码块实例化并初始化了std::vector<int> vec1。随后的代码块中,表达式count10 = std::count(vec1.begin(), vec1.end(), 10)用于统计vec1中等于10的元素数量。更准确地说,std::count()统计的是区间[vec1.begin(), vec1.end())内等于10的元素个数。该算法支持容器迭代器子范围的使用,例如执行std::count(vec1.begin() + 2, vec1.end(), 10)将会跳过vec1的前两个元素。不出所料,std::count()通过元素类型定义的operator==运算符进行比较操作。

//-------------------------------------------------------------------------
// Ch10_01_ex.cpp
//-------------------------------------------------------------------------
#include <algorithm>
#include <deque>
#include <set>
#include <vector>
#include <utility>
#include "Ch10_01.h"
#include "AminoAcid.h"
#include "MT.h"
namespace
{
    const std::initializer_list<int> c_Numbers { 10, 20, 30, -10, 40,
        50, 80, 10,
        50, -60, 10, 80, -90, 10, 80, 90, 60, 120, 90, 80, 60, 10,
        -20, -70 };
};
void Ch10_01_ex1()
{
    const char* fmt = "{:5d}";
    constexpr size_t epl_max {12};
    // using std::count with std::vector
    std::vector<int> vec1 {c_Numbers};
    MT::print_ctr("\nvec1:\n", vec1, fmt, epl_max);
    auto count10 = std::count(vec1.begin(), vec1.end(), 10);
    auto count75 = std::ranges::count(vec1, 75);
    std::println("\ncount10: {:d}", count10);
    std::println("count70: {:d}", count75);
    // using std::count with std::multiset
    // (elements ordered in ascending order)
    std::multiset<int> mset1 {c_Numbers};
    MT::print_ctr("\nmset1:\n", mset1, fmt, epl_max);
    auto count40 = std::count(mset1.begin(), mset1.end(), 40);
    auto count50 = std::ranges::count(mset1, 50);
    std::println("\ncount40: {:d}", count40);
    std::println("count80: {:d}", count50);
}

使用迭代器为std::count()指定范围的灵活性,在低频使用场景中颇为便利。但对于绝大多数应用场景而言,开发者通常希望像std::count()这样的算法能对整个容器元素执行操作。Ch10_01_ex1()中的下一条语句count75 = std::ranges::count(vec1, 75)用于统计vec1中等于75的元素数量。此表达式中,变量名vec1本质上是范围[vec1.begin(), vec1.end())的简写形式。更值得注意的是std::ranges命名空间的使用——正如前文所述,该命名空间定义了大多数C++20之前算法的约束版本。

清单10-1-1中的下一个代码块展示了使用std::count()和std::ranges::count()统计std::multiset<int> mset1中匹配元素的案例。这里需要特别强调的是:Ch10_01_ex1()函数通过这两个算法统计multiset匹配元素的方式,与其前一个代码块统计vector匹配元素的方式完全一致。这正是STL(标准模板库)传递的根本优势——其算法可应用于多种容器类型。

若提前查看Ch10_01示例的结果输出,会注意到MT::print_ctr()对vec1和mset1打印了不同的元素排序。需要明确的是:与std::vector不同,std::multiset会按照第7章所述规则对其元素进行排序,这种排序特性会通过MT::print_ctr()函数使用范围for循环(参见Common/MT.h文件)打印容器元素时体现出来。

如清单10-1-2所示的源码示例Ch10_01_ex2(),演示了std::count_if()和std::ranges::count_if()的用法。该函数的起始代码块定义了两个一元谓词neg_pred()和div30_pred():前者在x为负数时返回true,后者在x能被30整除时返回true。随后是对std::deque<int> deq1的实例化与初始化操作。

void Ch10_01_ex2()
{
    const char* fmt = "{:5d}";
    constexpr size_t epl_max {12};
    // predicates for std::count_if
    auto neg_pred = [](int x) { return x < 0; };
    auto div30_pred = [](int x) { return x % 30 == 0; };
    // using std::count_if
    std::deque<int> deq1 {c_Numbers};
    MT::print_ctr("\ndeq1:\n", deq1, fmt, epl_max);
    auto num_neg = std::count_if(deq1.begin(), deq1.end(), neg_pred);
    std::println("\nnum_neg: {:d}", num_neg);
    // using std::ranges::count_if
    auto num_div30 = std::ranges::count_if(deq1, div30_pred);
    std::println("num_div30: {:d}", num_div30);
}

根据谓词neg_pred()和div30_pred()的定义,Ch10_01_ex2()通过std::count_if(deq1.begin(), deq1.end(), neg_pred)统计deq1中的负值数量。与std::count()类似,算法std::count_if()支持使用迭代器子范围进行操作(例如std::count_if(deq1.begin()+2, deq1.end()-1, neg_pred))。随后的代码块运用std::ranges::count_if(deq1, div30_pred)计算deq1中能被30整除的元素数量。

目前展示的STL算法示例均应用于存储int类型元素的各种容器。实际应用通常涉及大量用户自定义类。接下来的示例函数Ch10_01_ex3()将使用名为AminoAcid(氨基酸)的用户自定义类。

氨基酸是构成蛋白质的基本单元。从化学结构看,氨基酸包含氨基(氮氢基团)、中心α碳原子和羧基(碳氧氢基团)。连接在中心α碳原子上的侧链由额外元素(氢、碳、氮、氧、硫)组成,这些侧链决定了不同氨基酸的差异。图10-1展示了氨基酸的通用结构形式。人类基因组直接编码20种标准氨基酸,每种标准氨基酸通过正式名称、单字母代码和三字母代码进行标识。例如,甘氨酸在科学文献中可通过其名称及单字母代码G、三字母代码Gly进行识别。

清单10-1-3-1展示了类AminoAcid的定义源代码。该类在Ch10_01_ex3()中被使用,后续章节也会用到。在文件AminoAcid.h的底部列出了该类的属性。需注意每个AminoAcid实例都包含m_Name、m_Code1和m_Code3属性,分别对应氨基酸的名称及其两种编码。其他属性还包括分子量m_MolMass和侧链(极性)类型m_SideChain。值得注意的是,枚举类型AminoAcid::SC定义了有效的侧链类型。类AminoAcid选择包含m_MolarMass和m_SideChain属性,是为了代表氨基酸众多化学特性(包括定性和定量特征)中的典型示例。 

//-------------------------------------------------------------------------
// AminoAcid.h
//-------------------------------------------------------------------------
#ifndef AMINIO_ACID_H_
#define AMINIO_ACID_H_
#include <cstddef>
#include <format>
#include <optional>
#include <string>
#include <tuple>
#include <vector>
// AA tuple <3-letter code, full name 1-letter code, molecular mass>
using AaTuple = std::tuple<std::string, std::string, char, double>;

class AminoAcid {
    friend struct std::formatter<AminoAcid>;
    static constexpr unsigned int s_RngSeedDefault{1001};

public:
    // amino acid side chain types
    enum class SC : unsigned int { Unknown, Acidic, Basic, NonPolar, UnchargedPolar };

    static constexpr char BadCode1{'?'};
    static constexpr const char *BadCode3 = "???";

    // constructors
    AminoAcid() = default;

    explicit AminoAcid(const char *name, char code1, const char *code3,
                       double mol_mass, SC side_chain) : m_Name{name}, m_Code1{code1},
                                                         m_Code3{code3},
                                                         m_MolMass{mol_mass}, m_SideChain{side_chain} {
    };
    // accessors
    const std::string &Name() const { return m_Name; }
    char Code1() const { return m_Code1; }
    std::string Code3() const { return m_Code3; }
    double MolMass() const { return m_MolMass; }
    SC SideChain() const { return m_SideChain; }
    // operators
    friend auto operator<=>(const AminoAcid &aa1, const AminoAcid &aa2) { return aa1.m_Name <=> aa2.m_Name; }
    friend bool operator==(const AminoAcid &aa1, const AminoAcid &aa2) { return aa1.m_Name == aa2.m_Name; }

    // helper functions (see AminoAcid.cpp)
    static std::optional<AminoAcid> find(char code1);

    static std::optional<AminoAcid> find(const std::string &code3);

    static bool is_valid(char code1);

    static bool is_valid(const std::string &code3);

    static char to_code1(const std::string &code3);

    static std::string to_code3(char code1);

    static std::string to_string(SC side_chain, bool short_text = true);

    // vector generators (see AminoAcid.cpp)
    static std::vector<AminoAcid> get_vector_all();

    static std::vector<char> get_vector_all_code1();

    static std::vector<std::string> get_vector_all_code3();

    static std::vector<double> get_vector_all_mol_mass();

    static std::vector<std::string> get_vector_all_name();

    static std::vector<char> get_vector_random_code1(size_t num_aa,
                                                     unsigned int rng_seed = s_RngSeedDefault);

    static std::vector<std::string> get_vector_random_code3(size_t num_aa,
                                                            unsigned int rng_seed = s_RngSeedDefault);

    static std::vector<AaTuple> get_vector_tuple();

private:
    std::string to_str() const;

    std::string m_Name{}; // full name
    char m_Code1{}; // 1-letter code
    std::string m_Code3{}; // 3-letter code
    double m_MolMass{}; // molecular mass (g/mol)
    SC m_SideChain{SC::Unknown}; // side chain type
};

// class AminoAcid formatter
template<>
struct std::formatter<AminoAcid> : std::formatter<std::string> {
    constexpr auto parse(std::format_parse_context &fpc) { return fpc.begin(); }

    auto format(const AminoAcid &aa, std::format_context &fc) const {
        return std::format_to(fc.out(), "{:s}", aa.to_str());
    }
};
#endif

注意在清单10-1-3-1中,AminoAcid类在执行operator<=>和operator==运算符时会比较m_Name属性。运算符后面是一系列辅助函数和向量生成函数的声明。稍后会详细介绍这些内容。

清单10-1-3-2展示了AminoAcid类的定义。目前请注意以下内容:在AminoAcid.cpp文件顶部附近有一个匿名命名空间,其中定义了包含20种标准氨基酸的std::vector<AminoAcid>。大约在清单中间位置,定义了一系列以get_vector_为前缀的函数。这些函数都会构建一个氨基酸属性的std::vector。清单10-1-3-2中的最后一个函数AminoAcid::to_str()会被AminoAcid类的格式化程序调用,用于创建包含AminoAcid属性的std::string。

//-------------------------------------------------------------------------
// AminoAcid.cpp
//-------------------------------------------------------------------------
#include <algorithm>
#include <format>
#include <optional>
#include <random>
#include "AminoAcid.h"
using SC = AminoAcid::SC;

namespace {
    // standard amino acids
    const std::vector<AminoAcid> c_AminoAcids
    {
        AminoAcid{"Alanine", 'A', "Ala", 89.094, SC::NonPolar},
        AminoAcid{"Arginine", 'R', "Arg", 174.203, SC::Basic},
        AminoAcid{"Asparagine", 'N', "Asn", 132.119, SC::UnchargedPolar},
        AminoAcid{"AsparticAcid", 'D', "Asp", 133.104, SC::Acidic},
        AminoAcid{"Cysteine", 'C', "Cys", 121.154, SC::NonPolar},
        AminoAcid{"Glutamine", 'Q', "Gln", 146.146, SC::UnchargedPolar},
        AminoAcid{"GlutamicAcid", 'E', "Glu", 147.131, SC::Acidic},
        AminoAcid{"Glycine", 'G', "Gly", 75.067, SC::NonPolar},
        AminoAcid{"Histidine", 'H', "His", 155.156, SC::Basic},
        AminoAcid{"IsoLeucine", 'I', "Ile", 131.175, SC::NonPolar},
        AminoAcid{"Leucine", 'L', "Leu", 131.175, SC::NonPolar},
        AminoAcid{"Lysine", 'K', "Lys", 146.189, SC::Basic},
        AminoAcid{"Methionine", 'M', "Met", 149.208, SC::NonPolar},
        AminoAcid{"Phenylalanine", 'F', "Phe", 165.192, SC::NonPolar},
        AminoAcid{"Proline", 'P', "Pro", 115.132, SC::NonPolar},
        AminoAcid{"Serine", 'S', "Ser", 105.093, SC::UnchargedPolar},
        AminoAcid{"Threonine", 'T', "Thr", 119.119, SC::UnchargedPolar},
        AminoAcid{"Tryptophan", 'W', "Trp", 204.228, SC::NonPolar},
        AminoAcid{"Tyrosine", 'Y', "Tyr", 181.191, SC::UnchargedPolar},
        AminoAcid{"Valine", 'V', "Val", 117.148, SC::NonPolar},
    };
};

// helper functions
std::optional<AminoAcid> AminoAcid::find(char code1) {
    auto pred = [code1](const AminoAcid &aa) {
        return aa.Code1() ==
               code1;
    };
    auto iter = std::ranges::find_if(c_AminoAcids, pred);
    return (iter != c_AminoAcids.end()) ? std::optional(*iter) : std::nullopt;
}

std::optional<AminoAcid> AminoAcid::find(const std::string &code3) {
    auto pred = [code3](const AminoAcid &aa) {
        return aa.Code3() ==
               code3;
    };
    auto iter = std::ranges::find_if(c_AminoAcids, pred);
    return (iter != c_AminoAcids.end()) ? std::optional(*iter) : std::nullopt;
}

bool AminoAcid::is_valid(char code1) {
    std::optional<AminoAci
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

akluse

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

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

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

打赏作者

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

抵扣说明:

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

余额充值