初学编程者往往会花费大量时间编写自定义循环来完成一些相对简单的任务,例如对数组进行排序、计数或查找。这些循环不仅容易出错,而且在可维护性方面也存在问题,因为循环代码往往难以理解。
由于搜索、计数和排序是极其常见的操作,C++ 标准库提供了一系列函数,仅需寥寥数行代码即可完成上述任务。此外,这些标准库函数已经过充分测试,具备高效性,适用于多种不同类型的容器,其中许多还支持并行化(即利用多个 CPU 线程同时处理同一任务,以加快完成速度)。
算法库中的功能通常可分为三类:
- 检视器(Inspectors):用于查看(但不修改)容器中的数据,例如搜索和计数。
- 修改器(Mutators):用于修改容器中的数据,例如排序和洗牌。
- 辅助器(Facilitators):用于根据数据成员的值生成结果,例如将值相乘的对象,或决定元素对排序顺序的对象。
这些算法均位于 <algorithm>
头文件中。本节将探讨一些常见算法——但库中算法远不止这些,建议您阅读后续链接的参考文档,了解全部可用功能!
注意:所有这些算法均使用迭代器。如果您对迭代器尚不熟悉,请先复习本节——迭代器简介。
使用 std::find
按值查找元素
std::find
用于在容器中查找某个值的首次出现。它接收三个参数:序列起始元素的迭代器、序列结束元素的迭代器,以及待查找的值。若找到该值,则返回指向该元素的迭代器;若未找到,则返回容器末尾迭代器。
示例:
#include <algorithm>
#include <array>
#include <iostream>
int main()
{
std::array arr{ 13, 90, 99, 5, 40, 80 };
std::cout << "请输入要查找并替换的值:";
int search{};
int replace{};
std::cin >> search >> replace;
// 此处省略输入校验
// std::find 返回指向找到元素的迭代器(若未找到则返回 end())
// 我们用 auto 推导迭代器类型,因为我们不关心具体类型
auto found{ std::find(arr.begin(), arr.end(), search) };
// 若算法未找到目标,则返回 end() 迭代器
if (found == arr.end())
{
std::cout << "未能找到 " << search << '\n';
}
else
{
// 覆盖找到的元素
*found = replace;
}
for (int i : arr)
{
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}
当元素被找到时的示例运行:
请输入要查找并替换的值:5 234
13 90 99 234 40 80
当元素未被找到时的示例运行:
请输入要查找并替换的值:0 234
未能找到 0
13 90 99 5 40 80
使用 std::find_if
按条件查找元素
有时我们需要查找满足特定条件的元素(例如包含特定子串的字符串),而非精确值。此时 std::find_if
非常合适。
std::find_if
的用法与 std::find
类似,但第三个参数不再是具体值,而是一个可调用对象,例如函数指针(或稍后介绍的 lambda)。算法对每个元素调用该对象(将元素作为参数传入),若返回 true,则表示匹配成功。
以下示例使用 std::find_if
检查是否有元素包含子串 “nut”:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
// 该函数在匹配时返回 true
bool containsNut(std::string_view str)
{
// std::string_view::find 若未找到子串则返回 std::string_view::npos
// 否则返回子串在 str 中的起始索引
return str.find("nut") != std::string_view::npos;
}
int main()
{
std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" };
// 扫描数组,查看是否有元素包含 "nut" 子串
auto found{ std::find_if(arr.begin(), arr.end(), containsNut) };
if (found == arr.end())
{
std::cout << "未找到坚果\n";
}
else
{
std::cout << "找到 " << *found << '\n';
}
return 0;
}
输出:
Found walnut
若手写上述逻辑,至少需要三层循环(一层遍历数组,两层匹配子串)。而标准库函数仅需几行即可完成!
使用 std::count
和 std::count_if
统计出现次数
std::count
与 std::count_if
用于统计满足特定值或特定条件的元素出现次数。
以下示例统计包含子串 “nut” 的元素个数:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
bool containsNut(std::string_view str)
{
return str.find("nut") != std::string_view::npos;
}
int main()
{
std::array<std::string_view, 5> arr{ "apple", "banana", "walnut", "lemon", "peanut" };
auto nuts{ std::count_if(arr.begin(), arr.end(), containsNut) };
std::cout << "共统计到 " << nuts << " 个坚果\n";
return 0;
}
输出:
Counted 2 nut(s)
使用 std::sort
自定义排序
我们之前曾用 std::sort
对数组进行升序排序,但 std::sort
功能远不止于此。其重载版本接收第三个参数——比较函数,允许我们按任意规则排序。该函数接收两个待比较参数,若第一个参数应排在第二个参数之前,则返回 true。默认情况下,std::sort
按升序排列。
以下示例使用名为 greater
的自定义比较函数,将数组按降序排序:
#include <algorithm>
#include <array>
#include <iostream>
bool greater(int a, int b)
{
// 若 a 大于 b,则将 a 排在前面
return (a > b);
}
int main()
{
std::array arr{ 13, 90, 99, 5, 40, 80 };
// 将 greater 传给 std::sort
std::sort(arr.begin(), arr.end(), greater);
for (int i : arr)
{
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}
输出:
99 90 80 40 13 5
同样,无需手写复杂循环,仅需几行即可完成任意排序!
我们的 greater
函数需要两个参数,但调用时并未显式传入,它们从何而来?当我们使用不带括号的函数名时,仅传入函数指针,而非调用函数。回忆之前打印函数名时 std::cout
输出 “1” 的情形。std::sort
利用该指针,在内部使用数组中的任意两个元素调用实际的 greater
函数。我们无法预知具体会传入哪些元素,因为 std::sort
底层使用的排序算法并未公开。后续章节将进一步讨论函数指针。
提示
由于降序排序极为常见,C++ 提供了名为 std::greater
的现成类型(位于 <functional>
头文件)。上述示例中,可将:
std::sort(arr.begin(), arr.end(), greater); // 调用自定义 greater 函数
替换为:
std::sort(arr.begin(), arr.end(), std::greater{}); // 使用标准库的 greater 比较器
// C++17 之前需指定元素类型
std::sort(arr.begin(), arr.end(), std::greater<int>{}); // 使用标准库的 greater 比较器
注意 std::greater{}
需使用花括号,因为它并非可调用函数,而是一个类型。要使用它,必须实例化该类型的对象。花括号会创建一个该类型的匿名对象(随后作为参数传给 std::sort
)。
供进阶读者阅读
为进一步说明 std::sort
如何使用比较函数,我们回到本节《使用选择排序对数组排序》中的修改版选择排序示例:
#include <iostream>
#include <iterator>
#include <utility>
void sort(int* begin, int* end)
{
for (auto startElement{ begin }; startElement != end-1; ++startElement)
{
auto smallestElement{ startElement };
// std::next 返回指向下一个元素的指针,与 (startElement + 1) 类似
for (auto currentElement{ std::next(startElement) }; currentElement != end; ++currentElement)
{
if (*currentElement < *smallestElement)
{
smallestElement = currentElement;
}
}
std::swap(*startElement, *smallestElement);
}
}
int main()
{
int array[]{ 2, 1, 9, 4, 5 };
sort(std::begin(array), std::end(array));
for (auto i : array)
{
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}
到目前为止,这段代码并无新意,且始终按升序排序。为添加比较函数,需引入新类型 std::function<bool(int, int)>
,用于存储接收两个 int 参数并返回 bool 的函数。目前请将其视为“黑魔法”,第 20 章会详细解释。
void sort(int* begin, int* end, std::function<bool(int, int)> compare)
现在可以向 sort
传入诸如 greater
的比较函数,但 sort
如何使用它?只需将:
if (*currentElement < *smallestElement)
替换为:
if (compare(*currentElement, *smallestElement))
至此,调用者即可决定如何比较两个元素。
完整示例:
#include <functional> // std::function
#include <iostream>
#include <iterator>
#include <utility>
// sort 接收一个比较函数
void sort(int* begin, int* end, std::function<bool(int, int)> compare)
{
for (auto startElement{ begin }; startElement != end-1; ++startElement)
{
auto smallestElement{ startElement };
for (auto currentElement{ std::next(startElement) }; currentElement != end; ++currentElement)
{
// 使用比较函数判断当前元素是否应排在当前“最小”元素之前
if (compare(*currentElement, *smallestElement))
{
smallestElement = currentElement;
}
}
std::swap(*startElement, *smallestElement);
}
}
int main()
{
int array[]{ 2, 1, 9, 4, 5 };
// 使用 std::greater 按降序排序
// (需使用全局作用域限定符,防止与 std::sort 冲突)
::sort(std::begin(array), std::end(array), std::greater{});
for (auto i : array)
{
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}
使用 std::for_each
对容器中所有元素执行操作
std::for_each
接收一个区间,并对每个元素应用自定义函数。当需要对列表中所有元素执行相同操作时,该算法非常有用。
以下示例使用 std::for_each
将数组中所有数字翻倍:
#include <algorithm>
#include <array>
#include <iostream>
void doubleNumber(int& i)
{
i *= 2;
}
int main()
{
std::array arr{ 1, 2, 3, 4 };
std::for_each(arr.begin(), arr.end(), doubleNumber);
for (int i : arr)
{
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}
输出:
2 4 6 8
新手开发者常认为该算法多余,因为用范围 for 循环写更短更易。但 std::for_each
有其优势。以下对比:
std::ranges::for_each(arr, doubleNumber); // C++20 起,无需 begin() 和 end()
// std::for_each(arr.begin(), arr.end(), doubleNumber); // C++20 之前
for (auto& i : arr)
{
doubleNumber(i);
}
使用 std::for_each
时,意图清晰:用 arr
的每个元素调用 doubleNumber
。而在范围 for 循环中,需引入新变量 i
,这容易导致多种错误:若未使用 auto
可能发生隐式转换;忘记写 &
会导致 doubleNumber
无法修改数组;可能误将其他变量传给 doubleNumber
。std::for_each
可避免这些失误。
此外,std::for_each
可跳过容器的起始或末尾元素。例如跳过 arr
的首元素,可用 std::next
将起始迭代器前移:
std::for_each(std::next(arr.begin()), arr.end(), doubleNumber);
// 此时 arr 为 [1, 4, 6, 8],首元素未被翻倍
范围 for 循环无法实现此功能。
与许多算法一样,std::for_each
可并行化以加速处理,因此在大规模项目及大数据场景中优于范围 for 循环。
性能与执行顺序保证
算法库中的许多算法会对执行方式作出某种保证,通常是性能保证,或是执行顺序保证。例如,std::for_each
保证每个元素仅被访问一次,且按前向顺序依次访问。
尽管大多数算法都提供某种性能保证,但提供执行顺序保证的算法较少。对于后者,我们需谨慎,切勿对元素被访问或处理的顺序作出假设。
例如,若使用标准库算法将第一个值乘以 1,第二个值乘以 2,第三个值乘以 3,以此类推,则应避免使用任何不保证前向顺序执行的算法!
以下算法保证顺序执行:std::for_each
、std::copy
、std::copy_backward
、std::move
和 std::move_backward
。许多其他算法(特别是要求前向迭代器的算法)因迭代器要求而隐式顺序执行。
最佳实践
使用特定算法前,请确保其性能与执行顺序保证符合您的实际需求。
C++20 中的 Ranges
每次调用算法时都显式传递 arr.begin()
和 arr.end()
颇为繁琐。C++20 引入了 Ranges,允许直接传入 arr
,代码更简洁易读。
结论
算法库提供了大量实用功能,可简化代码并增强健壮性。本节仅涵盖一小部分,但由于这些函数用法相似,掌握几个后,其余亦可触类旁通。
附注
该视频简洁地解释了库中多种算法。
最佳实践
优先使用算法库中的函数,而非手写相同功能的代码。